From 3c70b99368b9411b7f047b9b029b3bdcf6096b73 Mon Sep 17 00:00:00 2001 From: Imanol Fernandez Date: Tue, 2 Jun 2020 01:00:28 +0200 Subject: [PATCH] OpenXR implementation (#3428) --- README.md | 7 + app/CMakeLists.txt | 38 +- app/build.gradle | 35 +- app/src/main/cpp/SplashAnimation.cpp | 4 +- app/src/main/cpp/native-lib.cpp | 20 +- app/src/openxr/cpp/DeviceDelegateOpenXR.cpp | 1036 +++++++++++++++++++ app/src/openxr/cpp/DeviceDelegateOpenXR.h | 74 ++ app/src/openxr/cpp/OpenXRExtensions.cpp | 31 + app/src/openxr/cpp/OpenXRExtensions.h | 18 + app/src/openxr/cpp/OpenXRHelpers.h | 120 +++ app/src/openxr/cpp/OpenXRInput.cpp | 464 +++++++++ app/src/openxr/cpp/OpenXRInput.h | 67 ++ app/src/openxr/cpp/OpenXRLayers.cpp | 221 ++++ app/src/openxr/cpp/OpenXRLayers.h | 346 +++++++ app/src/openxr/cpp/OpenXRSwapChain.cpp | 178 ++++ app/src/openxr/cpp/OpenXRSwapChain.h | 54 + 16 files changed, 2689 insertions(+), 24 deletions(-) create mode 100644 app/src/openxr/cpp/DeviceDelegateOpenXR.cpp create mode 100644 app/src/openxr/cpp/DeviceDelegateOpenXR.h create mode 100644 app/src/openxr/cpp/OpenXRExtensions.cpp create mode 100644 app/src/openxr/cpp/OpenXRExtensions.h create mode 100644 app/src/openxr/cpp/OpenXRHelpers.h create mode 100644 app/src/openxr/cpp/OpenXRInput.cpp create mode 100644 app/src/openxr/cpp/OpenXRInput.h create mode 100644 app/src/openxr/cpp/OpenXRLayers.cpp create mode 100644 app/src/openxr/cpp/OpenXRLayers.h create mode 100644 app/src/openxr/cpp/OpenXRSwapChain.cpp create mode 100644 app/src/openxr/cpp/OpenXRSwapChain.h diff --git a/README.md b/README.md index c5c002b9e..39d891a34 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,13 @@ npm install npm run compress ``` +## Enable OpenXR builds +You can enable OpenXR API for Oculus by adding this property to your `user.properties` file: + +```ini +openxr=true +``` + ## Development troubleshooting ### `Device supports , but APK only supports armeabi-v7a[...]` diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index c091c9372..8f1c67d06 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -82,7 +82,34 @@ target_link_libraries(native-lib native_app_glue) set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate") endif() -if(OCULUSVR) + +if(OPENXR) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DXR_USE_PLATFORM_ANDROID -DXR_USE_GRAPHICS_API_OPENGL_ES") +include_directories( + ${CMAKE_SOURCE_DIR}/../third_party/OpenXR-SDK/include + ${CMAKE_SOURCE_DIR}/../third_party/ovr_openxr_mobile_sdk/OpenXR/Include + ${CMAKE_SOURCE_DIR}/../app/src/openxr/cpp +) +target_sources( + native-lib + PUBLIC + src/openxr/cpp/DeviceDelegateOpenXR.cpp + src/openxr/cpp/OpenXRSwapChain.cpp + src/openxr/cpp/OpenXRLayers.cpp + src/openxr/cpp/OpenXRInput.cpp + src/openxr/cpp/OpenXRExtensions.cpp +) +add_custom_command(TARGET native-lib POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_SOURCE_DIR}/../third_party/ovr_openxr_mobile_sdk/OpenXR/Libs/Android/${ANDROID_ABI}/Release/libopenxr_oculus.so + ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libopenxr_oculus.so + ) +elseif(OCULUSVR) +include_directories( + ${CMAKE_SOURCE_DIR}/../third_party/ovr_mobile/VrApi/Include + ${CMAKE_SOURCE_DIR}/../third_party/OVRPlatformSDK/Include + ${CMAKE_SOURCE_DIR}/../app/src/oculusvr/cpp +) target_sources( native-lib PUBLIC @@ -95,6 +122,9 @@ add_custom_command(TARGET native-lib POST_BUILD ${CMAKE_SOURCE_DIR}/../third_party/ovr_mobile/VrApi/Libs/Android/${ANDROID_ABI}/Release/libvrapi.so ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libvrapi.so ) +endif() + +if(OCULUSVR) add_custom_command(TARGET native-lib POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/../third_party/OVRPlatformSDK/Android/libs/${ANDROID_ABI}/libovrplatformloader.so @@ -117,8 +147,14 @@ find_library( # Sets the name of the path variable. android ) add_library(oculusvr-lib SHARED IMPORTED) +if(OPENXR) +set_target_properties(oculusvr-lib PROPERTIES IMPORTED_LOCATION + ${CMAKE_SOURCE_DIR}/../third_party/ovr_openxr_mobile_sdk/OpenXR/Libs/Android/${ANDROID_ABI}/Release/libopenxr_oculus.so ) +else() set_target_properties(oculusvr-lib PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../third_party/ovr_mobile/VrApi/Libs/Android/${ANDROID_ABI}/Release/libvrapi.so ) +endif() + add_library(ovrplatform-lib SHARED IMPORTED) set_target_properties(ovrplatform-lib PROPERTIES IMPORTED_LOCATION diff --git a/app/build.gradle b/app/build.gradle index 3b3bf3de5..af125b09f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,20 @@ def getUseDebugSigningOnRelease = { -> return false } +def getOpenXRFlags = { -> + if (gradle.hasProperty("userProperties.openxr")) { + return gradle."userProperties.openxr" == "true" ? " -DOPENXR" : "" + } + return "" +} + +def getOpenXRCMakeFlags = { -> + if (gradle.hasProperty("userProperties.openxr")) { + return gradle."userProperties.openxr" == "true" ? "-DOPENXR=ON" : "" + } + return "" +} + // Glean: Generate markdown docs for the collected metrics. ext.gleanGenerateMarkdownDocs = true ext.gleanDocsDirectory = "$rootDir/docs" @@ -120,11 +134,8 @@ android { dimension "platform" externalNativeBuild { cmake { - cppFlags " -I" + file("${project.rootDir}/third_party/ovr_mobile/VrApi/Include").absolutePath + - " -I" + file("${project.rootDir}/third_party/OVRPlatformSDK/Include").absolutePath + - " -I" + file("${project.rootDir}/app/src/oculusvr/cpp").absolutePath + - " -DOCULUSVR -DSTORE_BUILD=0" - arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON" + cppFlags "-DOCULUSVR -DSTORE_BUILD=0" + getOpenXRFlags() + arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON", getOpenXRCMakeFlags() } } manifestPlaceholders = [ headtrackingRequired:"false", permissionToRemove:"android.permission.RECEIVE_BOOT_COMPLETED" ] @@ -134,11 +145,8 @@ android { dimension "platform" externalNativeBuild { cmake { - cppFlags " -I" + file("${project.rootDir}/third_party/ovr_mobile/VrApi/Include").absolutePath + - " -I" + file("${project.rootDir}/third_party/OVRPlatformSDK/Include").absolutePath + - " -I" + file("${project.rootDir}/app/src/oculusvr/cpp").absolutePath + - " -DOCULUSVR -DSTORE_BUILD=1" - arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON" + cppFlags "-DOCULUSVR -DSTORE_BUILD=1" + getOpenXRFlags() + arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON", getOpenXRCMakeFlags() } } manifestPlaceholders = [ headtrackingRequired:"false", permissionToRemove:"android.permission.RECEIVE_BOOT_COMPLETED" ] @@ -148,11 +156,8 @@ android { dimension "platform" externalNativeBuild { cmake { - cppFlags " -I" + file("${project.rootDir}/third_party/ovr_mobile/VrApi/Include").absolutePath + - " -I" + file("${project.rootDir}/third_party/OVRPlatformSDK/Include").absolutePath + - " -I" + file("${project.rootDir}/app/src/oculusvr/cpp").absolutePath + - " -DOCULUSVR -DSTORE_BUILD=1" - arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON" + cppFlags "-DOCULUSVR -DSTORE_BUILD=1" + getOpenXRFlags() + arguments "-DVR_SDK_LIB=oculusvr-lib", "-DVR_SDK_EXTRA_LIB=ovrplatform-lib", "-DOCULUSVR=ON", getOpenXRCMakeFlags() } } manifestPlaceholders = [ headtrackingRequired:"false", permissionToRemove:"android.permission.CAMERA" ] diff --git a/app/src/main/cpp/SplashAnimation.cpp b/app/src/main/cpp/SplashAnimation.cpp index 3dedda576..77f41b2b9 100644 --- a/app/src/main/cpp/SplashAnimation.cpp +++ b/app/src/main/cpp/SplashAnimation.cpp @@ -62,10 +62,8 @@ struct SplashAnimation::State { return; } - read->Bind(GL_READ_FRAMEBUFFER); layer->Bind(GL_DRAW_FRAMEBUFFER); - VRB_GL_CHECK(glClearColor(0.0, 0.0f, 0.0f, 0.0f)); - VRB_GL_CHECK(glClear(GL_COLOR_BUFFER_BIT)); + read->Bind(GL_READ_FRAMEBUFFER); VRB_GL_CHECK(glBlitFramebuffer(0, 0, aTexture->GetWidth(), aTexture->GetHeight(), 0, 0, aTexture->GetWidth(), aTexture->GetHeight(), GL_COLOR_BUFFER_BIT, GL_LINEAR)); diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 92c317ef5..4f6e0652f 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -14,7 +14,9 @@ #include #include #include -#if defined(OCULUSVR) +#if defined(OPENXR) +#include "DeviceDelegateOpenXR.h" +#elif defined(OCULUSVR) #include "DeviceDelegateOculusVR.h" #endif @@ -28,7 +30,10 @@ using namespace crow; -#if defined(OCULUSVR) +#if defined(OPENXR) +typedef DeviceDelegateOpenXR PlatformDeviceDelegate; +typedef DeviceDelegateOpenXRPtr PlatformDeviceDelegatePtr; +#elif defined(OCULUSVR) typedef DeviceDelegateOculusVR PlatformDeviceDelegate; typedef DeviceDelegateOculusVRPtr PlatformDeviceDelegatePtr; #endif @@ -232,8 +237,6 @@ android_main(android_app *aAppState) { pSource->process(aAppState, pSource); } - - // Check if we are exiting. if (aAppState->destroyRequested != 0) { sAppContext->mEgl->MakeCurrent(); @@ -253,10 +256,17 @@ android_main(android_app *aAppState) { sAppContext->mEgl->MakeCurrent(); } sAppContext->mQueue->ProcessRunnables(); + if (!BrowserWorld::Instance().IsPaused() && sAppContext->mDevice->IsInVRMode()) { - VRB_GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); BrowserWorld::Instance().Draw(); } +#if defined(OPENXR) + else { + // OpenXR requires to wait for the XR_SESSION_STATE_READY to start presenting + // We need to call ProcessEvents to make sure we receive the event. + sAppContext->mDevice->ProcessEvents(); + } +#endif } } diff --git a/app/src/openxr/cpp/DeviceDelegateOpenXR.cpp b/app/src/openxr/cpp/DeviceDelegateOpenXR.cpp new file mode 100644 index 000000000..4d94c3c74 --- /dev/null +++ b/app/src/openxr/cpp/DeviceDelegateOpenXR.cpp @@ -0,0 +1,1036 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DeviceDelegateOpenXR.h" +#include "DeviceUtils.h" +#include "ElbowModel.h" +#include "BrowserEGLContext.h" +#include "VRBrowser.h" +#include "VRLayer.h" + +#include +#include +#include "vrb/CameraEye.h" +#include "vrb/Color.h" +#include "vrb/ConcreteClass.h" +#include "vrb/FBO.h" +#include "vrb/GLError.h" +#include "vrb/Matrix.h" +#include "vrb/Quaternion.h" +#include "vrb/RenderContext.h" +#include "vrb/Vector.h" + +#include +#include +#include +#include +#include + +#include "VRBrowser.h" + +#include +#include +#ifdef OCULUSVR +#include +#endif +#include "OpenXRHelpers.h" +#include "OpenXRSwapChain.h" +#include "OpenXRInput.h" +#include "OpenXRExtensions.h" +#include "OpenXRLayers.h" + +namespace crow { + +const vrb::Vector kAverageHeight(0.0f, 1.7f, 0.0f); + +struct DeviceDelegateOpenXR::State { + vrb::RenderContextWeak context; + android_app* app = nullptr; + bool layersEnabled = true; + XrInstanceCreateInfoAndroidKHR java; + XrInstance instance = XR_NULL_HANDLE; + XrSystemId system = XR_NULL_SYSTEM_ID; + XrSession session = XR_NULL_HANDLE; + XrSessionState sessionState = XR_SESSION_STATE_UNKNOWN; + bool vrReady = false; + XrGraphicsBindingOpenGLESAndroidKHR graphicsBinding{XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR}; + XrSystemProperties systemProperties{XR_TYPE_SYSTEM_PROPERTIES}; + XrEventDataBuffer eventBuffer; + XrViewConfigurationType viewConfigType{XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO}; + std::vector viewConfig; + std::vector views; + std::vector eyeSwapChains; + OpenXRSwapChainPtr boundSwapChain; + OpenXRSwapChainPtr previousBoundSwapchain; + XrSpace viewSpace = XR_NULL_HANDLE; + XrSpace localSpace = XR_NULL_HANDLE; + XrSpace layersSpace = XR_NULL_HANDLE; + XrSpace stageSpace = XR_NULL_HANDLE; + std::vector swapchainFormats; + OpenXRInputPtr input; + JNIEnv * jniEnv = nullptr; + OpenXRLayerCubePtr cubeLayer; + OpenXRLayerEquirectPtr equirectLayer; + std::vector uiLayers; + OpenXRSwapChainPtr crearColorSwapChain; + device::RenderMode renderMode = device::RenderMode::StandAlone; + vrb::CameraEyePtr cameras[2]; + FramePrediction framePrediction = FramePrediction::NO_FRAME_AHEAD; + XrTime prevPredictedDisplayTime = 0; + XrTime predictedDisplayTime = 0; + XrPosef predictedPose = {}; + XrPosef prevPredictedPose = {}; + uint32_t discardedFrameIndex = 0; + int discardCount = 0; + vrb::Color clearColor; + float near = 0.1f; + float far = 100.f; + bool hasEventFocus = true; + crow::ElbowModelPtr elbow; + ControllerDelegatePtr controller; + ImmersiveDisplayPtr immersiveDisplay; + int reorientCount = -1; + vrb::Matrix reorientMatrix = vrb::Matrix::Identity(); + device::CPULevel minCPULevel = device::CPULevel::Normal; + device::DeviceType deviceType = device::UnknownType; + std::vector frameEndLayers; + + void Initialize() { + vrb::RenderContextPtr localContext = context.lock(); + elbow = ElbowModel::Create(); + for (int i = 0; i < 2; ++i) { + cameras[i] = vrb::CameraEye::Create(localContext->GetRenderThreadCreationContext()); + } + layersEnabled = VRBrowser::AreLayersEnabled(); + + (*app->activity->vm).AttachCurrentThread(&jniEnv, NULL); + CHECK(jniEnv != nullptr); + +#ifdef OCULUSVR + // Adhoc loader required for OpenXR on Oculus + XrLoaderInitializeInfoAndroidOCULUS loaderData; + memset(&loaderData, 0, sizeof(loaderData)); + loaderData.type = XR_TYPE_LOADER_INITIALIZE_INFO_ANDROID_OCULUS; + loaderData.next = nullptr; + loaderData.applicationVM = app->activity->vm; + loaderData.applicationActivity = jniEnv->NewGlobalRef(app->activity->clazz); + xrInitializeLoaderOCULUS(&loaderData); +#endif + + // Initialize the XrInstance + std::vector extensions = OpenXRExtensions::ExtensionNames(); + java = {XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR}; + java.applicationVM = app->activity->vm; + java.applicationActivity = jniEnv->NewGlobalRef(app->activity->clazz); + + XrInstanceCreateInfo createInfo{XR_TYPE_INSTANCE_CREATE_INFO}; + createInfo.next = (XrBaseInStructure*)&java; + createInfo.enabledExtensionCount = (uint32_t)extensions.size(); + createInfo.enabledExtensionNames = extensions.data(); + strcpy(createInfo.applicationInfo.applicationName, "Firefox Reality"); + createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + + CHECK_XRCMD(xrCreateInstance(&createInfo, &instance)); + CHECK_MSG(instance != XR_NULL_HANDLE, "Failed to create XRInstance"); + + XrInstanceProperties instanceProperties{XR_TYPE_INSTANCE_PROPERTIES}; + CHECK_XRCMD(xrGetInstanceProperties(instance, &instanceProperties)); + VRB_LOG("OpenXR Instance Created: RuntimeName=%s RuntimeVersion=%s", instanceProperties.runtimeName, + GetXrVersionString(instanceProperties.runtimeVersion).c_str()); + + // Initialize Extensions + OpenXRExtensions::Initialize(instance); + + // Initialize System + XrSystemGetInfo systemInfo{XR_TYPE_SYSTEM_GET_INFO}; + systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + CHECK_XRCMD(xrGetSystem(instance, &systemInfo, &system)); + CHECK_MSG(system != XR_NULL_SYSTEM_ID, "Failed to initialize XRSystem"); + + // Retrieve system info + CHECK_XRCMD(xrGetSystemProperties(instance, system, &systemProperties)) + VRB_LOG("OpenXR system name: %s", systemProperties.systemName); + + input = OpenXRInput::Create(instance, systemProperties); + } + + // xrGet*GraphicsRequirementsKHR check must be called prior to xrCreateSession + // xrCreateSession fails if we don't call it. + void CheckGraphicsRequirements() { + + XrGraphicsRequirementsOpenGLESKHR graphicsRequirements{XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR}; + CHECK_XRCMD(OpenXRExtensions::sXrGetOpenGLESGraphicsRequirementsKHR(instance, system, &graphicsRequirements)); + + GLint major = 0; + GLint minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0); + if (graphicsRequirements.minApiVersionSupported > desiredApiVersion) { + THROW("Runtime does not support desired Graphics API and/or version"); + } + } + + void InitializeSwapChainFormats() { + uint32_t swapchainFormatCount; + CHECK_XRCMD(xrEnumerateSwapchainFormats(session, 0, &swapchainFormatCount, nullptr)); + CHECK_MSG(swapchainFormatCount > 0, "OpenXR unexpected swapchainFormatCount"); + swapchainFormats.resize(swapchainFormatCount, 0); + CHECK_XRCMD(xrEnumerateSwapchainFormats(session, (uint32_t)swapchainFormats.size(), &swapchainFormatCount, + swapchainFormats.data())); + VRB_LOG("OpenXR Available color formats: %d", swapchainFormatCount); + } + + bool SupportsColorFormat(int64_t aColorFormat) { + if (swapchainFormats.size() == 0) { + InitializeSwapChainFormats(); + } + return std::find(swapchainFormats.begin(), swapchainFormats.end(), aColorFormat) != swapchainFormats.end(); + } + + void InitializeViews() { + CHECK(session != XR_NULL_HANDLE) + // Enumerate configurations + uint32_t viewCount; + CHECK_XRCMD(xrEnumerateViewConfigurationViews(instance, system, viewConfigType, 0, &viewCount, nullptr)); + CHECK_MSG(viewCount > 0, "OpenXR unexpected viewCount"); + viewConfig.resize(viewCount, {XR_TYPE_VIEW_CONFIGURATION_VIEW}); + CHECK_XRCMD(xrEnumerateViewConfigurationViews(instance, system, viewConfigType, viewCount, &viewCount, viewConfig.data())); + + // Cache view buffer (used in xrLocateViews) + views.resize(viewCount, {XR_TYPE_VIEW}); + + vrb::RenderContextPtr render = context.lock(); + + // Create the main swapChain for each eye view + for (uint32_t i = 0; i < viewCount; i++) { + auto swapChain = OpenXRSwapChain::create(); + XrSwapchainCreateInfo info = GetSwapChainCreateInfo(); + swapChain->InitFBO(render, session, info, GetFBOAttributes()); + eyeSwapChains.push_back(swapChain); + } + VRB_DEBUG("OpenXR available views: %d", (int)eyeSwapChains.size()); + } + + void InitializeImmersiveDisplay() { + CHECK(immersiveDisplay); + CHECK(viewConfig.size() > 0); + + immersiveDisplay->SetDeviceName(systemProperties.systemName); + immersiveDisplay->SetEyeResolution(viewConfig.front().recommendedImageRectWidth, viewConfig.front().recommendedImageRectHeight); + immersiveDisplay->SetSittingToStandingTransform(vrb::Matrix::Translation(kAverageHeight)); + immersiveDisplay->CompleteEnumeration(); + } + + XrSwapchainCreateInfo GetSwapChainCreateInfo(uint32_t w = 0, uint32_t h = 0) { + const int64_t colorFormat = GL_RGBA8; + CHECK_MSG(SupportsColorFormat(colorFormat), "Runtime doesn't support selected swapChain color format"); + + CHECK(viewConfig.size() > 0); + + if (w == 0 || h == 0) { + w = viewConfig.front().recommendedImageRectWidth; + h = viewConfig.front().recommendedImageRectHeight; + } + + XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + info.arraySize = 1; + info.format = colorFormat; + info.width = w; + info.height = h; + info.mipCount = 1; + info.faceCount = 1; + info.arraySize = 1; + info.sampleCount = viewConfig.front().recommendedSwapchainSampleCount; + info.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; + return info; + } + + vrb::FBO::Attributes GetFBOAttributes() const { + vrb::FBO::Attributes attributes; + if (renderMode == device::RenderMode::StandAlone) { + attributes.depth = true; + attributes.samples = 4; + } else { + attributes.depth = false; + attributes.samples = 0; + } + return attributes; + } + + void UpdateSpaces() { + CHECK(session != XR_NULL_HANDLE); + + // Query supported reference spaces + uint32_t spaceCount = 0; + CHECK_XRCMD(xrEnumerateReferenceSpaces(session, 0, &spaceCount, nullptr)); + std::vector spaces(spaceCount); + CHECK_XRCMD(xrEnumerateReferenceSpaces(session, spaceCount, &spaceCount, spaces.data())); + VRB_DEBUG("OpenXR Available reference spaces: %d", spaceCount); + for (XrReferenceSpaceType space : spaces) { + VRB_DEBUG(" OpenXR Space Name: %s", to_string(space)); + } + + auto supportsSpace = [&](XrReferenceSpaceType aType) -> bool { + return std::find(spaces.begin(), spaces.end(), aType) != spaces.end(); + }; + + // Initialize view spaces used by default + if (viewSpace == XR_NULL_HANDLE) { + CHECK_MSG(supportsSpace(XR_REFERENCE_SPACE_TYPE_VIEW), "XR_REFERENCE_SPACE_TYPE_VIEW not supported"); + XrReferenceSpaceCreateInfo create{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + create.poseInReferenceSpace = XrPoseIdentity(); + create.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW; + CHECK_XRCMD(xrCreateReferenceSpace(session, &create, &viewSpace)); + } + + if (localSpace == XR_NULL_HANDLE) { + CHECK_MSG(supportsSpace(XR_REFERENCE_SPACE_TYPE_LOCAL), "XR_REFERENCE_SPACE_TYPE_LOCAL not supported"); + XrReferenceSpaceCreateInfo create{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + create.poseInReferenceSpace = XrPoseIdentity(); + create.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + CHECK_XRCMD(xrCreateReferenceSpace(session, &create, &localSpace)); + } + + if (layersSpace == XR_NULL_HANDLE) { + CHECK_MSG(supportsSpace(XR_REFERENCE_SPACE_TYPE_LOCAL), "XR_REFERENCE_SPACE_TYPE_LOCAL not supported"); + XrReferenceSpaceCreateInfo create{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + create.poseInReferenceSpace = XrPoseIdentity(); + create.poseInReferenceSpace.position = { + -kAverageHeight.x(), -kAverageHeight.y(), -kAverageHeight.z() + }; + create.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + CHECK_XRCMD(xrCreateReferenceSpace(session, &create, &layersSpace)); + } + + // Optionally create a stageSpace to be used in WebXR room scale apps. + if (stageSpace == XR_NULL_HANDLE && supportsSpace(XR_REFERENCE_SPACE_TYPE_LOCAL)) { + XrReferenceSpaceCreateInfo create{XR_TYPE_REFERENCE_SPACE_CREATE_INFO}; + create.poseInReferenceSpace = XrPoseIdentity(); + create.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + CHECK_XRCMD(xrCreateReferenceSpace(session, &create, &stageSpace)); + } + } + + void AddUILayer(const OpenXRLayerPtr& aLayer, VRLayerSurface::SurfaceType aSurfaceType) { + if (session != XR_NULL_HANDLE) { + vrb::RenderContextPtr ctx = context.lock(); + aLayer->Init(jniEnv, session, ctx); + } + uiLayers.push_back(aLayer); + if (aSurfaceType == VRLayerSurface::SurfaceType::FBO) { + aLayer->SetBindDelegate([=](const OpenXRSwapChainPtr& aSwapchain, GLenum aTarget, bool bound){ + if (aSwapchain) { + HandleQuadLayerBind(aSwapchain, aTarget, bound); + } + }); + if (boundSwapChain) { + boundSwapChain->BindFBO(); + } + } + } + + void HandleQuadLayerBind(const OpenXRSwapChainPtr& aSwapchain, GLenum aTarget, bool bound) { + if (!bound) { + if (boundSwapChain && boundSwapChain == aSwapchain) { + boundSwapChain->ReleaseImage(); + boundSwapChain = nullptr; + } + if (previousBoundSwapchain) { + previousBoundSwapchain->BindFBO(); + boundSwapChain = previousBoundSwapchain; + previousBoundSwapchain = nullptr; + } + return; + } + + if (boundSwapChain == aSwapchain) { + // Layer already bound + return; + } + + previousBoundSwapchain = boundSwapChain; + boundSwapChain = aSwapchain; + boundSwapChain->AcquireImage(); + boundSwapChain->BindFBO(aTarget); + } + + bool Is6DOF() const { + return systemProperties.trackingProperties.positionTracking != 0; + } + + const XrEventDataBaseHeader* PollEvent() { + if (!instance) { + return nullptr; + } + XrEventDataBaseHeader* baseHeader = reinterpret_cast(&eventBuffer); + *baseHeader = {XR_TYPE_EVENT_DATA_BUFFER}; + const XrResult xr = xrPollEvent(instance, &eventBuffer); + if (xr == XR_SUCCESS) { + return baseHeader; + } + + CHECK_MSG(xr == XR_EVENT_UNAVAILABLE, "Expected XR_EVENT_UNAVAILABLE result") + return nullptr; + } + + void HandleSessionEvent(const XrEventDataSessionStateChanged& event) { + VRB_DEBUG("OpenXR XrEventDataSessionStateChanged: state %s->%s session=%p time=%ld", + to_string(sessionState), to_string(event.state), event.session, event.time); + sessionState = event.state; + + CHECK(session == event.session); + + switch (sessionState) { + case XR_SESSION_STATE_READY: { + XrSessionBeginInfo sessionBeginInfo{XR_TYPE_SESSION_BEGIN_INFO}; + sessionBeginInfo.primaryViewConfigurationType = viewConfigType; + CHECK_XRCMD(xrBeginSession(session, &sessionBeginInfo)); + vrReady = true; + break; + } + case XR_SESSION_STATE_STOPPING: { + vrReady = false; + CHECK_XRCMD(xrEndSession(session)) + break; + } + case XR_SESSION_STATE_EXITING: { + vrReady = false; + break; + } + case XR_SESSION_STATE_LOSS_PENDING: { + vrReady = false; + break; + } + default: + break; + } + } + + void UpdateClockLevels() { + // TODO + } + + + void Shutdown() { + // Release swapChains + for (OpenXRSwapChainPtr swapChain: eyeSwapChains) { + swapChain->Destroy(); + } + + // Release spaces + if (viewSpace != XR_NULL_HANDLE) { + CHECK_XRCMD(xrDestroySpace(viewSpace)); + viewSpace = XR_NULL_HANDLE; + } + + if (localSpace != XR_NULL_HANDLE) { + CHECK_XRCMD(xrDestroySpace(localSpace)); + localSpace = XR_NULL_HANDLE; + } + + if (layersSpace != XR_NULL_HANDLE) { + CHECK_XRCMD(xrDestroySpace(layersSpace)); + layersSpace = XR_NULL_HANDLE; + } + + if (stageSpace != XR_NULL_HANDLE) { + CHECK_XRCMD(xrDestroySpace(stageSpace)); + stageSpace = XR_NULL_HANDLE; + } + + // Release input + input->Destroy(); + input = nullptr; + + // Shutdown OpenXR instance + if (instance) { + CHECK_XRCMD(xrDestroyInstance(instance)); + instance = XR_NULL_HANDLE; + } + + // TODO: Check if activity globarRef needs to be released + } +}; + +DeviceDelegateOpenXRPtr +DeviceDelegateOpenXR::Create(vrb::RenderContextPtr& aContext, android_app *aApp) { + DeviceDelegateOpenXRPtr result = std::make_shared >(); + result->m.context = aContext; + result->m.app = aApp; + result->m.Initialize(); + return result; +} + +device::DeviceType +DeviceDelegateOpenXR::GetDeviceType() { + return m.deviceType; +} + +void +DeviceDelegateOpenXR::SetRenderMode(const device::RenderMode aMode) { + if (aMode == m.renderMode) { + return; + } + m.renderMode = aMode; + vrb::RenderContextPtr render = m.context.lock(); + for (OpenXRSwapChainPtr& eyeSwapchain: m.eyeSwapChains) { + XrSwapchainCreateInfo info = m.GetSwapChainCreateInfo(); + eyeSwapchain->InitFBO(render, m.session, info, m.GetFBOAttributes()); + } + + // Reset reorient when exiting or entering immersive + m.reorientMatrix = vrb::Matrix::Identity(); +} + +device::RenderMode +DeviceDelegateOpenXR::GetRenderMode() { + return m.renderMode; +} + +void +DeviceDelegateOpenXR::RegisterImmersiveDisplay(ImmersiveDisplayPtr aDisplay) { + m.immersiveDisplay = std::move(aDisplay); +} + +void +DeviceDelegateOpenXR::SetImmersiveSize(const uint32_t aEyeWidth, const uint32_t aEyeHeight) { + +} + +vrb::CameraPtr +DeviceDelegateOpenXR::GetCamera(const device::Eye aWhich) { + const int32_t index = device::EyeIndex(aWhich); + if (index < 0) { return nullptr; } + return m.cameras[index]; +} + +const vrb::Matrix& +DeviceDelegateOpenXR::GetHeadTransform() const { + return m.cameras[0]->GetHeadTransform(); +} + +const vrb::Matrix& +DeviceDelegateOpenXR::GetReorientTransform() const { + return m.reorientMatrix; +} + +void +DeviceDelegateOpenXR::SetReorientTransform(const vrb::Matrix& aMatrix) { + m.reorientMatrix = aMatrix; +} + +void +DeviceDelegateOpenXR::SetClearColor(const vrb::Color& aColor) { + m.clearColor = aColor; +} + +void +DeviceDelegateOpenXR::SetClipPlanes(const float aNear, const float aFar) { + m.near = aNear; + m.far = aFar; +} + +void +DeviceDelegateOpenXR::SetControllerDelegate(ControllerDelegatePtr& aController) { + m.controller = aController; +} + +void +DeviceDelegateOpenXR::ReleaseControllerDelegate() { + m.controller = nullptr; +} + +int32_t +DeviceDelegateOpenXR::GetControllerModelCount() const { + return m.input->GetControllerModelCount(); +} + +const std::string +DeviceDelegateOpenXR::GetControllerModelName(const int32_t aModelIndex) const { + return m.input->GetControllerModelName(aModelIndex); +} + + +void +DeviceDelegateOpenXR::SetCPULevel(const device::CPULevel aLevel) { + m.minCPULevel = aLevel; + m.UpdateClockLevels(); +}; + + +void +DeviceDelegateOpenXR::ProcessEvents() { + while (const XrEventDataBaseHeader* ev = m.PollEvent()) { + switch (ev->type) { + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { + const auto& event = *reinterpret_cast(ev); + m.HandleSessionEvent(event); + break; + } + case XR_TYPE_EVENT_DATA_EVENTS_LOST: { + const auto& event = *reinterpret_cast(ev); + VRB_WARN("OpenXR %d events lost", event.lostEventCount); + break; + } + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: { + // Receiving the XrEventDataInstanceLossPending event structure indicates that the application + // is about to lose the indicated XrInstance at the indicated lossTime in the future. + const auto& event = *reinterpret_cast(ev); + VRB_WARN("OpenXR XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING by %ld", event.lossTime); + m.vrReady = false; + return; + } + case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: + default: { + VRB_DEBUG("OpenXR ignoring event type %d", ev->type); + break; + } + } + } +} + +bool +DeviceDelegateOpenXR::SupportsFramePrediction(FramePrediction aPrediction) const { + return true; +} + +void +DeviceDelegateOpenXR::StartFrame(const FramePrediction aPrediction) { + if (!m.vrReady) { + VRB_ERROR("OpenXR StartFrame called while not in VR mode"); + return; + } + + CHECK(m.session != XR_NULL_HANDLE); + CHECK(m.viewSpace != XR_NULL_HANDLE); + + // Throttle the application frame loop in order to synchronize + // application frame submissions with the display. + XrFrameWaitInfo frameWaitInfo{XR_TYPE_FRAME_WAIT_INFO}; + XrFrameState frameState{XR_TYPE_FRAME_STATE}; + CHECK_XRCMD(xrWaitFrame(m.session, &frameWaitInfo, &frameState)); + + // Begin frame and select the predicted display time + XrFrameBeginInfo frameBeginInfo{XR_TYPE_FRAME_BEGIN_INFO}; + CHECK_XRCMD(xrBeginFrame(m.session, &frameBeginInfo)); + + CHECK_MSG(frameState.shouldRender, "shouldRender==false bailout not implemented yet"); + + m.framePrediction = aPrediction; + if (aPrediction == FramePrediction::ONE_FRAME_AHEAD) { + m.prevPredictedDisplayTime = m.predictedDisplayTime; + m.prevPredictedPose = m.predictedPose; + m.predictedDisplayTime = frameState.predictedDisplayTime + frameState.predictedDisplayPeriod; + } else { + m.predictedDisplayTime = frameState.predictedDisplayTime; + } + + // Query head location + XrSpaceLocation location {XR_TYPE_SPACE_LOCATION}; + xrLocateSpace(m.viewSpace, m.localSpace, m.predictedDisplayTime, &location); + m.predictedPose = location.pose; + + vrb::Matrix head = XrPoseToMatrix(location.pose); + + if (m.renderMode == device::RenderMode::StandAlone) { + head.TranslateInPlace(kAverageHeight); + } + + m.cameras[0]->SetHeadTransform(head); + m.cameras[1]->SetHeadTransform(head); + + if (m.immersiveDisplay) { + // Setup capability caps for this frame + device::CapabilityFlags caps = + device::Orientation | device::Present | device::InlineSession | device::ImmersiveVRSession; + if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + caps |= m.Is6DOF() ? device::Position : device::PositionEmulated; + } + m.immersiveDisplay->SetCapabilityFlags(caps); + + // Update WebXR room scale transform if the device supports stage space + if (m.stageSpace != XR_NULL_HANDLE) { + // Compute the transform between local and stage space + XrSpaceLocation stageLocation{XR_TYPE_SPACE_LOCATION}; + xrLocateSpace(m.localSpace, m.stageSpace, m.predictedDisplayTime, &stageLocation); + vrb::Matrix transform = XrPoseToMatrix(stageLocation.pose); + m.immersiveDisplay->SetSittingToStandingTransform(transform); + } + } + + // Query eyeTransform ans perspective for each view + XrViewState viewState{XR_TYPE_VIEW_STATE}; + uint32_t viewCapacityInput = (uint32_t) m.views.size(); + uint32_t viewCountOutput = 0; + + XrViewLocateInfo viewLocateInfo{XR_TYPE_VIEW_LOCATE_INFO}; + viewLocateInfo.viewConfigurationType = m.viewConfigType; + viewLocateInfo.displayTime = m.predictedDisplayTime; + viewLocateInfo.space = m.viewSpace; + CHECK_XRCMD(xrLocateViews(m.session, &viewLocateInfo, &viewState, viewCapacityInput, &viewCountOutput, m.views.data())); + + for (int i = 0; i < m.views.size(); ++i) { + const XrView& view = m.views[i]; + + vrb::Matrix eyeTransform = XrPoseToMatrix(view.pose); + m.cameras[i]->SetEyeTransform(eyeTransform); + + + vrb::Matrix perspective = vrb::Matrix::PerspectiveMatrix(fabsf(view.fov.angleLeft), view.fov.angleRight, + view.fov.angleUp, fabsf(view.fov.angleDown), m.near, m.far); + m.cameras[i]->SetPerspective(perspective); + + if (m.immersiveDisplay) { + const device::Eye eye = i == 0 ? device::Eye::Left : device::Eye::Right; + auto toDegrees = [](float angle) -> float { + return angle * 180.0f / (float)M_PI; + }; + m.immersiveDisplay->SetFieldOfView(eye, toDegrees(fabsf(view.fov.angleLeft)), toDegrees(view.fov.angleRight), + toDegrees(view.fov.angleUp), toDegrees(fabsf(view.fov.angleDown))); + vrb::Vector offset = eyeTransform.GetTranslation(); + m.immersiveDisplay->SetEyeOffset(eye, offset.x(), offset.y(), offset.z()); + } + } + + // Update controllers + m.input->Update(m.session, m.predictedDisplayTime, m.localSpace, m.renderMode, m.controller); +} + +void +DeviceDelegateOpenXR::BindEye(const device::Eye aWhich) { + if (!m.vrReady) { + VRB_ERROR("OpenXR BindEye called while not in VR mode"); + return; + } + + int32_t index = device::EyeIndex(aWhich); + if (index < 0 || index >= m.eyeSwapChains.size()) { + VRB_ERROR("No eye found"); + return; + } + + if (m.boundSwapChain) { + m.boundSwapChain->ReleaseImage(); + } + + m.boundSwapChain = m.eyeSwapChains[index]; + m.boundSwapChain->AcquireImage(); + m.boundSwapChain->BindFBO(); + VRB_GL_CHECK(glViewport(0, 0, m.boundSwapChain->Width(), m.boundSwapChain->Height())); + VRB_GL_CHECK(glClearColor(m.clearColor.Red(), m.clearColor.Green(), m.clearColor.Blue(), m.clearColor.Alpha())); + VRB_GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); +} + +void +DeviceDelegateOpenXR::EndFrame(const FrameEndMode aEndMode) { + if (!m.vrReady) { + VRB_ERROR("OpenXR EndFrame called while not in VR mode"); + return; + } + if (m.boundSwapChain) { + m.boundSwapChain->ReleaseImage(); + m.boundSwapChain = nullptr; + } + + const bool frameAhead = m.framePrediction == FramePrediction::ONE_FRAME_AHEAD; + const XrPosef& predictedPose = frameAhead ? m.prevPredictedPose : m.predictedPose; + const XrTime displayTime = frameAhead ? m.prevPredictedDisplayTime : m.predictedDisplayTime; + + std::vector& layers = m.frameEndLayers; + layers.clear(); + + // Add skybox layer + if (m.cubeLayer && m.cubeLayer->IsLoaded() && m.cubeLayer->IsDrawRequested()) { + m.cubeLayer->Update(m.localSpace, predictedPose, XR_NULL_HANDLE); + for (uint32_t i = 0; i < m.cubeLayer->HeaderCount(); ++i) { + layers.push_back(m.cubeLayer->Header(i)); + } + m.cubeLayer->ClearRequestDraw(); + } + + // Add VR video layer + if (m.equirectLayer && m.equirectLayer->IsDrawRequested()) { + m.equirectLayer->Update(m.localSpace, predictedPose, XR_NULL_HANDLE); + for (uint32_t i = 0; i < m.equirectLayer->HeaderCount(); ++i) { + layers.push_back(m.equirectLayer->Header(i)); + } + m.equirectLayer->ClearRequestDraw(); + } + + // Sort quad layers by draw priority + std::sort(m.uiLayers.begin(), m.uiLayers.end(), [](const OpenXRLayerPtr & a, OpenXRLayerPtr & b) -> bool { + return a->GetLayer()->ShouldDrawBefore(*b->GetLayer()); + }); + + // Add back UI layers + for (const OpenXRLayerPtr& layer: m.uiLayers) { + if (!layer->GetDrawInFront() && layer->IsDrawRequested()) { + layer->Update(m.layersSpace, predictedPose, XR_NULL_HANDLE); + for (uint32_t i = 0; i < layer->HeaderCount(); ++i) { + layers.push_back(layer->Header(i)); + } + layer->ClearRequestDraw(); + } + } + + // Add main eye buffer layer + XrCompositionLayerProjection projectionLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; + std::vector projectionLayerViews; + projectionLayerViews.resize(m.views.size()); + projectionLayer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + for (int i = 0; i < m.views.size(); ++i) { + const OpenXRSwapChainPtr& viewSwapChain = m.eyeSwapChains[i]; + projectionLayerViews[i] = {XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}; + projectionLayerViews[i].pose = m.views[i].pose; + projectionLayerViews[i].fov = m.views[i].fov; + projectionLayerViews[i].subImage.swapchain = viewSwapChain->SwapChain(); + projectionLayerViews[i].subImage.imageRect.offset = {0, 0}; + projectionLayerViews[i].subImage.imageRect.extent = {viewSwapChain->Width(), viewSwapChain->Height()}; + projectionLayer.space = m.viewSpace; + projectionLayer.viewCount = (uint32_t)projectionLayerViews.size(); + projectionLayer.views = projectionLayerViews.data(); + } + layers.push_back(reinterpret_cast(&projectionLayer)); + + // Add front UI layers + for (const OpenXRLayerPtr& layer: m.uiLayers) { + if (layer->GetDrawInFront() && layer->IsDrawRequested()) { + layer->Update(m.layersSpace, predictedPose, XR_NULL_HANDLE); + for (uint32_t i = 0; i < layer->HeaderCount(); ++i) { + layers.push_back(layer->Header(i)); + } + layer->ClearRequestDraw(); + } + } + + XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; + frameEndInfo.displayTime = displayTime; + frameEndInfo.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + frameEndInfo.layerCount = (uint32_t )layers.size(); + frameEndInfo.layers = layers.data(); + CHECK_XRCMD(xrEndFrame(m.session, &frameEndInfo)); +} + +VRLayerQuadPtr +DeviceDelegateOpenXR::CreateLayerQuad(int32_t aWidth, int32_t aHeight, + VRLayerSurface::SurfaceType aSurfaceType) { + if (!m.layersEnabled) { + return nullptr; + } + + if (aSurfaceType == VRLayerSurface::SurfaceType::AndroidSurface) { + return nullptr; // Remove when XR_ERROR_LAYER_INVALID bug is fixed + } + + VRLayerQuadPtr layer = VRLayerQuad::Create(aWidth, aHeight, aSurfaceType); + OpenXRLayerQuadPtr xrLayer = OpenXRLayerQuad::Create(m.jniEnv, layer); + m.AddUILayer(xrLayer, aSurfaceType); + return layer; +} + +VRLayerQuadPtr +DeviceDelegateOpenXR::CreateLayerQuad(const VRLayerSurfacePtr& aMoveLayer) { + if (!m.layersEnabled) { + return nullptr; + } + if (aMoveLayer->GetSurfaceType() == VRLayerSurface::SurfaceType::AndroidSurface) { + return nullptr; // Remove when XR_ERROR_LAYER_INVALID bug is fixed + } + + VRLayerQuadPtr layer = VRLayerQuad::Create(aMoveLayer->GetWidth(), aMoveLayer->GetHeight(), aMoveLayer->GetSurfaceType()); + OpenXRLayerQuadPtr xrLayer; + + for (int i = 0; i < m.uiLayers.size(); ++i) { + if (m.uiLayers[i]->GetLayer() == aMoveLayer) { + xrLayer = OpenXRLayerQuad::Create(m.jniEnv, layer, m.uiLayers[i]); + m.uiLayers.erase(m.uiLayers.begin() + i); + break; + } + } + if (xrLayer) { + m.AddUILayer(xrLayer, aMoveLayer->GetSurfaceType()); + } + return layer; +} + +VRLayerCylinderPtr +DeviceDelegateOpenXR::CreateLayerCylinder(int32_t aWidth, int32_t aHeight, + VRLayerSurface::SurfaceType aSurfaceType) { + if (!m.layersEnabled) { + return nullptr; + } + if (aSurfaceType == VRLayerSurface::SurfaceType::AndroidSurface) { + return nullptr; // Remove when XR_ERROR_LAYER_INVALID bug is fixed + } + VRLayerCylinderPtr layer = VRLayerCylinder::Create(aWidth, aHeight, aSurfaceType); + OpenXRLayerCylinderPtr xrLayer = OpenXRLayerCylinder::Create(m.jniEnv, layer); + m.AddUILayer(xrLayer, aSurfaceType); + return layer; +} + +VRLayerCylinderPtr +DeviceDelegateOpenXR::CreateLayerCylinder(const VRLayerSurfacePtr& aMoveLayer) { + if (!m.layersEnabled) { + return nullptr; + } + if (aMoveLayer->GetSurfaceType() == VRLayerSurface::SurfaceType::AndroidSurface) { + return nullptr; // Remove when XR_ERROR_LAYER_INVALID bug is fixed + } + + VRLayerCylinderPtr layer = VRLayerCylinder::Create(aMoveLayer->GetWidth(), aMoveLayer->GetHeight(), aMoveLayer->GetSurfaceType()); + OpenXRLayerCylinderPtr xrLayer; + + for (int i = 0; i < m.uiLayers.size(); ++i) { + if (m.uiLayers[i]->GetLayer() == aMoveLayer) { + xrLayer = OpenXRLayerCylinder::Create(m.jniEnv, layer, m.uiLayers[i]); + m.uiLayers.erase(m.uiLayers.begin() + i); + break; + } + } + if (xrLayer) { + m.AddUILayer(xrLayer, aMoveLayer->GetSurfaceType()); + } + return layer; +} + + +VRLayerCubePtr +DeviceDelegateOpenXR::CreateLayerCube(int32_t aWidth, int32_t aHeight, GLint aInternalFormat) { + if (!m.layersEnabled) { + return nullptr; + } + if (m.cubeLayer) { + m.cubeLayer->Destroy(); + } + VRLayerCubePtr layer = VRLayerCube::Create(aWidth, aHeight, aInternalFormat); + m.cubeLayer = OpenXRLayerCube::Create(layer, aInternalFormat); + if (m.session != XR_NULL_HANDLE) { + vrb::RenderContextPtr context = m.context.lock(); + m.cubeLayer->Init(m.jniEnv, m.session, context); + } + return layer; +} + +VRLayerEquirectPtr +DeviceDelegateOpenXR::CreateLayerEquirect(const VRLayerPtr &aSource) { + if (!m.layersEnabled) { + return nullptr; + } + if (true) { + return nullptr; // Remove when XR_ERROR_LAYER_INVALID bug is fixed + } + VRLayerEquirectPtr result = VRLayerEquirect::Create(); + OpenXRLayerPtr source; + for (const OpenXRLayerPtr& layer: m.uiLayers) { + if (layer->GetLayer() == aSource) { + source = layer; + break; + } + } + if (m.equirectLayer) { + m.equirectLayer->Destroy(); + } + m.equirectLayer = OpenXRLayerEquirect::Create(result, source); + if (m.session != XR_NULL_HANDLE) { + vrb::RenderContextPtr context = m.context.lock(); + m.equirectLayer->Init(m.jniEnv, m.session, context); + } + return result; +} + +void +DeviceDelegateOpenXR::DeleteLayer(const VRLayerPtr& aLayer) { + if (m.cubeLayer && m.cubeLayer->layer == aLayer) { + m.cubeLayer->Destroy(); + m.cubeLayer = nullptr; + return; + } + if (m.equirectLayer && m.equirectLayer->layer == aLayer) { + m.equirectLayer->Destroy(); + m.equirectLayer = nullptr; + return; + } + for (int i = 0; i < m.uiLayers.size(); ++i) { + if (m.uiLayers[i]->GetLayer() == aLayer) { + m.uiLayers[i]->Destroy(); + m.uiLayers.erase(m.uiLayers.begin() + i); + return; + } + } +} + +void +DeviceDelegateOpenXR::EnterVR(const crow::BrowserEGLContext& aEGLContext) { + // Reset reorientation after Enter VR + m.reorientMatrix = vrb::Matrix::Identity(); + + if (m.session != XR_NULL_HANDLE && m.graphicsBinding.context == aEGLContext.Context()) { + ProcessEvents(); + // Session already created and valid. + return; + } + + CHECK(m.instance != XR_NULL_HANDLE && m.system != XR_NULL_SYSTEM_ID); + m.CheckGraphicsRequirements(); + + m.graphicsBinding.context = aEGLContext.Context(); + m.graphicsBinding.display = aEGLContext.Display(); + m.graphicsBinding.config = (EGLConfig)0; + + XrSessionCreateInfo createInfo{XR_TYPE_SESSION_CREATE_INFO}; + createInfo.next = reinterpret_cast(&m.graphicsBinding); + createInfo.systemId = m.system; + CHECK_XRCMD(xrCreateSession(m.instance, &createInfo, &m.session)); + CHECK(m.session != XR_NULL_HANDLE); + VRB_LOG("OpenXR session created succesfully"); + + m.input->Initialize(m.session); + m.UpdateSpaces(); + m.InitializeViews(); + m.InitializeImmersiveDisplay(); + ProcessEvents(); + + // Initialize layers if needed + vrb::RenderContextPtr context = m.context.lock(); + for (OpenXRLayerPtr& layer: m.uiLayers) { + layer->Init(m.jniEnv, m.session, context); + } + if (m.cubeLayer) { + m.cubeLayer->Init(m.jniEnv, m.session, context); + } + if (m.equirectLayer) { + m.equirectLayer->Init(m.jniEnv, m.session, context); + } +} + + +void +DeviceDelegateOpenXR::LeaveVR() { + CHECK_MSG(!m.boundSwapChain, "Eye swapChain not released before LeaveVR"); + ProcessEvents(); +} + +void +DeviceDelegateOpenXR::OnDestroy() { + m.Shutdown(); +} + +bool +DeviceDelegateOpenXR::IsInVRMode() const { + return m.vrReady; +} + +bool +DeviceDelegateOpenXR::ExitApp() { + return true; +} + +DeviceDelegateOpenXR::DeviceDelegateOpenXR(State &aState) : m(aState) {} + +DeviceDelegateOpenXR::~DeviceDelegateOpenXR() { m.Shutdown(); } + +} // namespace crow diff --git a/app/src/openxr/cpp/DeviceDelegateOpenXR.h b/app/src/openxr/cpp/DeviceDelegateOpenXR.h new file mode 100644 index 000000000..253c90230 --- /dev/null +++ b/app/src/openxr/cpp/DeviceDelegateOpenXR.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DEVICE_DELEGATE_OCULUS_VR_DOT_H +#define DEVICE_DELEGATE_OCULUS_VR_DOT_H + +#include "vrb/Forward.h" +#include "vrb/MacroUtils.h" +#include "DeviceDelegate.h" +#include + +class android_app; +namespace crow { + +class BrowserEGLContext; + +class DeviceDelegateOpenXR; +typedef std::shared_ptr DeviceDelegateOpenXRPtr; + +class DeviceDelegateOpenXR : public DeviceDelegate { +public: + static DeviceDelegateOpenXRPtr Create(vrb::RenderContextPtr& aContext, android_app* aApp); + // DeviceDelegate interface + device::DeviceType GetDeviceType() override; + void SetRenderMode(const device::RenderMode aMode) override; + device::RenderMode GetRenderMode() override; + void RegisterImmersiveDisplay(ImmersiveDisplayPtr aDisplay) override; + void SetImmersiveSize(const uint32_t aEyeWidth, const uint32_t aEyeHeight) override; + GestureDelegateConstPtr GetGestureDelegate() override { return nullptr; } + vrb::CameraPtr GetCamera(const device::Eye aWhich) override; + const vrb::Matrix& GetHeadTransform() const override; + const vrb::Matrix& GetReorientTransform() const override; + void SetReorientTransform(const vrb::Matrix& aMatrix) override; + void SetClearColor(const vrb::Color& aColor) override; + void SetClipPlanes(const float aNear, const float aFar) override; + void SetControllerDelegate(ControllerDelegatePtr& aController) override; + void ReleaseControllerDelegate() override; + int32_t GetControllerModelCount() const override; + const std::string GetControllerModelName(const int32_t aModelIndex) const override; + void SetCPULevel(const device::CPULevel aLevel) override; + void ProcessEvents() override; + bool SupportsFramePrediction(FramePrediction aPrediction) const override; + void StartFrame(const FramePrediction aPrediction) override; + void BindEye(const device::Eye aWhich) override; + void EndFrame(const FrameEndMode aMode) override; + VRLayerQuadPtr CreateLayerQuad(int32_t aWidth, int32_t aHeight, + VRLayerSurface::SurfaceType aSurfaceType) override; + VRLayerQuadPtr CreateLayerQuad(const VRLayerSurfacePtr& aMoveLayer) override; + VRLayerCylinderPtr CreateLayerCylinder(int32_t aWidth, int32_t aHeight, + VRLayerSurface::SurfaceType aSurfaceType) override; + VRLayerCylinderPtr CreateLayerCylinder(const VRLayerSurfacePtr& aMoveLayer) override; + VRLayerCubePtr CreateLayerCube(int32_t aWidth, int32_t aHeight, GLint aInternalFormat) override; + VRLayerEquirectPtr CreateLayerEquirect(const VRLayerPtr &aSource) override; + void DeleteLayer(const VRLayerPtr& aLayer) override; + // Custom methods for NativeActivity render loop based devices. + void EnterVR(const crow::BrowserEGLContext& aEGLContext); + void LeaveVR(); + void OnDestroy(); + bool IsInVRMode() const; + bool ExitApp(); +protected: + struct State; + DeviceDelegateOpenXR(State& aState); + virtual ~DeviceDelegateOpenXR(); +private: + State& m; + VRB_NO_DEFAULTS(DeviceDelegateOpenXR) +}; + +} // namespace crow + +#endif // DEVICE_DELEGATE_OCULUS_VR_DOT_H diff --git a/app/src/openxr/cpp/OpenXRExtensions.cpp b/app/src/openxr/cpp/OpenXRExtensions.cpp new file mode 100644 index 000000000..115a7cb16 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRExtensions.cpp @@ -0,0 +1,31 @@ +#include "OpenXRExtensions.h" +#include "OpenXRHelpers.h" + +namespace crow { + +PFN_xrGetOpenGLESGraphicsRequirementsKHR OpenXRExtensions::sXrGetOpenGLESGraphicsRequirementsKHR = nullptr; +PFN_xrCreateSwapchainAndroidSurfaceKHR OpenXRExtensions::sXrCreateSwapchainAndroidSurfaceKHR = nullptr; + +std::vector OpenXRExtensions::ExtensionNames() { + return { + XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, + XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME, + XR_KHR_ANDROID_SURFACE_SWAPCHAIN_EXTENSION_NAME, + XR_KHR_COMPOSITION_LAYER_CUBE_EXTENSION_NAME, + XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME, + XR_KHR_COMPOSITION_LAYER_EQUIRECT_EXTENSION_NAME + }; +} + +void OpenXRExtensions::Initialize(XrInstance instance) { + CHECK(instance != XR_NULL_HANDLE); + // Extension function must be loaded by name + CHECK_XRCMD(xrGetInstanceProcAddr(instance, "xrGetOpenGLESGraphicsRequirementsKHR", + reinterpret_cast(&sXrGetOpenGLESGraphicsRequirementsKHR))); + + CHECK_XRCMD(xrGetInstanceProcAddr(instance, "xrCreateSwapchainAndroidSurfaceKHR", + reinterpret_cast(&sXrCreateSwapchainAndroidSurfaceKHR))); + +} + +} // namespace crow diff --git a/app/src/openxr/cpp/OpenXRExtensions.h b/app/src/openxr/cpp/OpenXRExtensions.h new file mode 100644 index 000000000..2e7203b11 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRExtensions.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace crow { + class OpenXRExtensions { + public: + static void Initialize(XrInstance instance); + static std::vector ExtensionNames(); + + static PFN_xrGetOpenGLESGraphicsRequirementsKHR sXrGetOpenGLESGraphicsRequirementsKHR; + static PFN_xrCreateSwapchainAndroidSurfaceKHR sXrCreateSwapchainAndroidSurfaceKHR; + }; +} // namespace crow \ No newline at end of file diff --git a/app/src/openxr/cpp/OpenXRHelpers.h b/app/src/openxr/cpp/OpenXRHelpers.h new file mode 100644 index 000000000..6ae482db6 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRHelpers.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include "vrb/Matrix.h" + +namespace crow { + +inline std::string Fmt(const char* fmt, ...) { + va_list vl; + va_start(vl, fmt); + int size = std::vsnprintf(nullptr, 0, fmt, vl); + va_end(vl); + + if (size != -1) { + std::unique_ptr buffer(new char[size + 1]); + + va_start(vl, fmt); + size = std::vsnprintf(buffer.get(), size + 1, fmt, vl); + va_end(vl); + if (size != -1) { + return std::string(buffer.get(), size); + } + } + + throw std::runtime_error("Unexpected vsnprintf failure"); +} + +inline std::string GetXrVersionString(XrVersion ver) { + return Fmt("%d.%d.%d", XR_VERSION_MAJOR(ver), XR_VERSION_MINOR(ver), XR_VERSION_PATCH(ver)); +} + +#define CHK_STRINGIFY(x) #x +#define TOSTRING(x) CHK_STRINGIFY(x) +#define FILE_AND_LINE __FILE__ ":" TOSTRING(__LINE__) + +// Macro to generate stringify functions for OpenXR enumerations based data provided in openxr_reflection.h +// clang-format off +#define ENUM_CASE_STR(name, val) case name: return #name; +#define MAKE_TO_STRING_FUNC(enumType) \ + inline const char* to_string(enumType e) { \ + switch (e) { \ + XR_LIST_ENUM_##enumType(ENUM_CASE_STR) \ + default: return "Unknown " #enumType; \ + } \ + } +// clang-format on + +MAKE_TO_STRING_FUNC(XrReferenceSpaceType); +MAKE_TO_STRING_FUNC(XrViewConfigurationType); +MAKE_TO_STRING_FUNC(XrEnvironmentBlendMode); +MAKE_TO_STRING_FUNC(XrSessionState); +MAKE_TO_STRING_FUNC(XrResult); +MAKE_TO_STRING_FUNC(XrFormFactor); + +[[noreturn]] inline void Throw(std::string failureMessage, const char* originator = nullptr, const char* sourceLocation = nullptr) { + if (originator != nullptr) { + failureMessage += Fmt("\n Origin: %s", originator); + } + if (sourceLocation != nullptr) { + failureMessage += Fmt("\n Source: %s", sourceLocation); + } + + throw std::logic_error(failureMessage); +} + +#define THROW(msg) Throw(msg, nullptr, FILE_AND_LINE); +#define CHECK(exp) \ + { \ + if (!(exp)) { \ + Throw("Check failed", #exp, FILE_AND_LINE); \ + } \ + } +#define CHECK_MSG(exp, msg) \ + { \ + if (!(exp)) { \ + Throw(msg, #exp, FILE_AND_LINE); \ + } \ + } + +[[noreturn]] inline void ThrowXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr) { + Throw(Fmt("XrResult failure [%s]", to_string(res)), originator, sourceLocation); +} + +inline XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr) { + if (XR_FAILED(res)) { + ThrowXrResult(res, originator, sourceLocation); + } + + return res; +} + +#define THROW_XR(xr, cmd) ThrowXrResult(xr, #cmd, FILE_AND_LINE); +#define CHECK_XRCMD(cmd) CheckXrResult(cmd, #cmd, FILE_AND_LINE); +#define CHECK_XRRESULT(res, cmdStr) CheckXrResult(res, cmdStr, FILE_AND_LINE); + + +inline XrPosef XrPoseIdentity() { + XrPosef t{}; + t.orientation.w = 1; + return t; +} + +inline vrb::Matrix XrPoseToMatrix(const XrPosef& aPose) { + vrb::Matrix matrix = vrb::Matrix::Rotation(vrb::Quaternion(aPose.orientation.x, aPose.orientation.y, aPose.orientation.z, aPose.orientation.w)); + matrix.TranslateInPlace(vrb::Vector(aPose.position.x, aPose.position.y, aPose.position.z)); + return matrix; +} + +inline XrPosef MatrixToXrPose(const vrb::Matrix& aMatrix) { + vrb::Quaternion q; + q.SetFromRotationMatrix(aMatrix); + vrb::Vector p = aMatrix.GetTranslation(); + XrPosef result; + result.orientation = XrQuaternionf{q.x(), q.y(), q.z(), q.w()}; + result.position = XrVector3f{p.x(), p.y(), p.z()}; + return result; +} + +} // namespace crow \ No newline at end of file diff --git a/app/src/openxr/cpp/OpenXRInput.cpp b/app/src/openxr/cpp/OpenXRInput.cpp new file mode 100644 index 000000000..3fdc6cfa7 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRInput.cpp @@ -0,0 +1,464 @@ +#include "OpenXRInput.h" +#include "OpenXRHelpers.h" +#include + +namespace crow { + +// Threshold to consider a trigger value as a click +// Used when devices don't map the click value for triggers; +const float kPressThreshold = 0.95f; + + +OpenXRInputPtr OpenXRInput::Create(XrInstance instance, XrSystemProperties systemProperties) { + CHECK(instance != XR_NULL_HANDLE); + auto result = std::make_shared(); + result->instance = instance; + result->systemProperties = systemProperties; + return result; +} + +void +OpenXRInput::Initialize(XrSession session) { + CHECK(session != XR_NULL_HANDLE); + + // Create the main action set. + { + XrActionSetCreateInfo actionSetInfo{XR_TYPE_ACTION_SET_CREATE_INFO}; + strcpy(actionSetInfo.actionSetName, "browser"); + strcpy(actionSetInfo.localizedActionSetName, "Browser"); + actionSetInfo.priority = 0; + CHECK_XRCMD(xrCreateActionSet(instance, &actionSetInfo, &actionSet)); + } + + // Create subactions for left and right hands. + CHECK_XRCMD(xrStringToPath(instance, "/user/hand/left", &handSubactionPath[Hand::Left])); + CHECK_XRCMD(xrStringToPath(instance, "/user/hand/right", &handSubactionPath[Hand::Right])); + + + auto createAction = [&](const char * name, Hand hand, XrActionType actionType, XrAction * action) { + std::string actionName = std::string(name) + (hand == 0 ? "_left" : "_right"); + XrActionCreateInfo actionInfo{XR_TYPE_ACTION_CREATE_INFO}; + actionInfo.actionType = actionType; + strcpy(actionInfo.actionName, actionName.c_str()); + strcpy(actionInfo.localizedActionName, actionName.c_str()); + actionInfo.countSubactionPaths = 1; + actionInfo.subactionPaths = &handSubactionPath[hand]; + CHECK_XRCMD(xrCreateAction(actionSet, &actionInfo, action)); + }; + + auto createPoseAction = [&](const char * name, std::array& action){ + for (auto hand: {Hand::Left, Hand::Right}) { + createAction(name, hand, XR_ACTION_TYPE_POSE_INPUT, &action[hand]); + } + }; + + auto createBooleanAction = [&](const char * name, std::array& action){ + for (auto hand: {Hand::Left, Hand::Right}) { + createAction(name, hand, XR_ACTION_TYPE_BOOLEAN_INPUT, &action[hand]); + } + }; + + auto createFloatAction = [&](const char * name, std::array& action){ + for (auto hand: {Hand::Left, Hand::Right}) { + createAction(name, hand, XR_ACTION_TYPE_FLOAT_INPUT, &action[hand]); + } + }; + + // Create actions. We try to mimic https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping + // Create an input action for getting the left and right hand poses. + createPoseAction("hand_pose", actionPose); + + // Create input actions for menu click detection, usually used for back action. + createBooleanAction("menu", actionMenuClick); + + // Create an input action for trigger click, touch and value detection + createBooleanAction("trigger_click", actionTriggerClick); + createBooleanAction("trigger_touch", actionTriggerTouch); + createFloatAction("trigger_value", actionTriggerValue); + + // Create an input action for squeeze click and value detection + createBooleanAction("squeeze_click", actionSqueezeClick); + createFloatAction("squeeze_value", actionSqueezeValue); + + // Create an input action for trackpad click, touch and values detection + createBooleanAction("trackpad_click", actionTrackpadClick); + createBooleanAction("trackpad_touch", actionTrackpadTouch); + createFloatAction("trackpad_value_x", actionTrackpadX); + createFloatAction("trackpad_value_y", actionTrackpadY); + + // Create an input action for thumbstick click, touch and values detection + createBooleanAction("thumbstick_click", actionThumbstickClick); + createBooleanAction("thumbstick_touch", actionThumbstickTouch); + createFloatAction("thumbstick_value_x", actionThumbstickX); + createFloatAction("thumbstick_value_y", actionThumbstickY); + + // Create an input action for ButtonA and Button B clicks and touch + createBooleanAction("button_a_click", actionButtonAClick); + createBooleanAction("button_a_touch", actionButtonATouch); + createBooleanAction("button_b_click", actionButtonBClick); + createBooleanAction("button_b_touch", actionButtonBTouch); + + // See https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#semantic-path-interaction-profiles +#define DECLARE_PATH(subpath, variable) \ + std::array variable; \ + CHECK_XRCMD(xrStringToPath(instance, "/user/hand/left/" subpath, &variable[Hand::Left])); \ + CHECK_XRCMD(xrStringToPath(instance, "/user/hand/right/" subpath, &variable[Hand::Right])); + + DECLARE_PATH("input/select/click", selectClickPath); + DECLARE_PATH("input/trigger/value", triggerValuePath); + DECLARE_PATH("input/trigger/touch", triggerTouchPath); + DECLARE_PATH("input/trigger/click", triggerClickPath); + DECLARE_PATH("input/squeeze/value", squeezeValuePath); + DECLARE_PATH("input/squeeze/click", squeezeClickPath); + DECLARE_PATH("input/aim/pose", posePath); + DECLARE_PATH("output/haptic", hapticPath); + DECLARE_PATH("input/menu/click", menuClickPath); + DECLARE_PATH("input/back/click", backClickPath); + DECLARE_PATH("input/trackpad/click", trackpadClickPath); + DECLARE_PATH("input/trackpad/touch", trackpadTouchPath); + DECLARE_PATH("input/trackpad/x", trackpadXPath); + DECLARE_PATH("input/trackpad/y", trackpadYPath); + DECLARE_PATH("input/thumbstick/click", thumbstickClickPath); + DECLARE_PATH("input/thumbstick/touch", thumbstickTouchPath); + DECLARE_PATH("input/thumbstick/x", thumbstickXPath); + DECLARE_PATH("input/thumbstick/y", thumbstickYPath); + DECLARE_PATH("input/a/click", buttonAClickPath); + DECLARE_PATH("input/a/touch", buttonATouchPath); + DECLARE_PATH("input/b/click", buttonBClickPath); + DECLARE_PATH("input/b/touch", buttonBTouchPath); + DECLARE_PATH("input/x/click", buttonXClickPath); + DECLARE_PATH("input/x/touch", buttonXTouchPath); + DECLARE_PATH("input/y/click", buttonYClickPath); + DECLARE_PATH("input/y/touch", buttonYTouchPath); + + // Suggest bindings for KHR Simple. Default fallback when we have not implemented a specific controller binding. + { + XrPath khrSimpleInteractionProfilePath; + CHECK_XRCMD( + xrStringToPath(instance, "/interaction_profiles/khr/simple_controller", &khrSimpleInteractionProfilePath)); + std::vector bindings{{// Generic controller mappings + {actionPose[Hand::Left], posePath[Hand::Left]}, + {actionPose[Hand::Right], posePath[Hand::Right]}, + {actionMenuClick[Hand::Left], menuClickPath[Hand::Left]}, + {actionMenuClick[Hand::Right], menuClickPath[Hand::Right]}, + {actionTriggerClick[Hand::Left], selectClickPath[Hand::Left]}, + {actionTriggerClick[Hand::Right], selectClickPath[Hand::Right]}}}; + XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; + suggestedBindings.interactionProfile = khrSimpleInteractionProfilePath; + suggestedBindings.suggestedBindings = bindings.data(); + suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); + CHECK_XRCMD(xrSuggestInteractionProfileBindings(instance, &suggestedBindings)); + } + + // Suggest bindings for Oculus Go controller + { + + } + + // Suggest bindings for Oculus Touch controller. + { + XrPath khrSimpleInteractionProfilePath; + CHECK_XRCMD( + xrStringToPath(instance, "/interaction_profiles/oculus/touch_controller", &khrSimpleInteractionProfilePath)); + std::vector bindings{{// Controller mappings + {actionPose[Hand::Left], posePath[Hand::Left]}, + {actionPose[Hand::Right], posePath[Hand::Right]}, + // Actions available only on left controller + {actionMenuClick[Hand::Left], menuClickPath[Hand::Left]}, + {actionButtonAClick[Hand::Left], buttonXClickPath[Hand::Left]}, + {actionButtonATouch[Hand::Left], buttonXTouchPath[Hand::Left]}, + {actionButtonBClick[Hand::Left], buttonYClickPath[Hand::Left]}, + {actionButtonBTouch[Hand::Left], buttonYTouchPath[Hand::Left]}, + // Actions available only on right controller + {actionButtonAClick[Hand::Right], buttonAClickPath[Hand::Right]}, + {actionButtonATouch[Hand::Right], buttonATouchPath[Hand::Right]}, + {actionButtonBClick[Hand::Right], buttonAClickPath[Hand::Right]}, + {actionButtonBTouch[Hand::Right], buttonATouchPath[Hand::Right]}, + // Actions available on both controllers + {actionTriggerValue[Hand::Left], triggerValuePath[Hand::Left]}, + {actionTriggerValue[Hand::Right], triggerValuePath[Hand::Right]}, + {actionTriggerTouch[Hand::Left], triggerTouchPath[Hand::Left]}, + {actionTriggerTouch[Hand::Right], triggerTouchPath[Hand::Right]}, + {actionSqueezeValue[Hand::Left], squeezeValuePath[Hand::Left]}, + {actionSqueezeValue[Hand::Right], squeezeValuePath[Hand::Right]}, + {actionThumbstickClick[Hand::Left], thumbstickClickPath[Hand::Left]}, + {actionThumbstickClick[Hand::Right], thumbstickClickPath[Hand::Right]}, + {actionThumbstickTouch[Hand::Left], thumbstickTouchPath[Hand::Left]}, + {actionThumbstickTouch[Hand::Right], thumbstickTouchPath[Hand::Right]}, + {actionThumbstickX[Hand::Left], thumbstickXPath[Hand::Left]}, + {actionThumbstickX[Hand::Right], thumbstickXPath[Hand::Right]}, + {actionThumbstickY[Hand::Left], thumbstickYPath[Hand::Left]}, + {actionThumbstickY[Hand::Right], thumbstickYPath[Hand::Right]}}}; + XrInteractionProfileSuggestedBinding suggestedBindings{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING}; + suggestedBindings.interactionProfile = khrSimpleInteractionProfilePath; + suggestedBindings.suggestedBindings = bindings.data(); + suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size(); + CHECK_XRCMD(xrSuggestInteractionProfileBindings(instance, &suggestedBindings)); + } + + // Initialize pose actions + { + XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; + actionSpaceInfo.action = actionPose[Hand::Left]; + actionSpaceInfo.poseInActionSpace.orientation.w = 1.f; + actionSpaceInfo.subactionPath = handSubactionPath[Hand::Left]; + CHECK_XRCMD(xrCreateActionSpace(session, &actionSpaceInfo, &controllerState[Hand::Left].space)); + } + { + XrActionSpaceCreateInfo actionSpaceInfo{XR_TYPE_ACTION_SPACE_CREATE_INFO}; + actionSpaceInfo.action = actionPose[Hand::Right]; + actionSpaceInfo.poseInActionSpace.orientation.w = 1.f; + actionSpaceInfo.subactionPath = handSubactionPath[Hand::Right]; + CHECK_XRCMD(xrCreateActionSpace(session, &actionSpaceInfo, &controllerState[Hand::Right].space)); + } + + // Attach actions to session + XrSessionActionSetsAttachInfo attachInfo{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO}; + attachInfo.countActionSets = 1; + attachInfo.actionSets = &actionSet; + CHECK_XRCMD(xrAttachSessionActionSets(session, &attachInfo)); +} + +void OpenXRInput::Update(XrSession session, XrTime predictedDisplayTime, XrSpace baseSpace, device::RenderMode renderMode, ControllerDelegatePtr& delegate) { + CHECK(session != XR_NULL_HANDLE); + + // Sync actions + const XrActiveActionSet activeActionSet{actionSet, XR_NULL_PATH}; + XrActionsSyncInfo syncInfo{XR_TYPE_ACTIONS_SYNC_INFO}; + syncInfo.countActiveActionSets = 1; + syncInfo.activeActionSets = &activeActionSet; + CHECK_XRCMD(xrSyncActions(session, &syncInfo)); + + // Query actions and pose state for each hand + for (auto hand : {Hand::Left, Hand::Right}) { + const int index = hand; + ControllerState& controller = controllerState[hand]; + + // Query pose state + XrActionStateGetInfo getInfo{XR_TYPE_ACTION_STATE_GET_INFO}; + getInfo.subactionPath = handSubactionPath[hand]; + getInfo.action = actionPose[hand]; + XrActionStatePose poseState{XR_TYPE_ACTION_STATE_POSE}; + CHECK_XRCMD(xrGetActionStatePose(session, &getInfo, &poseState)); + + if (!poseState.isActive) { + if (controller.created) { + delegate->SetEnabled(hand, false); + } + // Controller inactive, skip. + continue; + } + + if (!controller.created) { + if (hand == 0) { + vrb::Matrix beamTransform = vrb::Matrix::Translation(vrb::Vector(-0.011f, -0.007f, 0.0f)); + delegate->CreateController(index, index,"Oculus Touch (Left)", beamTransform); + delegate->SetLeftHanded(index, true); + delegate->SetImmersiveBeamTransform(index, beamTransform); + + } else { + vrb::Matrix beamTransform = vrb::Matrix::Translation(vrb::Vector(0.011f, -0.007f, 0.0f)); + delegate->CreateController(hand, hand, "Oculus Touch (Right)", beamTransform); + delegate->SetImmersiveBeamTransform(index, beamTransform); + } + + delegate->SetControllerType(index, device::OculusQuest); // TODO: remove this + // Set default counts for xr-standard-gamepad-mapping + // See: https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping + delegate->SetButtonCount(index, 7); + delegate->SetHapticCount(index, 0); + controller.created = true; + } + + // Query controller tracking and map the pose. + XrSpaceLocation spaceLocation{XR_TYPE_SPACE_LOCATION}; + XrResult res = xrLocateSpace(controller.space, baseSpace, predictedDisplayTime, &spaceLocation); + CHECK_XRRESULT(res, "Input xrLocateSpace"); + if (XR_UNQUALIFIED_SUCCESS(res)) { + controller.enabled = true; + delegate->SetEnabled(index, true); + delegate->SetVisible(hand, true); + // set up controller capability caps + device::CapabilityFlags caps = device::Orientation; + if (spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) { + caps |= (spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) ? device::Position : device::PositionEmulated; + } + delegate->SetCapabilityFlags(index, caps); + // set up pose + vrb::Matrix transform = XrPoseToMatrix(spaceLocation.pose); + if (renderMode == device::RenderMode::StandAlone) { + transform.TranslateInPlace(vrb::Vector(0.0f, 1.7f, 0.0f)); + } + delegate->SetTransform(index, transform); + } else { + controller.enabled = false; + delegate->SetEnabled(hand, false); + delegate->SetVisible(hand, false); + // Tracking lost or inactive, skip. + continue; + } + +#define QUERY_BOOLEAN_STATE(variable, actionName) \ + XrActionStateBoolean variable{XR_TYPE_ACTION_STATE_BOOLEAN}; \ + { \ + XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO}; \ + info.subactionPath = handSubactionPath[hand]; \ + info.action = actionName[hand]; \ + CHECK_XRCMD(xrGetActionStateBoolean(session, &info, &variable)); \ + } + +#define QUERY_FLOAT_STATE(variable, actionName) \ + XrActionStateFloat variable{XR_TYPE_ACTION_STATE_FLOAT}; \ + { \ + XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO}; \ + info.subactionPath = handSubactionPath[hand]; \ + info.action = actionName[hand]; \ + CHECK_XRCMD(xrGetActionStateFloat(session, &info, &variable)); \ + } + + // Query buttons and axes + QUERY_BOOLEAN_STATE(menuClick, actionMenuClick); + QUERY_BOOLEAN_STATE(triggerClick, actionTriggerClick); + QUERY_BOOLEAN_STATE(triggerTouch, actionTriggerTouch); + QUERY_FLOAT_STATE(triggerValue, actionTriggerValue); + QUERY_BOOLEAN_STATE(squeezeClick, actionSqueezeClick); + QUERY_FLOAT_STATE(squeezeValue, actionSqueezeValue); + QUERY_BOOLEAN_STATE(trackpadClick, actionTrackpadClick); + QUERY_BOOLEAN_STATE(trackpadTouch, actionTrackpadTouch); + QUERY_FLOAT_STATE(trackpadX, actionTrackpadX); + QUERY_FLOAT_STATE(trackpadY, actionTrackpadY); + QUERY_BOOLEAN_STATE(thumbStickClick, actionThumbstickTouch); + QUERY_BOOLEAN_STATE(thumbstickTouch, actionThumbstickTouch); + QUERY_FLOAT_STATE(thumbstickX, actionThumbstickX); + QUERY_FLOAT_STATE(thumbstickY, actionThumbstickY); + QUERY_BOOLEAN_STATE(buttonAClick, actionButtonAClick); + QUERY_BOOLEAN_STATE(buttonATouch, actionButtonATouch); + QUERY_BOOLEAN_STATE(buttonBClick, actionButtonBClick); + QUERY_BOOLEAN_STATE(buttonBTouch, actionButtonBTouch); + + // Map to controller delegate + std::array axes; + + if (menuClick.isActive) { + const bool pressed = menuClick.currentState != 0; + delegate->SetButtonState(index, ControllerDelegate::BUTTON_APP, -1, pressed, pressed); + } + + if (triggerValue.isActive || triggerClick.isActive || triggerTouch.isActive) { + bool pressed = triggerClick.isActive && triggerClick.currentState != 0; + if (!triggerClick.isActive) { + pressed |= triggerValue.isActive && triggerValue.currentState > kPressThreshold; + } + bool touched = pressed; + touched |= triggerValue.isActive && triggerValue.currentState > 0.0f; + touched |= triggerTouch.isActive && triggerTouch.currentState != 0; + float value = pressed ? 1.0f : 0.0f; + if (triggerValue.isActive) { + value = triggerValue.currentState; + } + + delegate->SetButtonState(index, ControllerDelegate::BUTTON_TRIGGER, device::kImmersiveButtonTrigger, pressed, touched, value); + if (pressed && renderMode == device::RenderMode::Immersive) { + delegate->SetSelectActionStart(index); + } else { + delegate->SetSelectActionStop(index); + } + } + + if (squeezeClick.isActive || squeezeValue.isActive) { + bool pressed = squeezeClick.isActive && squeezeClick.currentState != 0; + if (!squeezeClick.isActive) { + pressed |= squeezeValue.isActive && squeezeValue.currentState > kPressThreshold; + } + float value = pressed ? 1.0f : 0.0f; + if (squeezeValue.isActive) { + value = squeezeValue.currentState; + } + delegate->SetButtonState(index, ControllerDelegate::BUTTON_SQUEEZE, device::kImmersiveButtonSqueeze, pressed, pressed, value); + if (pressed && renderMode == device::RenderMode::Immersive) { + delegate->SetSqueezeActionStart(index); + } else { + delegate->SetSqueezeActionStop(index); + } + } + + if (trackpadClick.isActive || trackpadTouch.isActive || trackpadX.isActive || trackpadY.isActive) { + bool pressed = trackpadClick.isActive && trackpadClick.currentState != 0; + bool touched = pressed || (trackpadTouch.isActive && trackpadTouch.currentState != 0); + const float x = trackpadX.isActive ? trackpadX.currentState : 0.0f; + const float y = trackpadY.isActive ? trackpadY.currentState : 0.0f; + delegate->SetButtonState(index, ControllerDelegate::BUTTON_TOUCHPAD, device::kImmersiveButtonTouchpad, pressed, touched); + axes[device::kImmersiveAxisTouchpadX] = x; + axes[device::kImmersiveAxisTouchpadY] = y; + delegate->SetScrolledDelta(index, x, y); + } + + if (thumbStickClick.isActive || thumbstickTouch.isActive || thumbstickX.isActive || thumbstickY.isActive) { + bool pressed = thumbStickClick.isActive && thumbStickClick.currentState != 0; + bool touched = pressed || (thumbstickTouch.isActive && thumbstickTouch.currentState != 0); + const float x = thumbstickX.isActive ? thumbstickX.currentState : 0.0f; + const float y = thumbstickY.isActive ? thumbstickY.currentState : 0.0f; + delegate->SetButtonState(index, ControllerDelegate::BUTTON_TOUCHPAD, device::kImmersiveButtonThumbstick, pressed, touched); + axes[device::kImmersiveAxisThumbstickX] = x; + axes[device::kImmersiveAxisThumbstickY] = y; + delegate->SetScrolledDelta(index, x, y); + } + + if (buttonAClick.isActive) { + const bool pressed = buttonAClick.currentState != 0; + const bool touched = pressed || (buttonATouch.isActive && buttonATouch.currentState != 0); + delegate->SetButtonState(index, ControllerDelegate::BUTTON_A, device::kImmersiveButtonA, pressed, touched); + } + + if (buttonBClick.isActive) { + const bool pressed = buttonBClick.currentState != 0; + const bool touched = pressed || (buttonBTouch.isActive && buttonBTouch.currentState != 0); + delegate->SetButtonState(index, ControllerDelegate::BUTTON_B, device::kImmersiveButtonB, pressed, touched); + } + + delegate->SetAxes(index, axes.data(), axes.size()); + } +} + +int32_t OpenXRInput::GetControllerModelCount() const { +#ifdef OCULUSVR + return systemProperties.trackingProperties.positionTracking ? 2 : 1; +#else +#error Platform controller not implemented +#endif +} + +const std::string OpenXRInput::GetControllerModelName(const int32_t aModelIndex) const { +#ifdef OCULUSVR + if (systemProperties.trackingProperties.positionTracking != 0) { + switch (aModelIndex) { + case 0: + return "vr_controller_oculusquest_left.obj"; + case 1: + return "vr_controller_oculusquest_right.obj"; + default: + VRB_WARN("GetControllerModelName() failed."); + return ""; + } + } else { + return "vr_controller_oculusgo.obj"; + } +#else +#error Platform controller not implemented +#endif +} + + +void OpenXRInput::Destroy() { + if (actionSet != XR_NULL_HANDLE) { + CHECK_XRCMD(xrDestroyActionSet(actionSet)); + actionSet = XR_NULL_HANDLE; + // No need to destroy input actions, they are destroyed automatically when destroying the parent action set + } +} + +OpenXRInput::~OpenXRInput() { + Destroy(); +} + +} // namespace crow \ No newline at end of file diff --git a/app/src/openxr/cpp/OpenXRInput.h b/app/src/openxr/cpp/OpenXRInput.h new file mode 100644 index 000000000..0054bf0c0 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRInput.h @@ -0,0 +1,67 @@ +#pragma once + +#include "vrb/Forward.h" +#include +#include "jni.h" +#include +#include + +#include + +#include "ControllerDelegate.h" +#include "ElbowModel.h" + +namespace crow { + +class OpenXRInput; +typedef std::shared_ptr OpenXRInputPtr; + +class OpenXRInput { +public: + enum Hand { + Left = 0, + Right = 1, + Count = 2 + }; + struct ControllerState { + bool enabled = false; + bool created = false; + XrSpace space = XR_NULL_HANDLE; + }; + XrInstance instance = XR_NULL_HANDLE; + XrActionSet actionSet = XR_NULL_HANDLE; + std::array actionPose; + std::array actionMenuClick; + std::array actionTriggerClick; + std::array actionTriggerTouch; + std::array actionTriggerValue; + std::array actionSqueezeClick; + std::array actionSqueezeValue; + std::array actionTrackpadClick; + std::array actionTrackpadTouch; + std::array actionTrackpadX; + std::array actionTrackpadY; + std::array actionThumbstickClick; + std::array actionThumbstickTouch; + std::array actionThumbstickX; + std::array actionThumbstickY; + std::array actionButtonAClick; + std::array actionButtonBClick; + std::array actionButtonATouch; + std::array actionButtonBTouch; + + std::array handSubactionPath; + std::array controllerState; + XrSystemProperties systemProperties; + + static OpenXRInputPtr Create(XrInstance instance, XrSystemProperties systemProperties); + void Initialize(XrSession session); + void Update(XrSession session, XrTime predictedDisplayTime, XrSpace baseSpace, device::RenderMode renderMode, ControllerDelegatePtr& delegate); + int32_t GetControllerModelCount() const; + const std::string GetControllerModelName(const int32_t aModelIndex) const; + void Destroy(); + + ~OpenXRInput(); +}; + +} // namespace crow \ No newline at end of file diff --git a/app/src/openxr/cpp/OpenXRLayers.cpp b/app/src/openxr/cpp/OpenXRLayers.cpp new file mode 100644 index 000000000..8f31d298c --- /dev/null +++ b/app/src/openxr/cpp/OpenXRLayers.cpp @@ -0,0 +1,221 @@ +#include "OpenXRLayers.h" +#include "vrb/RenderContext.h" + +namespace crow { + +static XrRect2Di GetRect(int32_t width, int32_t height, device::EyeRect rect) { + XrRect2Di result; + result.offset.x = (int32_t)(rect.mX * (float)width); + result.offset.y = (int32_t)(rect.mY * (float)height); + result.extent.width = (int32_t)(rect.mWidth * (float)width); + result.extent.height = (int32_t)(rect.mHeight * (float)height); + return result; +} + +// OpenXRLayerQuad + +OpenXRLayerQuadPtr +OpenXRLayerQuad::Create(JNIEnv *aEnv, const VRLayerQuadPtr& aLayer, const OpenXRLayerPtr& aSource) { + auto result = std::make_shared(); + result->layer = aLayer; + if (aSource) { + result->TakeSurface(aEnv, aSource); + } + return result; +} + +void +OpenXRLayerQuad::Init(JNIEnv * aEnv, XrSession session, vrb::RenderContextPtr& aContext) { + for (auto& xrLayer: xrLayers) { + xrLayer = {XR_TYPE_COMPOSITION_LAYER_QUAD}; + } + OpenXRLayerSurface::Init(aEnv, session, aContext); +} + +void +OpenXRLayerQuad::Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) { + OpenXRLayerSurface::Update(aSpace, aPose, aClearSwapChain); + + for (int i = 0; i < xrLayers.size(); ++i) { + device::Eye eye = i == 0 ? device::Eye::Left : device::Eye::Right; + xrLayers[i].pose = MatrixToXrPose(layer->GetModelTransform(eye)); + xrLayers[i].size.width = layer->GetWorldWidth() * 0.5f; + xrLayers[i].size.height = layer->GetWorldHeight() * 0.5f; + device::EyeRect rect = layer->GetTextureRect(eye); + xrLayers[i].subImage.swapchain = swapchain->SwapChain(); + xrLayers[i].subImage.imageArrayIndex = 0; + xrLayers[i].subImage.imageRect = GetRect(layer->GetWidth(), layer->GetHeight(), rect); + } +} + +// OpenXRLayerCylinder +OpenXRLayerCylinderPtr +OpenXRLayerCylinder::Create(JNIEnv *aEnv, const VRLayerCylinderPtr& aLayer, const OpenXRLayerPtr& aSource) { + auto result = std::make_shared(); + result->layer = aLayer; + if (aSource) { + result->TakeSurface(aEnv, aSource); + } + return result; +} + +void +OpenXRLayerCylinder::Init(JNIEnv * aEnv, XrSession session, vrb::RenderContextPtr& aContext) { + for (auto& xrLayer: xrLayers) { + xrLayer = {XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR}; + } + OpenXRLayerSurface::Init(aEnv, session, aContext); +} + +void +OpenXRLayerCylinder::Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) { + OpenXRLayerSurface::Update(aSpace, aPose, aClearSwapChain); + + for (int i = 0; i < xrLayers.size(); ++i) { + device::Eye eye = i == 0 ? device::Eye::Left : device::Eye::Right; + vrb::Matrix matrix = XrPoseToMatrix(aPose).PostMultiply(layer->GetModelTransform(eye)); + xrLayers[i].pose = MatrixToXrPose(matrix); + xrLayers[i].radius = layer->GetRadius(); + // See Cylinder.cpp: texScaleX = M_PI / theta; + xrLayers[i].centralAngle = (float) M_PI / layer->GetUVTransform(eye).GetScale().x(); + xrLayers[i].aspectRatio = layer->GetWorldWidth() / layer->GetWorldHeight(); + device::EyeRect rect = layer->GetTextureRect(device::Eye::Left); + xrLayers[i].subImage.swapchain = swapchain->SwapChain(); + xrLayers[i].subImage.imageArrayIndex = 0; + xrLayers[i].subImage.imageRect = GetRect(layer->GetWidth(), layer->GetHeight(), rect); + } +} + + +// OpenXRLayerCube + +OpenXRLayerCubePtr +OpenXRLayerCube::Create(const VRLayerCubePtr& aLayer, GLint aInternalFormat) { + auto result = std::make_shared(); + result->layer = aLayer; + result->glFormat = aInternalFormat; + return result; +} + +void +OpenXRLayerCube::Init(JNIEnv * aEnv, XrSession session, vrb::RenderContextPtr& aContext) { + if (this->IsSwapChainReady()) { + return; + } + + for (auto& xrLayer: xrLayers) { + xrLayer = {XR_TYPE_COMPOSITION_LAYER_CUBE_KHR}; + } + + XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + info.width = (uint32_t) layer->GetWidth(); + info.height = (uint32_t) layer->GetHeight(); + info.format = glFormat; + info.mipCount = 1; + info.faceCount = 6; + info.sampleCount = 1; + info.arraySize = 1; + swapchain = OpenXRSwapChain::create(); + swapchain->InitCubemap(aContext, session, info); + layer->SetTextureHandle(swapchain->CubemapTexture()); + + OpenXRLayerBase::Init(aEnv, session, aContext); +} + +void +OpenXRLayerCube::Destroy() { + if (!swapchain) { + return; + } + layer->SetTextureHandle(0); + layer->SetLoaded(false); + OpenXRLayerBase::Destroy(); +} + +bool +OpenXRLayerCube::IsLoaded() const { + return layer->IsLoaded(); +} + +void +OpenXRLayerCube::Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) { + OpenXRLayerBase::Update(aSpace, aPose, aClearSwapChain); + + for (auto& xrLayer: xrLayers) { + xrLayer.layerFlags = 0; + xrLayer.swapchain = swapchain->SwapChain(); + xrLayer.imageArrayIndex = 0; + xrLayer.orientation = XrQuaternionf {0.0f, 0.0f, 0.0f, 1.0f}; + } +} + +// OpenXRLayerEquirect; + +OpenXRLayerEquirectPtr +OpenXRLayerEquirect::Create(const VRLayerEquirectPtr& aLayer, const OpenXRLayerPtr& aSourceLayer) { + auto result = std::make_shared(); + result->layer = aLayer; + result->sourceLayer = aSourceLayer; + return result; +} + +void +OpenXRLayerEquirect::Init(JNIEnv * aEnv, XrSession session, vrb::RenderContextPtr& aContext) { + OpenXRLayerPtr source = sourceLayer.lock(); + if (!source) { + return; + } + swapchain = source->GetSwapChain(); + for (auto& xrLayer: xrLayers) { + xrLayer = {XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR}; + } + OpenXRLayerBase::Init(aEnv, session, aContext); +} + +void +OpenXRLayerEquirect::Destroy() { + swapchain = nullptr; + OpenXRLayerBase::Destroy(); +} + +bool +OpenXRLayerEquirect::IsDrawRequested() const { + OpenXRLayerPtr source = sourceLayer.lock(); + return source && source->GetSwapChain() && source->IsComposited() && layer->IsDrawRequested(); +} + +void +OpenXRLayerEquirect::Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) { + OpenXRLayerPtr source = sourceLayer.lock(); + if (source) { + swapchain = source->GetSwapChain(); + } + OpenXRLayerBase::Update(aSpace, aPose, aClearSwapChain); + + for (int i = 0; i < xrLayers.size(); ++i) { + device::Eye eye = i == 0 ? device::Eye::Left : device::Eye::Right; + // Map video orientation + vrb::Matrix transform = XrPoseToMatrix(aPose).PostMultiply(layer->GetModelTransform(eye)); + xrLayers[i].pose = XrPoseIdentity(); //MatrixToXrPose(transform); + + // Map surface and rect + device::EyeRect rect = layer->GetTextureRect(eye); + xrLayers[i].subImage.swapchain = swapchain->SwapChain(); + xrLayers[i].subImage.imageArrayIndex = 0; + xrLayers[i].subImage.imageRect = GetRect(swapchain->Width(), swapchain->Height(), rect); + + // Zero radius value is treated as an infinite sphere + xrLayers[i].radius = 0; + + // Map video projection UV transform + const vrb::Vector scale = layer->GetUVTransform(eye).GetScale(); + const vrb::Vector translation = layer->GetUVTransform(eye).GetTranslation(); + xrLayers[i].scale.x = scale.x(); + xrLayers[i].scale.y = scale.y(); + xrLayers[i].bias.x = translation.x(); + xrLayers[i].bias.y = translation.y(); + } +} + + +} diff --git a/app/src/openxr/cpp/OpenXRLayers.h b/app/src/openxr/cpp/OpenXRLayers.h new file mode 100644 index 000000000..6dc96e96e --- /dev/null +++ b/app/src/openxr/cpp/OpenXRLayers.h @@ -0,0 +1,346 @@ +#pragma once + +#include "vrb/Forward.h" +#include "vrb/MacroUtils.h" +#include "vrb/FBO.h" +#include "vrb/Color.h" +#include "vrb/Matrix.h" +#include "vrb/GLError.h" +#include "DeviceDelegate.h" +#include "VRLayer.h" +#include +#include "jni.h" +#include +#include +#include "OpenXRSwapChain.h" +#include "OpenXRHelpers.h" +#include + + +namespace crow { + +class OpenXRLayer; + +typedef std::shared_ptr OpenXRLayerPtr; + +struct SurfaceChangedTarget { + OpenXRLayer * layer; + + SurfaceChangedTarget(OpenXRLayer *aLayer) : layer(aLayer) {}; +}; + +typedef std::shared_ptr SurfaceChangedTargetPtr; +typedef std::weak_ptr SurfaceChangedTargetWeakPtr; + +class OpenXRLayer { +public: + virtual void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) = 0; + virtual void Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) = 0; + virtual OpenXRSwapChainPtr GetSwapChain() const = 0; + virtual uint32_t HeaderCount() const = 0; + virtual const XrCompositionLayerBaseHeader* Header(uint32_t aIndex) const = 0; + virtual void SetCurrentEye(device::Eye aEye) = 0; + virtual bool IsDrawRequested() const = 0; + virtual bool GetDrawInFront() const = 0; + virtual void ClearRequestDraw() = 0; + virtual bool IsComposited() const = 0; + virtual void SetComposited(bool aValue) = 0; + virtual VRLayerPtr GetLayer() const = 0; + virtual void Destroy() = 0; + typedef std::function BindDelegate; + virtual void SetBindDelegate(const BindDelegate &aDelegate) = 0; + virtual jobject GetSurface() const = 0; + virtual SurfaceChangedTargetPtr GetSurfaceChangedTarget() const = 0; + + virtual void + HandleResize(const OpenXRSwapChainPtr& newSwapChain) = 0; + + virtual ~OpenXRLayer() {} +}; + +template +class OpenXRLayerBase : public OpenXRLayer { +public: + OpenXRSwapChainPtr swapchain; + SurfaceChangedTargetPtr surfaceChangedTarget; + T layer; + std::array xrLayers; + + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override { + layer->SetInitialized(true); + surfaceChangedTarget = std::make_shared(this); + SurfaceChangedTargetWeakPtr weakTarget = surfaceChangedTarget; + layer->NotifySurfaceChanged(VRLayer::SurfaceChange::Create, [=]() { + SurfaceChangedTargetPtr target = weakTarget.lock(); + if (target) { + target->layer->SetComposited(true); + } + }); + } + + virtual void + Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) override { + for (int i = 0; i < xrLayers.size(); ++i) { + xrLayers[i].layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + xrLayers[i].eyeVisibility = XR_EYE_VISIBILITY_BOTH; + xrLayers[i].space = aSpace; + } + } + + virtual OpenXRSwapChainPtr GetSwapChain() const override { + return swapchain; + } + + uint32_t HeaderCount() const override { + // The first layer is used for both eyes by default. + // Layers can override this behavior to support different settings per eye. + if (xrLayers[0].eyeVisibility == XR_EYE_VISIBILITY_BOTH) { + return 1; + } + return 2; + } + + const XrCompositionLayerBaseHeader* Header(uint32_t aIndex) const override { + CHECK(aIndex < xrLayers.size()); + return reinterpret_cast(&xrLayers[aIndex]); + } + + void SetCurrentEye(device::Eye aEye) override { + layer->SetCurrentEye(aEye); + } + + virtual bool IsDrawRequested() const override { + return layer->IsDrawRequested() && + ((IsSwapChainReady() && IsComposited()) || layer->GetClearColor().Alpha() > 9999999999999.0f); // TODO: remove + } + + bool GetDrawInFront() const override { + return layer->GetDrawInFront(); + } + + void ClearRequestDraw() override { + layer->ClearRequestDraw(); + } + + bool IsComposited() const override { + return layer->IsComposited(); + } + + bool IsSwapChainReady() const { + return this->swapchain && this->swapchain->SwapChain() != XR_NULL_HANDLE; + } + + void SetComposited(bool aValue) override { + layer->SetComposited(aValue); + } + + VRLayerPtr GetLayer() const override { + return layer; + } + + void SetClipEnabled(bool aEnabled) { + } + + void Destroy() override { + swapchain = nullptr; + layer->SetInitialized(false); + SetComposited(false); + layer->NotifySurfaceChanged(VRLayer::SurfaceChange::Destroy, nullptr); + } + + void SetBindDelegate(const BindDelegate &aDelegate) override {} + + jobject GetSurface() const override { + return nullptr; + } + + SurfaceChangedTargetPtr GetSurfaceChangedTarget() const override { + return surfaceChangedTarget; + } + + void HandleResize(const OpenXRSwapChainPtr& newSwapChain) override {} + + XrSwapchain GetTargetSwapChain(XrSwapchain aClearSwapChain) { + return (IsComposited() || layer->GetClearColor().Alpha() == 0) ? swapchain->SwapChain() : aClearSwapChain; + } + +protected: + virtual ~OpenXRLayerBase() {} + XrSwapchainCreateInfo GetSwapChainCreateInfo(VRLayerSurface::SurfaceType aSurfaceType, uint32_t width, uint32_t height) { + XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO}; + info.width = width; + info.height = height; + if (aSurfaceType == VRLayerSurface::SurfaceType::AndroidSurface) { + // These members must be zero + // See https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/xrCreateSwapchainAndroidSurfaceKHR.html#XR_KHR_android_surface_swapchain + info.format = 0; + info.mipCount = 0; + info.faceCount = 0; + info.sampleCount = 0; + info.arraySize = 0; + info.usageFlags = 0; + } else { + info.format = GL_RGBA8; + info.mipCount = 1; + info.faceCount = 1; + info.sampleCount = 1; + info.arraySize = 1; + info.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; + } + + return info; + } +}; + + +template +class OpenXRLayerSurface : public OpenXRLayerBase { +public: + vrb::RenderContextWeak contextWeak; + OpenXRLayer::BindDelegate bindDelegate; + + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override { + this->contextWeak = aContext; + if (this->swapchain && this->swapchain->SwapChain() != XR_NULL_HANDLE) { + return; + } + + InitSwapChain(aEnv, session, this->swapchain); + this->layer->SetResizeDelegate([=] { + Resize(); + }); + OpenXRLayerBase::Init(aEnv, session, aContext); + } + + void Resize() { + if (!this->IsSwapChainReady()) { + return; + } + // Delay the destruction of the current swapChain until the new one is composited. + // This is required to prevent a black flicker when resizing. + OpenXRSwapChainPtr newSwapChain; + InitSwapChain(this->swapchain->Env(), this->swapchain->Session(), newSwapChain); + this->layer->SetSurface(newSwapChain->AndroidSurface()); + + SurfaceChangedTargetWeakPtr weakTarget = this->surfaceChangedTarget; + this->layer->NotifySurfaceChanged(VRLayer::SurfaceChange::Create, [=]() { + SurfaceChangedTargetPtr target = weakTarget.lock(); + if (target && target->layer) { + target->layer->HandleResize(newSwapChain); + } + }); + } + + void + HandleResize(const OpenXRSwapChainPtr& newSwapChain) override { + this->swapchain = newSwapChain; + this->SetComposited(true); + } + + void SetBindDelegate(const OpenXRLayer::BindDelegate &aDelegate) override { + bindDelegate = aDelegate; + this->layer->SetBindDelegate([=](GLenum aTarget, bool aBind) { + if (bindDelegate) { + bindDelegate(this->swapchain, aTarget, aBind); + } + }); + } + + virtual jobject GetSurface() const override { + if (!this->swapchain) { + return nullptr; + } + return this->swapchain->AndroidSurface(); + } + +protected: + void TakeSurface(JNIEnv * aEnv, const OpenXRLayerPtr &aSource) { + this->swapchain = aSource->GetSwapChain(); + this->surfaceChangedTarget = aSource->GetSurfaceChangedTarget(); + if (this->surfaceChangedTarget) { + // Indicate that the first composite notification should be notified to this layer. + this->surfaceChangedTarget->layer = this; + } + this->SetComposited(aSource->IsComposited()); + this->layer->SetInitialized(aSource->GetLayer()->IsInitialized()); + this->layer->SetResizeDelegate([=] { + Resize(); + }); + } + +private: + void InitSwapChain(JNIEnv* aEnv, XrSession session, OpenXRSwapChainPtr &swapChainOut) { + swapChainOut = OpenXRSwapChain::create(); + XrSwapchainCreateInfo info = this->GetSwapChainCreateInfo(this->layer->GetSurfaceType(), this->layer->GetWidth(), this->layer->GetHeight()); + if (this->layer->GetSurfaceType() == VRLayerQuad::SurfaceType::AndroidSurface) { + swapChainOut->InitAndroidSurface(aEnv, session, info); + this->layer->SetSurface(swapChainOut->AndroidSurface()); + } else { + auto render = this->contextWeak.lock(); + vrb::FBO::Attributes attributes; + attributes.depth = false; + attributes.samples = 0; + swapChainOut->InitFBO(render, session, info, attributes); + } + } +}; + +class OpenXRLayerQuad; + +typedef std::shared_ptr OpenXRLayerQuadPtr; + +class OpenXRLayerQuad : public OpenXRLayerSurface { +public: + static OpenXRLayerQuadPtr + Create(JNIEnv *aEnv, const VRLayerQuadPtr &aLayer, const OpenXRLayerPtr &aSource = nullptr); + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override; + void Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) override; +}; + + +class OpenXRLayerCylinder; + +typedef std::shared_ptr OpenXRLayerCylinderPtr; + +class OpenXRLayerCylinder : public OpenXRLayerSurface { +public: + static OpenXRLayerCylinderPtr + Create(JNIEnv *aEnv, const VRLayerCylinderPtr &aLayer, const OpenXRLayerPtr &aSource = nullptr); + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override; + void Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) override; +}; + + +class OpenXRLayerCube; + +typedef std::shared_ptr OpenXRLayerCubePtr; + +class OpenXRLayerCube : public OpenXRLayerBase { +public: + static OpenXRLayerCubePtr Create(const VRLayerCubePtr &aLayer, GLint aInternalFormat); + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override; + void Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) override; + void Destroy() override; + bool IsLoaded() const; + +protected: + GLint glFormat; +}; + +class OpenXRLayerEquirect; + +typedef std::shared_ptr OpenXRLayerEquirectPtr; + +class OpenXRLayerEquirect : public OpenXRLayerBase { +public: + std::weak_ptr sourceLayer; + + static OpenXRLayerEquirectPtr + Create(const VRLayerEquirectPtr &aLayer, const OpenXRLayerPtr &aSourceLayer); + void Init(JNIEnv *aEnv, XrSession session, vrb::RenderContextPtr &aContext) override; + void Update(XrSpace aSpace, const XrPosef &aPose, XrSwapchain aClearSwapChain) override; + void Destroy() override; + bool IsDrawRequested() const override; +}; + +} diff --git a/app/src/openxr/cpp/OpenXRSwapChain.cpp b/app/src/openxr/cpp/OpenXRSwapChain.cpp new file mode 100644 index 000000000..28de386c2 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRSwapChain.cpp @@ -0,0 +1,178 @@ +#include "OpenXRSwapChain.h" +#include "OpenXRHelpers.h" +#include "OpenXRExtensions.h" +#include "vrb/FBO.h" +#include "vrb/GLError.h" +#include "vrb/Logger.h" + +namespace crow { + +OpenXRSwapChainPtr +OpenXRSwapChain::create() { + return std::make_shared(); +} + +void +OpenXRSwapChain::InitFBO(vrb::RenderContextPtr &aContext, XrSession aSession, const XrSwapchainCreateInfo& aInfo, vrb::FBO::Attributes aAttributes) { + Destroy(); + info = aInfo; + context = aContext; + attributes = aAttributes; + session = aSession; + + CHECK(aInfo.faceCount == 1); + CHECK(aSession != XR_NULL_HANDLE); + CHECK_XRCMD(xrCreateSwapchain(aSession, &info, &swapchain)); + CHECK(swapchain != XR_NULL_HANDLE); + + uint32_t imageCount; + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, 0, &imageCount, nullptr)); + CHECK(imageCount > 0); + imageBuffer.resize(imageCount); + fbos.resize(imageCount); + for (XrSwapchainImageOpenGLESKHR& image : imageBuffer) { + image.type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR; + images.push_back(reinterpret_cast(&image)); + } + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, imageCount, &imageCount, images[0])); +} + +void +OpenXRSwapChain::InitAndroidSurface(JNIEnv* aEnv, XrSession aSession, const XrSwapchainCreateInfo& aInfo) { + Destroy(); + info = aInfo; + env = aEnv; + session = aSession; + CHECK(aSession != XR_NULL_HANDLE); + CHECK_MSG(env, "JNIEnv must be not null"); + CHECK_XRCMD(OpenXRExtensions::sXrCreateSwapchainAndroidSurfaceKHR(aSession, &info, &swapchain, &surface)); + CHECK(surface); + CHECK(swapchain != XR_NULL_HANDLE); + surface = env->NewGlobalRef(surface); +} + +void OpenXRSwapChain::InitCubemap(vrb::RenderContextPtr &aContext, XrSession aSession, const XrSwapchainCreateInfo &aInfo) { + Destroy(); + info = aInfo; + context = aContext; + session = aSession; + // XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT is used to hint that we only acquire the image once + info.createFlags = XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT; + + CHECK(aInfo.faceCount == 6); + CHECK(aSession != XR_NULL_HANDLE); + CHECK_XRCMD(xrCreateSwapchain(aSession, &info, &swapchain)); + CHECK(swapchain != XR_NULL_HANDLE); + + // Initialize image structs + uint32_t imageCount; + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, 0, &imageCount, nullptr)); + CHECK(imageCount > 0); + imageBuffer.resize(imageCount); + for (XrSwapchainImageOpenGLESKHR& image : imageBuffer) { + image.type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR; + images.push_back(reinterpret_cast(&image)); + } + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, imageCount, &imageCount, images[0])); + + // Acquire image and get cube texture + XrSwapchainImageAcquireInfo acquireInfo{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t swapchainImageIndex = 0; + CHECK_XRCMD(xrAcquireSwapchainImage(swapchain, &acquireInfo, &swapchainImageIndex)); + CHECK(swapchainImageIndex < imageBuffer.size()); + + XrSwapchainImageWaitInfo waitInfo{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + waitInfo.timeout = XR_INFINITE_DURATION; + CHECK_XRCMD(xrWaitSwapchainImage(swapchain, &waitInfo)); + + // Assert that cubeTexture has a value + cubeTexture = imageBuffer[swapchainImageIndex].image; + CHECK(cubeTexture != 0); + + // Release image + XrSwapchainImageReleaseInfo releaseInfo{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + CHECK_XRCMD(xrReleaseSwapchainImage(swapchain, &releaseInfo)); +} + +void +OpenXRSwapChain::AcquireImage() { + CHECK_MSG(!surface, "AcquireImage must not be called for Android Surfaces"); + CHECK_MSG(!acquiredFBO, "Expected no acquired FBOs. ReleaseImage not called?"); + CHECK_MSG(!cubeTexture, "AcquireImage must not be called for cubemap textures"); + + XrSwapchainImageAcquireInfo acquireInfo{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; + uint32_t swapchainImageIndex = 0; + CHECK_XRCMD(xrAcquireSwapchainImage(swapchain, &acquireInfo, &swapchainImageIndex)); + CHECK(swapchainImageIndex < imageBuffer.size()); + CHECK(swapchainImageIndex < fbos.size()); + + XrSwapchainImageWaitInfo waitInfo{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO}; + waitInfo.timeout = XR_INFINITE_DURATION; + CHECK_XRCMD(xrWaitSwapchainImage(swapchain, &waitInfo)); + + if (!fbos[swapchainImageIndex]) { + vrb::FBOPtr fbo = vrb::FBO::Create(context); + fbos[swapchainImageIndex] = fbo; + uint32_t texture = imageBuffer[swapchainImageIndex].image; + VRB_GL_CHECK(glBindTexture(GL_TEXTURE_2D, texture)); + VRB_GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + VRB_GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + VRB_GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + VRB_GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + + VRB_GL_CHECK(fbo->SetTextureHandle(texture, info.width, info.height, attributes)); + if (!fbo->IsValid()) { + VRB_ERROR("OpenXR XrSwapchainImageOpenGLESKHR texture FBO is not valid"); + } else{ + VRB_DEBUG("OpenXR succesfully created FBO for swapChainImageIndex: %d", swapchainImageIndex); + } + } + + acquiredFBO = fbos[swapchainImageIndex]; +} + +void +OpenXRSwapChain::ReleaseImage() { + CHECK_MSG(!surface, "ReleaseImage must not be called for Android Surfaces"); + CHECK_MSG(acquiredFBO, "Expected a valid acquired FBO. AcquireImage not called?"); + CHECK_MSG(!cubeTexture, "ReleaseImage must not be called for cubemap textures"); + + XrSwapchainImageReleaseInfo releaseInfo{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; + CHECK_XRCMD(xrReleaseSwapchainImage(swapchain, &releaseInfo)); + acquiredFBO = nullptr; +} + +void +OpenXRSwapChain::BindFBO(GLenum target) { + CHECK_MSG(!surface, "BindFBO must not be called for Android Surfaces"); + CHECK_MSG(acquiredFBO, "Expected a valid acquired FBO. AcquireImage not called?"); + CHECK_MSG(!cubeTexture, "BindFBO must not be called for cubemap textures"); + acquiredFBO->Bind(target); +} + +void +OpenXRSwapChain::Destroy() { + if (acquiredFBO) { + ReleaseImage(); + } + fbos.clear(); + imageBuffer.clear(); + images.clear(); + if (swapchain != XR_NULL_HANDLE) { + xrDestroySwapchain(swapchain); + swapchain = XR_NULL_HANDLE; + } + if (surface) { + CHECK_MSG(env, "JNIEnv must be non null to release the AndroidSurface reference"); + env->DeleteGlobalRef(surface); + surface = nullptr; + } + session = XR_NULL_HANDLE; + env = nullptr; +} + +OpenXRSwapChain::~OpenXRSwapChain() { + Destroy(); +} + +} diff --git a/app/src/openxr/cpp/OpenXRSwapChain.h b/app/src/openxr/cpp/OpenXRSwapChain.h new file mode 100644 index 000000000..8613348e9 --- /dev/null +++ b/app/src/openxr/cpp/OpenXRSwapChain.h @@ -0,0 +1,54 @@ +#pragma once + +#include "vrb/Forward.h" +#include "Device.h" +#include +#include + +#include +#include "jni.h" +#include +#include +#include +#include "vrb/gl.h" + +namespace crow { + +class OpenXRSwapChain; +typedef std::shared_ptr OpenXRSwapChainPtr; + +class OpenXRSwapChain { +private: + vrb::RenderContextPtr context; + XrSwapchainCreateInfo info; + vrb::FBO::Attributes attributes; + XrSwapchain swapchain = XR_NULL_HANDLE; + std::vector imageBuffer; + std::vector images; + std::vector fbos; + vrb::FBOPtr acquiredFBO; + JNIEnv* env = nullptr; + jobject surface = nullptr; + XrSession session = XR_NULL_HANDLE; + uint32_t cubeTexture = 0; +public: + ~OpenXRSwapChain(); + + static OpenXRSwapChainPtr create(); + void InitFBO(vrb::RenderContextPtr &aContext, XrSession aSession, const XrSwapchainCreateInfo& aInfo, vrb::FBO::Attributes aAttributes); + void InitAndroidSurface(JNIEnv* aEnv, XrSession aSession, const XrSwapchainCreateInfo& aInfo); + void InitCubemap(vrb::RenderContextPtr &aContext, XrSession aSession, const XrSwapchainCreateInfo& aInfo); + void AcquireImage(); + void ReleaseImage(); + void BindFBO(GLenum target = GL_FRAMEBUFFER); + void Destroy(); + inline XrSwapchain SwapChain() const { return swapchain;} + inline int32_t Width() const { return info.width; } + inline int32_t Height() const { return info.height; } + inline jobject AndroidSurface() const { return surface; } + inline JNIEnv* Env() const { return env; }; + inline XrSession Session() const { return session; } + inline uint32_t CubemapTexture() const { return cubeTexture; } +}; + +}