diff --git a/Dependencies/xr/CMakeLists.txt b/Dependencies/xr/CMakeLists.txt index 8fb22bd29..141a5cfb9 100644 --- a/Dependencies/xr/CMakeLists.txt +++ b/Dependencies/xr/CMakeLists.txt @@ -24,15 +24,27 @@ endif() project(xr) +# Android Quest builds select OpenXR instead of ARCore via -DXR_ANDROID_BACKEND=OpenXR. +if(ANDROID) + set(XR_ANDROID_BACKEND "ARCore" CACHE STRING "Android XR backend: ARCore or OpenXR") + set_property(CACHE XR_ANDROID_BACKEND PROPERTY STRINGS ARCore OpenXR) +endif() + set(DYNAMIC_LOADER OFF CACHE BOOL "Build the loader as a .dll library") set(SOURCES "Include/XR.h" "Source/Shared/XRHelpers.h") if (ANDROID) - set(SOURCES ${SOURCES} - "Source/ARCore/Include/IXrContextARCore.h" - "Source/ARCore/XR.cpp") + if(XR_ANDROID_BACKEND STREQUAL "OpenXR") + set(SOURCES ${SOURCES} + "Source/OpenXR/Android/Include/IXrContextOpenXR.h" + "Source/OpenXR/Android/XR.cpp") + else() + set(SOURCES ${SOURCES} + "Source/ARCore/Include/IXrContextARCore.h" + "Source/ARCore/XR.cpp") + endif() elseif(IOS OR BABYLON_NATIVE_BUILD_SOURCETREE) set(SOURCES ${SOURCES} "Source/ARKit/Include/IXrContextARKit.h" @@ -49,16 +61,36 @@ if (APPLE) endif() if (ANDROID) - add_library(arcore SHARED IMPORTED) - set_target_properties(arcore PROPERTIES IMPORTED_LOCATION - ${ARCORE_LIBPATH}/${ANDROID_ABI}/libarcore_sdk_c.so - INTERFACE_INCLUDE_DIRECTORIES ${arcore-android-sdk_SOURCE_DIR}/libraries/include) - - add_library(glm INTERFACE) - set_target_properties(glm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${arcore-android-sdk_SOURCE_DIR}/libraries/glm) - - target_link_libraries(xr PRIVATE glm arcore AndroidExtensions) -elseif (IOS) + if(XR_ANDROID_BACKEND STREQUAL "OpenXR") + set(BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE) + set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE) + set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(BUILD_LOADER ON CACHE BOOL "" FORCE) + + FetchContent_Declare(OpenXR-SDK + GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git + GIT_TAG release-1.1.47) + FetchContent_MakeAvailable(OpenXR-SDK) + + target_compile_definitions(xr PRIVATE + XR_USE_PLATFORM_ANDROID + XR_USE_GRAPHICS_API_OPENGL_ES) + + target_include_directories(xr PRIVATE "Source/OpenXR/Android") + target_link_libraries(xr PRIVATE openxr_loader AndroidExtensions EGL GLESv3) + else() + add_library(arcore SHARED IMPORTED) + set_target_properties(arcore PROPERTIES IMPORTED_LOCATION + ${ARCORE_LIBPATH}/${ANDROID_ABI}/libarcore_sdk_c.so + INTERFACE_INCLUDE_DIRECTORIES ${arcore-android-sdk_SOURCE_DIR}/libraries/include) + + add_library(glm INTERFACE) + set_target_properties(glm PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${arcore-android-sdk_SOURCE_DIR}/libraries/glm) + + target_link_libraries(xr PRIVATE glm arcore AndroidExtensions) + endif() +elseif (IOS) target_link_libraries(xr PRIVATE "-framework ARKit") endif() diff --git a/Dependencies/xr/Source/OpenXR/Android/Include/IXrContextOpenXR.h b/Dependencies/xr/Source/OpenXR/Android/Include/IXrContextOpenXR.h new file mode 100644 index 000000000..32d4a3cb8 --- /dev/null +++ b/Dependencies/xr/Source/OpenXR/Android/Include/IXrContextOpenXR.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +typedef struct IXrContextOpenXR +{ + virtual bool IsInitialized() const = 0; + virtual XrInstance Instance() const = 0; + virtual XrSession Session() const = 0; + virtual ~IXrContextOpenXR() = default; +} IXrContextOpenXR; diff --git a/Dependencies/xr/Source/OpenXR/Android/XR.cpp b/Dependencies/xr/Source/OpenXR/Android/XR.cpp new file mode 100644 index 000000000..7f33c5f3b --- /dev/null +++ b/Dependencies/xr/Source/OpenXR/Android/XR.cpp @@ -0,0 +1,1707 @@ +#include +#include + +#include + +#define LOG_TAG "LoonyOpenXR" +#include + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Include/IXrContextOpenXR.h" + +using namespace android; +using namespace android::global; + +namespace +{ + template + T MakeXrStruct(XrStructureType type) + { + T value{}; + value.type = type; + return value; + } + + template + std::vector MakeXrStructVector(size_t count, XrStructureType type) + { + std::vector values(count); + for (auto& value : values) + { + value.type = type; + } + return values; + } +} + +namespace +{ + // For the XR_KHR_opengl_es_enable graphics binding, xrEnumerateSwapchainFormats + // returns raw OpenGL(ES) sized-internal-format tokens (the same enum space as + // glTexStorage2D's internalformat) -- NOT Vulkan VkFormat numeric values. Only the + // Vulkan graphics binding extension enumerates VkFormat values. + constexpr int64_t OPENXR_COLOR_FORMAT_SRGB = 0x8C43; // GL_SRGB8_ALPHA8 + constexpr int64_t OPENXR_COLOR_FORMAT_UNORM = 0x8058; // GL_RGBA8 + constexpr int64_t OPENXR_DEPTH_FORMAT_D24S8 = 0x88F0; // GL_DEPTH24_STENCIL8 + constexpr int64_t OPENXR_DEPTH_FORMAT_D32 = 0x8CAC; // GL_DEPTH_COMPONENT32F + + constexpr XrPosef IDENTITY_POSE{ + {0.f, 0.f, 0.f, 1.f}, + {0.f, 0.f, 0.f}, + }; + + constexpr std::array SUPPORTED_COLOR_FORMATS{ + OPENXR_COLOR_FORMAT_SRGB, + OPENXR_COLOR_FORMAT_UNORM, + }; + + constexpr std::array SUPPORTED_DEPTH_FORMATS{ + OPENXR_DEPTH_FORMAT_D24S8, + OPENXR_DEPTH_FORMAT_D32, + }; + + // Layout matches WebXROculusTouchMotionController (oculus-touch WebXR profile). + constexpr uint32_t kTouchControllerCount = 2; + constexpr uint32_t kTouchControllerButtonCount = 6; + constexpr uint32_t kTouchControllerAxisCount = 4; + constexpr uint32_t kTriggerButtonIndex = 0; + constexpr uint32_t kSqueezeButtonIndex = 1; + constexpr uint32_t kThumbstickClickButtonIndex = 3; + constexpr uint32_t kPrimaryButtonIndex = 4; + constexpr uint32_t kSecondaryButtonIndex = 5; + constexpr uint32_t kThumbstickXAxisIndex = 2; + constexpr uint32_t kThumbstickYAxisIndex = 3; + + constexpr const char* kOculusTouchInteractionProfile = "/interaction_profiles/oculus/touch_controller"; + constexpr const char* kHandSubactionPathLeft = "/user/hand/left"; + constexpr const char* kHandSubactionPathRight = "/user/hand/right"; + + void XrCheck(XrResult result, const char* context) + { + if (XR_FAILED(result)) + { + std::ostringstream message; + message << context << " failed with XrResult " << result; + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s", message.str().c_str()); + throw std::runtime_error(message.str()); + } + } + + xr::TextureFormat SwapchainFormatToTextureFormat(int64_t format) + { + switch (format) + { + case OPENXR_COLOR_FORMAT_SRGB: + case OPENXR_COLOR_FORMAT_UNORM: + return xr::TextureFormat::RGBA8_SRGB; + case OPENXR_DEPTH_FORMAT_D24S8: + return xr::TextureFormat::D24S8; + case OPENXR_DEPTH_FORMAT_D32: + return xr::TextureFormat::D16; + default: + return xr::TextureFormat::RGBA8_SRGB; + } + } + + void PopulateProjectionMatrix(const XrView& view, float depthNear, float depthFar, std::array& projectionMatrix) + { + const float n = depthNear; + const float f = depthFar; + + const float l = std::tanf(view.fov.angleLeft) * n; + const float r = std::tanf(view.fov.angleRight) * n; + const float t = std::tanf(view.fov.angleUp) * n; + const float b = std::tanf(view.fov.angleDown) * n; + + const float invDiffRL = 1.f / (r - l); + const float invDiffTB = 1.f / (t - b); + const float ww = 2.f * n * invDiffRL; + const float hh = 2.f * n * invDiffTB; + const float xx = (r + l) * invDiffRL; + const float yy = (t + b) * invDiffTB; + + // OpenGL ES uses a [-1, 1] depth range. + constexpr bool homogeneousDepth = true; + + const float diffFN = f - n; + const float za = -(homogeneousDepth ? (f + n) / diffFN : f / diffFN); + const float zb = -(homogeneousDepth ? (2.f * f * n) / diffFN : n * -za); + constexpr float zc = -1.f; + + projectionMatrix = { + ww, 0, 0, 0, + 0, hh, 0, 0, + xx, yy, za, zc, + 0, 0, zb, 0, + }; + } + + uint32_t AcquireAndWaitForSwapchainImage(XrSwapchain handle) + { + uint32_t swapchainImageIndex{}; + XrSwapchainImageAcquireInfo acquireInfo = MakeXrStruct(XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO); + XrCheck(xrAcquireSwapchainImage(handle, &acquireInfo, &swapchainImageIndex), "xrAcquireSwapchainImage"); + + XrSwapchainImageWaitInfo waitInfo = MakeXrStruct(XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO); + waitInfo.timeout = XR_INFINITE_DURATION; + XrCheck(xrWaitSwapchainImage(handle, &waitInfo), "xrWaitSwapchainImage"); + + return swapchainImageIndex; + } + + // android::global::GetAppContext()/GetCurrentActivity() return short-lived C++ wrapper + // objects whose destructor deletes the JNI global reference they hold. OpenXR retains + // the raw jobject beyond the lifetime of that wrapper (the loader and runtime may use it + // asynchronously), so we must promote it to a global ref we own for the life of the + // process before handing it to the OpenXR API. + jobject ToPersistentGlobalRef(jobject transient) + { + return GetEnvForCurrentThread()->NewGlobalRef(transient); + } + + void InitializeLoader() + { + PFN_xrInitializeLoaderKHR initializeLoader = nullptr; + XrCheck( + xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR", reinterpret_cast(&initializeLoader)), + "xrGetInstanceProcAddr(xrInitializeLoaderKHR)"); + + JavaVM* javaVm = nullptr; + if (GetEnvForCurrentThread()->GetJavaVM(&javaVm) != JNI_OK || javaVm == nullptr) + { + throw std::runtime_error("Failed to obtain JavaVM for OpenXR loader initialization."); + } + + XrLoaderInitInfoAndroidKHR loaderInitInfo = MakeXrStruct(XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR); + loaderInitInfo.applicationVM = javaVm; + loaderInitInfo.applicationContext = ToPersistentGlobalRef(GetAppContext()); + XrCheck(initializeLoader(reinterpret_cast(&loaderInitInfo)), "xrInitializeLoaderKHR"); + } + + std::vector SelectEnabledInstanceExtensions() + { + uint32_t extensionCount = 0; + XrCheck(xrEnumerateInstanceExtensionProperties(nullptr, 0, &extensionCount, nullptr), "xrEnumerateInstanceExtensionProperties(count)"); + std::vector extensionProperties = MakeXrStructVector(extensionCount, XR_TYPE_EXTENSION_PROPERTIES); + XrCheck( + xrEnumerateInstanceExtensionProperties(nullptr, extensionCount, &extensionCount, extensionProperties.data()), + "xrEnumerateInstanceExtensionProperties"); + + const char* desiredExtensions[] = { + XR_KHR_ANDROID_CREATE_INSTANCE_EXTENSION_NAME, + XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME, + }; + + std::vector enabledExtensions; + for (const char* desired : desiredExtensions) + { + const auto found = std::find_if( + extensionProperties.begin(), + extensionProperties.end(), + [desired](const XrExtensionProperties& properties) { + return std::strcmp(properties.extensionName, desired) == 0; + }); + if (found != extensionProperties.end()) + { + enabledExtensions.push_back(desired); + } + } + + if (enabledExtensions.size() != std::size(desiredExtensions)) + { + throw std::runtime_error("Required OpenXR instance extensions are unavailable on this device."); + } + + return enabledExtensions; + } + + int64_t SelectSwapchainFormat(XrSession session, const std::array& candidates) + { + uint32_t formatCount = 0; + XrCheck(xrEnumerateSwapchainFormats(session, 0, &formatCount, nullptr), "xrEnumerateSwapchainFormats(count)"); + std::vector runtimeFormats(formatCount); + XrCheck( + xrEnumerateSwapchainFormats(session, formatCount, &formatCount, runtimeFormats.data()), + "xrEnumerateSwapchainFormats"); + + for (const auto candidate : candidates) + { + if (std::find(runtimeFormats.begin(), runtimeFormats.end(), candidate) != runtimeFormats.end()) + { + return candidate; + } + } + + throw std::runtime_error("No compatible OpenXR swapchain format found."); + } + + XrPath StringToPath(XrInstance instance, const char* pathString) + { + XrPath path{}; + XrCheck(xrStringToPath(instance, pathString, &path), "xrStringToPath"); + return path; + } + + std::string PathToString(XrInstance instance, XrPath path) + { + if (path == XR_NULL_PATH) + { + return {}; + } + + uint32_t bufferSize = 0; + XrCheck(xrPathToString(instance, path, 0, &bufferSize, nullptr), "xrPathToString(count)"); + if (bufferSize == 0) + { + return {}; + } + + std::string pathString(bufferSize, '\0'); + XrCheck(xrPathToString(instance, path, bufferSize, &bufferSize, pathString.data()), "xrPathToString"); + if (!pathString.empty() && pathString.back() == '\0') + { + pathString.pop_back(); + } + return pathString; + } + + XrAction CreateInputAction( + XrActionSet actionSet, + const char* actionName, + XrActionType actionType, + uint32_t subactionPathCount, + const XrPath* subactionPaths) + { + XrActionCreateInfo actionCreateInfo = MakeXrStruct(XR_TYPE_ACTION_CREATE_INFO); + std::strncpy(actionCreateInfo.actionName, actionName, XR_MAX_ACTION_NAME_SIZE - 1); + std::strncpy(actionCreateInfo.localizedActionName, actionName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1); + actionCreateInfo.actionType = actionType; + actionCreateInfo.countSubactionPaths = subactionPathCount; + actionCreateInfo.subactionPaths = subactionPaths; + + XrAction action{XR_NULL_HANDLE}; + XrCheck(xrCreateAction(actionSet, &actionCreateInfo, &action), "xrCreateAction"); + return action; + } + + bool LocateActionSpacePose( + XrSpace actionSpace, + XrSpace referenceSpace, + XrTime displayTime, + xr::Pose& pose) + { + XrSpaceLocation location = MakeXrStruct(XR_TYPE_SPACE_LOCATION); + const XrResult locateResult = xrLocateSpace(actionSpace, referenceSpace, displayTime, &location); + if (XR_FAILED(locateResult)) + { + return false; + } + + const bool orientationValid = (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0; + const bool positionValid = (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0; + if (!orientationValid && !positionValid) + { + return false; + } + + if (orientationValid) + { + pose.Orientation.X = location.pose.orientation.x; + pose.Orientation.Y = location.pose.orientation.y; + pose.Orientation.Z = location.pose.orientation.z; + pose.Orientation.W = location.pose.orientation.w; + } + + if (positionValid) + { + pose.Position.X = location.pose.position.x; + pose.Position.Y = location.pose.position.y; + pose.Position.Z = location.pose.position.z; + } + + return orientationValid || positionValid; + } + + void ResetInputSourceTrackingState(xr::System::Session::Frame::InputSource& inputSource) + { + inputSource.TrackedThisFrame = false; + inputSource.GamepadTrackedThisFrame = false; + inputSource.HandTrackedThisFrame = false; + inputSource.JointsTrackedThisFrame = false; + inputSource.InteractionProfileName.clear(); + + for (auto& button : inputSource.GamepadObject.Buttons) + { + button.Pressed = false; + button.Touched = false; + button.Value = 0.f; + } + + for (auto& axis : inputSource.GamepadObject.Axes) + { + axis = 0.f; + } + } +} + +namespace xr +{ + struct XrContextOpenXR : public IXrContextOpenXR + { + XrInstance InstanceHandle{XR_NULL_HANDLE}; + XrSystemId SystemId{XR_NULL_SYSTEM_ID}; + XrSession SessionHandle{XR_NULL_HANDLE}; + XrSpace ReferenceSpace{XR_NULL_HANDLE}; + XrSessionState SessionState{XR_SESSION_STATE_UNKNOWN}; + bool SessionRunning{false}; + bool Initialized{false}; + + bool ActionSetCreated{false}; + XrActionSet ActionSet{XR_NULL_HANDLE}; + std::array HandSubactionPaths{}; + + XrAction GripPoseAction{XR_NULL_HANDLE}; + XrAction AimPoseAction{XR_NULL_HANDLE}; + XrAction SelectAction{XR_NULL_HANDLE}; + XrAction SqueezeAction{XR_NULL_HANDLE}; + XrAction SelectClickAction{XR_NULL_HANDLE}; + XrAction SqueezeClickAction{XR_NULL_HANDLE}; + XrAction ThumbstickXAction{XR_NULL_HANDLE}; + XrAction ThumbstickYAction{XR_NULL_HANDLE}; + XrAction ThumbstickClickAction{XR_NULL_HANDLE}; + XrAction PrimaryClickAction{XR_NULL_HANDLE}; + XrAction SecondaryClickAction{XR_NULL_HANDLE}; + + bool IsInitialized() const override + { + return Initialized; + } + + XrInstance Instance() const override + { + return InstanceHandle; + } + + XrSession Session() const override + { + return SessionHandle; + } + + ~XrContextOpenXR() override = default; + }; + + namespace + { + // OpenXR runtimes (including Quest's) do not support multiple concurrently-live + // XrInstance handles in one process. Babylon probes availability via the static + // System::IsSessionSupportedAsync before the "real" session is ever created, so + // every System::Impl must share a single process-wide instance/system/session + // rather than each creating its own. + std::shared_ptr& GlobalXrContext() + { + static std::shared_ptr context{std::make_shared()}; + return context; + } + } + + struct System::Impl + { + std::shared_ptr XrContext{GlobalXrContext()}; + std::string ApplicationName; + + explicit Impl(const std::string& applicationName) + : ApplicationName{applicationName.empty() ? "Loony Quest" : applicationName} + { + } + + bool IsInitialized() const + { + return XrContext->InstanceHandle != XR_NULL_HANDLE && XrContext->SystemId != XR_NULL_SYSTEM_ID; + } + + bool TryInitialize() + { + if (IsInitialized()) + { + return true; + } + + InitializeLoader(); + + const auto enabledExtensions = SelectEnabledInstanceExtensions(); + + JavaVM* javaVm = nullptr; + if (GetEnvForCurrentThread()->GetJavaVM(&javaVm) != JNI_OK || javaVm == nullptr) + { + throw std::runtime_error("Failed to obtain JavaVM for OpenXR instance creation."); + } + + XrInstanceCreateInfoAndroidKHR instanceCreateInfoAndroid = MakeXrStruct(XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR); + instanceCreateInfoAndroid.applicationVM = javaVm; + instanceCreateInfoAndroid.applicationActivity = ToPersistentGlobalRef(GetCurrentActivity()); + + XrInstanceCreateInfo createInfo = MakeXrStruct(XR_TYPE_INSTANCE_CREATE_INFO); + createInfo.next = &instanceCreateInfoAndroid; + createInfo.enabledExtensionCount = static_cast(enabledExtensions.size()); + createInfo.enabledExtensionNames = enabledExtensions.data(); + std::strncpy(createInfo.applicationInfo.applicationName, ApplicationName.c_str(), XR_MAX_APPLICATION_NAME_SIZE - 1); + createInfo.applicationInfo.applicationVersion = 1; + std::strncpy(createInfo.applicationInfo.engineName, "BabylonNative", XR_MAX_ENGINE_NAME_SIZE - 1); + createInfo.applicationInfo.engineVersion = 1; + createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + + XrCheck(xrCreateInstance(&createInfo, &XrContext->InstanceHandle), "xrCreateInstance"); + + XrSystemGetInfo systemInfo = MakeXrStruct(XR_TYPE_SYSTEM_GET_INFO); + systemInfo.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + const XrResult systemResult = xrGetSystem(XrContext->InstanceHandle, &systemInfo, &XrContext->SystemId); + if (systemResult == XR_ERROR_FORM_FACTOR_UNAVAILABLE) + { + return false; + } + XrCheck(systemResult, "xrGetSystem"); + + CreateActionSetOnce(); + + return true; + } + + void CreateActionSetOnce() + { + if (XrContext->ActionSetCreated) + { + return; + } + + XrActionSetCreateInfo actionSetCreateInfo = MakeXrStruct(XR_TYPE_ACTION_SET_CREATE_INFO); + std::strncpy(actionSetCreateInfo.actionSetName, "loony_touch", XR_MAX_ACTION_SET_NAME_SIZE - 1); + std::strncpy(actionSetCreateInfo.localizedActionSetName, "Loony Touch Controllers", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1); + actionSetCreateInfo.priority = 0; + XrCheck(xrCreateActionSet(XrContext->InstanceHandle, &actionSetCreateInfo, &XrContext->ActionSet), "xrCreateActionSet"); + + XrContext->HandSubactionPaths[0] = StringToPath(XrContext->InstanceHandle, kHandSubactionPathLeft); + XrContext->HandSubactionPaths[1] = StringToPath(XrContext->InstanceHandle, kHandSubactionPathRight); + const XrPath handSubactionPaths[] = { + XrContext->HandSubactionPaths[0], + XrContext->HandSubactionPaths[1], + }; + + XrContext->GripPoseAction = CreateInputAction( + XrContext->ActionSet, + "grip_pose", + XR_ACTION_TYPE_POSE_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->AimPoseAction = CreateInputAction( + XrContext->ActionSet, + "aim_pose", + XR_ACTION_TYPE_POSE_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->SelectAction = CreateInputAction( + XrContext->ActionSet, + "select", + XR_ACTION_TYPE_FLOAT_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->SqueezeAction = CreateInputAction( + XrContext->ActionSet, + "squeeze", + XR_ACTION_TYPE_FLOAT_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->SelectClickAction = CreateInputAction( + XrContext->ActionSet, + "select_click", + XR_ACTION_TYPE_BOOLEAN_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->SqueezeClickAction = CreateInputAction( + XrContext->ActionSet, + "squeeze_click", + XR_ACTION_TYPE_BOOLEAN_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->ThumbstickXAction = CreateInputAction( + XrContext->ActionSet, + "thumbstick_x", + XR_ACTION_TYPE_FLOAT_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->ThumbstickYAction = CreateInputAction( + XrContext->ActionSet, + "thumbstick_y", + XR_ACTION_TYPE_FLOAT_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->ThumbstickClickAction = CreateInputAction( + XrContext->ActionSet, + "thumbstick_click", + XR_ACTION_TYPE_BOOLEAN_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->PrimaryClickAction = CreateInputAction( + XrContext->ActionSet, + "primary_click", + XR_ACTION_TYPE_BOOLEAN_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + XrContext->SecondaryClickAction = CreateInputAction( + XrContext->ActionSet, + "secondary_click", + XR_ACTION_TYPE_BOOLEAN_INPUT, + static_cast(std::size(handSubactionPaths)), + handSubactionPaths); + + // XrActionSuggestedBinding.binding must be a *full* input source path rooted at + // a top-level user path (e.g. "/user/hand/left/input/trigger/value"); paths + // missing that prefix are rejected by xrSuggestInteractionProfileBindings with + // XR_ERROR_PATH_UNSUPPORTED. x/y only exist under .../left/..., a/b only under + // .../right/..., per the oculus/touch_controller interaction profile's binding + // table, so Primary/Secondary click bind asymmetrically per hand. + const std::string leftHandPath = kHandSubactionPathLeft; + const std::string rightHandPath = kHandSubactionPathRight; + const XrPath interactionProfile = StringToPath(XrContext->InstanceHandle, kOculusTouchInteractionProfile); + const XrActionSuggestedBinding suggestedBindings[] = { + {XrContext->GripPoseAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/grip/pose").c_str())}, + {XrContext->GripPoseAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/grip/pose").c_str())}, + {XrContext->AimPoseAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/aim/pose").c_str())}, + {XrContext->AimPoseAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/aim/pose").c_str())}, + {XrContext->SelectAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/trigger/value").c_str())}, + {XrContext->SelectAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/trigger/value").c_str())}, + {XrContext->SqueezeAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/squeeze/value").c_str())}, + {XrContext->SqueezeAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/squeeze/value").c_str())}, + {XrContext->SelectClickAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/trigger/value").c_str())}, + {XrContext->SelectClickAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/trigger/value").c_str())}, + {XrContext->SqueezeClickAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/squeeze/value").c_str())}, + {XrContext->SqueezeClickAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/squeeze/value").c_str())}, + {XrContext->ThumbstickXAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/thumbstick/x").c_str())}, + {XrContext->ThumbstickXAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/thumbstick/x").c_str())}, + {XrContext->ThumbstickYAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/thumbstick/y").c_str())}, + {XrContext->ThumbstickYAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/thumbstick/y").c_str())}, + {XrContext->ThumbstickClickAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/thumbstick/click").c_str())}, + {XrContext->ThumbstickClickAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/thumbstick/click").c_str())}, + {XrContext->PrimaryClickAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/x/click").c_str())}, + {XrContext->PrimaryClickAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/a/click").c_str())}, + {XrContext->SecondaryClickAction, StringToPath(XrContext->InstanceHandle, (leftHandPath + "/input/y/click").c_str())}, + {XrContext->SecondaryClickAction, StringToPath(XrContext->InstanceHandle, (rightHandPath + "/input/b/click").c_str())}, + }; + + XrInteractionProfileSuggestedBinding profileBindings = MakeXrStruct(XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING); + profileBindings.interactionProfile = interactionProfile; + profileBindings.suggestedBindings = suggestedBindings; + profileBindings.countSuggestedBindings = static_cast(std::size(suggestedBindings)); + XrCheck( + xrSuggestInteractionProfileBindings(XrContext->InstanceHandle, &profileBindings), + "xrSuggestInteractionProfileBindings"); + + XrContext->ActionSetCreated = true; + } + }; + + struct System::Session::Impl + { + struct Swapchain + { + XrSwapchain Handle{XR_NULL_HANDLE}; + int64_t Format{}; + uint32_t Width{}; + uint32_t Height{}; + uint32_t ArraySize{}; + std::vector Images{}; + }; + + System::Impl& SystemImpl; + std::shared_ptr XrContext; + std::function WindowProvider; + + EGLDisplay Display{EGL_NO_DISPLAY}; + EGLConfig Config{}; + EGLContext Context{EGL_NO_CONTEXT}; + + std::vector ActiveFrameViews{}; + std::vector InputSources{}; + std::vector FeaturePointCloud{}; + std::optional EyeTrackerSpace{}; + + std::vector CachedViews{}; + std::vector ViewConfigViews{}; + Swapchain ColorSwapchain{}; + Swapchain DepthSwapchain{}; + std::vector ProjectionLayerViews{}; + + XrEnvironmentBlendMode BlendMode{XR_ENVIRONMENT_BLEND_MODE_OPAQUE}; + XrTime PredictedDisplayTime{}; + bool ShouldRender{false}; + bool SessionEndRequested{false}; + // Tracks whether BeginFrame() actually called xrBeginFrame() for the frame in + // progress. xrEndFrame() must never be called without a matching xrBeginFrame() — + // doing so violates the OpenXR frame-loop contract and has been observed to wedge + // the Quest runtime's internal frame-pacing state machine (silent, CPU-idle hang). + bool FrameBegun{false}; + + // Babylon Native's NativeXr plugin lazily creates its bgfx per-eye framebuffers the + // *first* time it sees a given swapchain image's GL texture pointer, via an async + // chain that hops from the render thread to the JS thread (see NativeXrImpl.cpp + // BeginUpdate()/"Initialized" flag). That chain cannot possibly finish within the + // same frame that first exposes the pointer via PopulateViews() below, so + // compositing that frame's real (still being set up) swapchain image can show + // whatever stale/garbage GPU memory the runtime last had there for one or both + // eyes (observed as intermittent, right-eye-specific tearing/black frames right + // after entering XR). Suppress compositor submission for the first several frames + // of every session so every rotating swapchain image slot gets a full frame's + // worth of head start before it is ever actually displayed. + static constexpr uint32_t SWAPCHAIN_WARMUP_FRAME_COUNT{90}; + uint32_t SwapchainWarmupFramesRemaining{SWAPCHAIN_WARMUP_FRAME_COUNT}; + + std::array GripSpaces{}; + std::array AimSpaces{}; + bool InputSourcesInitialized{false}; + bool ActionSpacesCreated{false}; + + float DepthNearZ{DEFAULT_DEPTH_NEAR_Z}; + float DepthFarZ{DEFAULT_DEPTH_FAR_Z}; + + Impl(System::Impl& systemImpl, void* graphicsContext, std::function windowProvider) + : SystemImpl{systemImpl} + , XrContext{systemImpl.XrContext} + , WindowProvider{std::move(windowProvider)} + , Context{reinterpret_cast(graphicsContext)} + { + } + + ~Impl() + { + DestroySession(); + } + + void DestroySession() + { + if (!XrContext->Initialized) + { + return; + } + + ColorSwapchain = {}; + DepthSwapchain = {}; + + DestroyActionSpaces(); + + if (XrContext->ReferenceSpace != XR_NULL_HANDLE) + { + xrDestroySpace(XrContext->ReferenceSpace); + XrContext->ReferenceSpace = XR_NULL_HANDLE; + } + + if (XrContext->SessionHandle != XR_NULL_HANDLE) + { + if (XrContext->SessionRunning) + { + xrEndSession(XrContext->SessionHandle); + XrContext->SessionRunning = false; + } + xrDestroySession(XrContext->SessionHandle); + XrContext->SessionHandle = XR_NULL_HANDLE; + } + + XrContext->Initialized = false; + } + + void Initialize() + { + if (XrContext->Initialized) + { + return; + } + + if (!SystemImpl.IsInitialized() && !SystemImpl.TryInitialize()) + { + throw std::runtime_error("OpenXR system is not initialized."); + } + + InitializeEglFromCurrentContext(); + CreateSession(); + SystemImpl.CreateActionSetOnce(); + CreateActionSpacesAndAttach(); + InitializeInputSources(); + CreateReferenceSpace(); + InitializeViewConfiguration(); + CreateSwapchains(); + + XrContext->Initialized = true; + } + + void InitializeEglFromCurrentContext() + { + Display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (Display == EGL_NO_DISPLAY) + { + throw std::runtime_error("No default EGL display."); + } + + EGLSurface currentDrawSurface = eglGetCurrentSurface(EGL_DRAW); + if (currentDrawSurface == EGL_NO_SURFACE) + { + throw std::runtime_error("No current EGL draw surface."); + } + + EGLint configId{}; + if (!eglQuerySurface(Display, currentDrawSurface, EGL_CONFIG_ID, &configId)) + { + throw std::runtime_error("Failed to query EGL surface config id."); + } + + EGLint numConfigs{}; + if (!eglGetConfigs(Display, nullptr, 0, &numConfigs)) + { + throw std::runtime_error("Failed to enumerate EGL configs."); + } + + std::vector configs(static_cast(numConfigs)); + if (!eglGetConfigs(Display, configs.data(), numConfigs, &numConfigs)) + { + throw std::runtime_error("Failed to get EGL configs."); + } + + const auto configIt = std::find_if(configs.begin(), configs.end(), [&](const EGLConfig& candidate) { + EGLint id{}; + if (!eglGetConfigAttrib(Display, candidate, EGL_CONFIG_ID, &id)) + { + return false; + } + return id == configId; + }); + + if (configIt == configs.end()) + { + throw std::runtime_error("EGL config not found for current surface."); + } + + Config = *configIt; + if (Context == EGL_NO_CONTEXT) + { + Context = eglGetCurrentContext(); + } + } + + void CreateSession() + { + PFN_xrGetOpenGLESGraphicsRequirementsKHR getOpenGLESGraphicsRequirements = nullptr; + XrCheck( + xrGetInstanceProcAddr( + XrContext->InstanceHandle, + "xrGetOpenGLESGraphicsRequirementsKHR", + reinterpret_cast(&getOpenGLESGraphicsRequirements)), + "xrGetInstanceProcAddr(xrGetOpenGLESGraphicsRequirementsKHR)"); + + XrGraphicsRequirementsOpenGLESKHR graphicsRequirements = MakeXrStruct(XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR); + XrCheck( + getOpenGLESGraphicsRequirements(XrContext->InstanceHandle, XrContext->SystemId, &graphicsRequirements), + "xrGetOpenGLESGraphicsRequirementsKHR"); + + XrGraphicsBindingOpenGLESAndroidKHR graphicsBinding = MakeXrStruct(XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR); + graphicsBinding.display = Display; + graphicsBinding.config = Config; + graphicsBinding.context = Context; + + XrSessionCreateInfo sessionCreateInfo = MakeXrStruct(XR_TYPE_SESSION_CREATE_INFO); + sessionCreateInfo.next = &graphicsBinding; + sessionCreateInfo.systemId = XrContext->SystemId; + XrCheck(xrCreateSession(XrContext->InstanceHandle, &sessionCreateInfo, &XrContext->SessionHandle), "xrCreateSession"); + } + + void InitializeInputSources() + { + if (InputSourcesInitialized) + { + return; + } + + InputSources.clear(); + InputSources.emplace_back(); + InputSources.back().Handedness = Frame::InputSource::HandednessEnum::Left; + InputSources.back().GamepadObject.Buttons.assign(kTouchControllerButtonCount, {}); + InputSources.back().GamepadObject.Axes.assign(kTouchControllerAxisCount, 0.f); + + InputSources.emplace_back(); + InputSources.back().Handedness = Frame::InputSource::HandednessEnum::Right; + InputSources.back().GamepadObject.Buttons.assign(kTouchControllerButtonCount, {}); + InputSources.back().GamepadObject.Axes.assign(kTouchControllerAxisCount, 0.f); + + InputSourcesInitialized = true; + } + + void CreateActionSpacesAndAttach() + { + if (ActionSpacesCreated || !XrContext->ActionSetCreated) + { + return; + } + + for (uint32_t handIndex = 0; handIndex < kTouchControllerCount; ++handIndex) + { + const XrPath subactionPath = XrContext->HandSubactionPaths[handIndex]; + + XrActionSpaceCreateInfo gripSpaceCreateInfo = MakeXrStruct(XR_TYPE_ACTION_SPACE_CREATE_INFO); + gripSpaceCreateInfo.action = XrContext->GripPoseAction; + gripSpaceCreateInfo.subactionPath = subactionPath; + gripSpaceCreateInfo.poseInActionSpace = IDENTITY_POSE; + XrCheck( + xrCreateActionSpace(XrContext->SessionHandle, &gripSpaceCreateInfo, &GripSpaces[handIndex]), + "xrCreateActionSpace(grip)"); + + XrActionSpaceCreateInfo aimSpaceCreateInfo = MakeXrStruct(XR_TYPE_ACTION_SPACE_CREATE_INFO); + aimSpaceCreateInfo.action = XrContext->AimPoseAction; + aimSpaceCreateInfo.subactionPath = subactionPath; + aimSpaceCreateInfo.poseInActionSpace = IDENTITY_POSE; + XrCheck( + xrCreateActionSpace(XrContext->SessionHandle, &aimSpaceCreateInfo, &AimSpaces[handIndex]), + "xrCreateActionSpace(aim)"); + } + + XrSessionActionSetsAttachInfo attachInfo = MakeXrStruct(XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO); + attachInfo.countActionSets = 1; + attachInfo.actionSets = &XrContext->ActionSet; + XrCheck(xrAttachSessionActionSets(XrContext->SessionHandle, &attachInfo), "xrAttachSessionActionSets"); + + ActionSpacesCreated = true; + } + + void DestroyActionSpaces() + { + for (auto& gripSpace : GripSpaces) + { + if (gripSpace != XR_NULL_HANDLE) + { + xrDestroySpace(gripSpace); + gripSpace = XR_NULL_HANDLE; + } + } + + for (auto& aimSpace : AimSpaces) + { + if (aimSpace != XR_NULL_HANDLE) + { + xrDestroySpace(aimSpace); + aimSpace = XR_NULL_HANDLE; + } + } + + ActionSpacesCreated = false; + } + + bool GetActionStateBoolean(XrAction action, XrPath subactionPath, bool& outValue) const + { + XrActionStateGetInfo getInfo = MakeXrStruct(XR_TYPE_ACTION_STATE_GET_INFO); + getInfo.action = action; + getInfo.subactionPath = subactionPath; + + XrActionStateBoolean state = MakeXrStruct(XR_TYPE_ACTION_STATE_BOOLEAN); + const XrResult result = xrGetActionStateBoolean(XrContext->SessionHandle, &getInfo, &state); + if (XR_FAILED(result)) + { + return false; + } + + outValue = state.isActive && state.currentState; + return state.isActive; + } + + bool GetActionStateFloat(XrAction action, XrPath subactionPath, float& outValue) const + { + XrActionStateGetInfo getInfo = MakeXrStruct(XR_TYPE_ACTION_STATE_GET_INFO); + getInfo.action = action; + getInfo.subactionPath = subactionPath; + + XrActionStateFloat state = MakeXrStruct(XR_TYPE_ACTION_STATE_FLOAT); + const XrResult result = xrGetActionStateFloat(XrContext->SessionHandle, &getInfo, &state); + if (XR_FAILED(result)) + { + return false; + } + + outValue = state.isActive ? state.currentState : 0.f; + return state.isActive; + } + + void SyncActionsAndUpdateInputSources() + { + if (!XrContext->SessionRunning || !XrContext->ActionSetCreated || !ActionSpacesCreated) + { + for (auto& inputSource : InputSources) + { + ResetInputSourceTrackingState(inputSource); + } + return; + } + + XrActiveActionSet activeActionSet{}; + activeActionSet.actionSet = XrContext->ActionSet; + activeActionSet.subactionPath = XR_NULL_PATH; + + XrActionsSyncInfo syncInfo = MakeXrStruct(XR_TYPE_ACTIONS_SYNC_INFO); + syncInfo.countActiveActionSets = 1; + syncInfo.activeActionSets = &activeActionSet; + + const XrResult syncResult = xrSyncActions(XrContext->SessionHandle, &syncInfo); + if (syncResult == XR_SESSION_NOT_FOCUSED) + { + for (auto& inputSource : InputSources) + { + ResetInputSourceTrackingState(inputSource); + } + return; + } + XrCheck(syncResult, "xrSyncActions"); + + for (uint32_t handIndex = 0; handIndex < kTouchControllerCount; ++handIndex) + { + auto& inputSource = InputSources[handIndex]; + ResetInputSourceTrackingState(inputSource); + + const XrPath subactionPath = XrContext->HandSubactionPaths[handIndex]; + + XrActionStateGetInfo gripPoseGetInfo = MakeXrStruct(XR_TYPE_ACTION_STATE_GET_INFO); + gripPoseGetInfo.action = XrContext->GripPoseAction; + gripPoseGetInfo.subactionPath = subactionPath; + XrActionStatePose gripPoseState = MakeXrStruct(XR_TYPE_ACTION_STATE_POSE); + XrCheck(xrGetActionStatePose(XrContext->SessionHandle, &gripPoseGetInfo, &gripPoseState), "xrGetActionStatePose(grip)"); + + XrActionStateGetInfo aimPoseGetInfo = MakeXrStruct(XR_TYPE_ACTION_STATE_GET_INFO); + aimPoseGetInfo.action = XrContext->AimPoseAction; + aimPoseGetInfo.subactionPath = subactionPath; + XrActionStatePose aimPoseState = MakeXrStruct(XR_TYPE_ACTION_STATE_POSE); + XrCheck(xrGetActionStatePose(XrContext->SessionHandle, &aimPoseGetInfo, &aimPoseState), "xrGetActionStatePose(aim)"); + + const bool gripTracked = gripPoseState.isActive; + const bool aimTracked = aimPoseState.isActive; + const bool controllerTracked = gripTracked || aimTracked; + + if (gripTracked) + { + LocateActionSpacePose( + GripSpaces[handIndex], + XrContext->ReferenceSpace, + PredictedDisplayTime, + inputSource.GripSpace.Pose); + } + + if (aimTracked) + { + LocateActionSpacePose( + AimSpaces[handIndex], + XrContext->ReferenceSpace, + PredictedDisplayTime, + inputSource.AimSpace.Pose); + } + + float triggerValue = 0.f; + float squeezeValue = 0.f; + bool triggerPressed = false; + bool squeezePressed = false; + bool thumbstickClicked = false; + bool primaryPressed = false; + bool secondaryPressed = false; + float thumbstickX = 0.f; + float thumbstickY = 0.f; + + GetActionStateFloat(XrContext->SelectAction, subactionPath, triggerValue); + GetActionStateFloat(XrContext->SqueezeAction, subactionPath, squeezeValue); + GetActionStateBoolean(XrContext->SelectClickAction, subactionPath, triggerPressed); + GetActionStateBoolean(XrContext->SqueezeClickAction, subactionPath, squeezePressed); + GetActionStateBoolean(XrContext->ThumbstickClickAction, subactionPath, thumbstickClicked); + GetActionStateBoolean(XrContext->PrimaryClickAction, subactionPath, primaryPressed); + GetActionStateBoolean(XrContext->SecondaryClickAction, subactionPath, secondaryPressed); + GetActionStateFloat(XrContext->ThumbstickXAction, subactionPath, thumbstickX); + GetActionStateFloat(XrContext->ThumbstickYAction, subactionPath, thumbstickY); + + inputSource.GamepadObject.Buttons[kTriggerButtonIndex].Pressed = triggerPressed; + inputSource.GamepadObject.Buttons[kTriggerButtonIndex].Touched = triggerPressed || triggerValue > 0.f; + inputSource.GamepadObject.Buttons[kTriggerButtonIndex].Value = triggerValue; + + inputSource.GamepadObject.Buttons[kSqueezeButtonIndex].Pressed = squeezePressed; + inputSource.GamepadObject.Buttons[kSqueezeButtonIndex].Touched = squeezePressed || squeezeValue > 0.f; + inputSource.GamepadObject.Buttons[kSqueezeButtonIndex].Value = squeezeValue; + + inputSource.GamepadObject.Buttons[kThumbstickClickButtonIndex].Pressed = thumbstickClicked; + inputSource.GamepadObject.Buttons[kThumbstickClickButtonIndex].Touched = thumbstickClicked; + inputSource.GamepadObject.Buttons[kThumbstickClickButtonIndex].Value = thumbstickClicked ? 1.f : 0.f; + + inputSource.GamepadObject.Buttons[kPrimaryButtonIndex].Pressed = primaryPressed; + inputSource.GamepadObject.Buttons[kPrimaryButtonIndex].Touched = primaryPressed; + inputSource.GamepadObject.Buttons[kPrimaryButtonIndex].Value = primaryPressed ? 1.f : 0.f; + + inputSource.GamepadObject.Buttons[kSecondaryButtonIndex].Pressed = secondaryPressed; + inputSource.GamepadObject.Buttons[kSecondaryButtonIndex].Touched = secondaryPressed; + inputSource.GamepadObject.Buttons[kSecondaryButtonIndex].Value = secondaryPressed ? 1.f : 0.f; + + inputSource.GamepadObject.Axes[kThumbstickXAxisIndex] = thumbstickX; + inputSource.GamepadObject.Axes[kThumbstickYAxisIndex] = thumbstickY; + + XrInteractionProfileState profileState = MakeXrStruct(XR_TYPE_INTERACTION_PROFILE_STATE); + if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(XrContext->SessionHandle, subactionPath, &profileState))) + { + inputSource.InteractionProfileName = PathToString(XrContext->InstanceHandle, profileState.interactionProfile); + } + + inputSource.TrackedThisFrame = controllerTracked; + inputSource.GamepadTrackedThisFrame = controllerTracked; + } + } + + void CreateReferenceSpace() + { + XrReferenceSpaceCreateInfo referenceSpaceCreateInfo = MakeXrStruct(XR_TYPE_REFERENCE_SPACE_CREATE_INFO); + referenceSpaceCreateInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + referenceSpaceCreateInfo.poseInReferenceSpace = IDENTITY_POSE; + XrCheck( + xrCreateReferenceSpace(XrContext->SessionHandle, &referenceSpaceCreateInfo, &XrContext->ReferenceSpace), + "xrCreateReferenceSpace"); + } + + void InitializeViewConfiguration() + { + uint32_t viewCount = 0; + XrCheck( + xrEnumerateViewConfigurationViews( + XrContext->InstanceHandle, + XrContext->SystemId, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + 0, + &viewCount, + nullptr), + "xrEnumerateViewConfigurationViews(count)"); + ViewConfigViews = MakeXrStructVector(viewCount, XR_TYPE_VIEW_CONFIGURATION_VIEW); + XrCheck( + xrEnumerateViewConfigurationViews( + XrContext->InstanceHandle, + XrContext->SystemId, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + viewCount, + &viewCount, + ViewConfigViews.data()), + "xrEnumerateViewConfigurationViews"); + + CachedViews = MakeXrStructVector(viewCount, XR_TYPE_VIEW); + ProjectionLayerViews = MakeXrStructVector(viewCount, XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW); + ActiveFrameViews.assign(viewCount, {}); + + uint32_t blendModeCount = 0; + XrCheck( + xrEnumerateEnvironmentBlendModes( + XrContext->InstanceHandle, + XrContext->SystemId, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + 0, + &blendModeCount, + nullptr), + "xrEnumerateEnvironmentBlendModes(count)"); + std::vector blendModes(blendModeCount); + XrCheck( + xrEnumerateEnvironmentBlendModes( + XrContext->InstanceHandle, + XrContext->SystemId, + XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + blendModeCount, + &blendModeCount, + blendModes.data()), + "xrEnumerateEnvironmentBlendModes"); + BlendMode = blendModes.empty() ? XR_ENVIRONMENT_BLEND_MODE_OPAQUE : blendModes.front(); + } + + void CreateSwapchain( + int64_t format, + uint32_t width, + uint32_t height, + uint32_t arraySize, + XrSwapchainUsageFlags usageFlags, + Swapchain& swapchain) + { + swapchain.Format = format; + swapchain.Width = width; + swapchain.Height = height; + swapchain.ArraySize = arraySize; + + XrSwapchainCreateInfo swapchainCreateInfo = MakeXrStruct(XR_TYPE_SWAPCHAIN_CREATE_INFO); + swapchainCreateInfo.usageFlags = usageFlags; + swapchainCreateInfo.format = format; + swapchainCreateInfo.sampleCount = 1; + swapchainCreateInfo.width = width; + swapchainCreateInfo.height = height; + swapchainCreateInfo.faceCount = 1; + swapchainCreateInfo.arraySize = arraySize; + swapchainCreateInfo.mipCount = 1; + XrCheck(xrCreateSwapchain(XrContext->SessionHandle, &swapchainCreateInfo, &swapchain.Handle), "xrCreateSwapchain"); + + uint32_t imageCount = 0; + XrCheck(xrEnumerateSwapchainImages(swapchain.Handle, 0, &imageCount, nullptr), "xrEnumerateSwapchainImages(count)"); + swapchain.Images = MakeXrStructVector(imageCount, XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR); + XrCheck( + xrEnumerateSwapchainImages( + swapchain.Handle, + imageCount, + &imageCount, + reinterpret_cast(swapchain.Images.data())), + "xrEnumerateSwapchainImages"); + } + + void CreateSwapchains() + { + const auto& viewConfig = ViewConfigViews.front(); + const uint32_t viewCount = static_cast(CachedViews.size()); + const int64_t colorFormat = SelectSwapchainFormat(XrContext->SessionHandle, SUPPORTED_COLOR_FORMATS); + const int64_t depthFormat = SelectSwapchainFormat(XrContext->SessionHandle, SUPPORTED_DEPTH_FORMATS); + + CreateSwapchain( + colorFormat, + viewConfig.recommendedImageRectWidth, + viewConfig.recommendedImageRectHeight, + viewCount, + XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT, + ColorSwapchain); + CreateSwapchain( + depthFormat, + viewConfig.recommendedImageRectWidth, + viewConfig.recommendedImageRectHeight, + viewCount, + XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, + DepthSwapchain); + } + + void ProcessSessionStateChanged(const XrEventDataSessionStateChanged& stateChanged) + { + XrContext->SessionState = stateChanged.state; + switch (stateChanged.state) + { + case XR_SESSION_STATE_READY: + { + XrSessionBeginInfo beginInfo = MakeXrStruct(XR_TYPE_SESSION_BEGIN_INFO); + beginInfo.primaryViewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + XrCheck(xrBeginSession(XrContext->SessionHandle, &beginInfo), "xrBeginSession"); + XrContext->SessionRunning = true; + break; + } + case XR_SESSION_STATE_STOPPING: + XrCheck(xrEndSession(XrContext->SessionHandle), "xrEndSession"); + XrContext->SessionRunning = false; + break; + default: + break; + } + } + + void PollEvents(bool& shouldEndSession, bool& shouldRestartSession) + { + shouldEndSession = false; + shouldRestartSession = false; + + XrEventDataBuffer eventData = MakeXrStruct(XR_TYPE_EVENT_DATA_BUFFER); + while (true) + { + const XrResult pollResult = xrPollEvent(XrContext->InstanceHandle, &eventData); + if (pollResult == XR_EVENT_UNAVAILABLE) + { + break; + } + XrCheck(pollResult, "xrPollEvent"); + + switch (eventData.type) + { + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: + shouldEndSession = true; + shouldRestartSession = false; + return; + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: + { + const auto& stateChanged = *reinterpret_cast(&eventData); + if (stateChanged.session == XrContext->SessionHandle) + { + ProcessSessionStateChanged(stateChanged); + if (stateChanged.state == XR_SESSION_STATE_EXITING) + { + shouldEndSession = true; + shouldRestartSession = false; + } + else if (stateChanged.state == XR_SESSION_STATE_LOSS_PENDING) + { + shouldEndSession = true; + shouldRestartSession = true; + } + } + break; + } + default: + break; + } + + eventData = MakeXrStruct(XR_TYPE_EVENT_DATA_BUFFER); + } + + if (SessionEndRequested) + { + xrRequestExitSession(XrContext->SessionHandle); + SessionEndRequested = false; + } + } + + void LocateViews() + { + XrViewState viewState = MakeXrStruct(XR_TYPE_VIEW_STATE); + XrViewLocateInfo locateInfo = MakeXrStruct(XR_TYPE_VIEW_LOCATE_INFO); + locateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + locateInfo.displayTime = PredictedDisplayTime; + locateInfo.space = XrContext->ReferenceSpace; + + uint32_t viewCount = 0; + XrCheck( + xrLocateViews( + XrContext->SessionHandle, + &locateInfo, + &viewState, + static_cast(CachedViews.size()), + &viewCount, + CachedViews.data()), + "xrLocateViews"); + assert(viewCount == CachedViews.size()); + + // Tracking isn't guaranteed to be valid yet (e.g. the first frame or two after + // xrBeginSession), and the runtime is not required to overwrite pose fields when + // it isn't: submitting the resulting {0,0,0,0} orientation (not a unit quaternion) + // to xrEndFrame is rejected with XR_ERROR_LAYER_INVALID. Fall back to identity. + const bool orientationValid = (viewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0; + const bool positionValid = (viewState.viewStateFlags & XR_VIEW_STATE_POSITION_VALID_BIT) != 0; + if (!orientationValid || !positionValid) + { + for (auto& view : CachedViews) + { + if (!orientationValid) + { + view.pose.orientation = IDENTITY_POSE.orientation; + } + if (!positionValid) + { + view.pose.position = IDENTITY_POSE.position; + } + } + } + } + + void PopulateViews() + { + const uint32_t colorImageIndex = AcquireAndWaitForSwapchainImage(ColorSwapchain.Handle); + const uint32_t depthImageIndex = AcquireAndWaitForSwapchainImage(DepthSwapchain.Handle); + const XrRect2Di imageRect{{0, 0}, {static_cast(ColorSwapchain.Width), static_cast(ColorSwapchain.Height)}}; + + for (uint32_t viewIndex = 0; viewIndex < CachedViews.size(); ++viewIndex) + { + auto& view = ActiveFrameViews[viewIndex]; + const auto& cachedView = CachedViews[viewIndex]; + + view.Space.Pose.Position.X = cachedView.pose.position.x; + view.Space.Pose.Position.Y = cachedView.pose.position.y; + view.Space.Pose.Position.Z = cachedView.pose.position.z; + view.Space.Pose.Orientation.X = cachedView.pose.orientation.x; + view.Space.Pose.Orientation.Y = cachedView.pose.orientation.y; + view.Space.Pose.Orientation.Z = cachedView.pose.orientation.z; + view.Space.Pose.Orientation.W = cachedView.pose.orientation.w; + + view.ColorTextureFormat = SwapchainFormatToTextureFormat(ColorSwapchain.Format); + view.ColorTexturePointer = reinterpret_cast(static_cast(ColorSwapchain.Images[colorImageIndex].image)); + view.ColorTextureSize = {ColorSwapchain.Width, ColorSwapchain.Height, ColorSwapchain.ArraySize}; + + view.DepthTextureFormat = SwapchainFormatToTextureFormat(DepthSwapchain.Format); + view.DepthTexturePointer = reinterpret_cast(static_cast(DepthSwapchain.Images[depthImageIndex].image)); + view.DepthTextureSize = {DepthSwapchain.Width, DepthSwapchain.Height, DepthSwapchain.ArraySize}; + + view.DepthNearZ = DepthNearZ; + view.DepthFarZ = DepthFarZ; + view.RequiresAppClear = true; + view.IsFirstPersonObserver = false; + + PopulateProjectionMatrix(cachedView, DepthNearZ, DepthFarZ, view.ProjectionMatrix); + + auto& projectionLayerView = ProjectionLayerViews[viewIndex]; + projectionLayerView = MakeXrStruct(XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW); + projectionLayerView.pose = cachedView.pose; + projectionLayerView.fov = cachedView.fov; + projectionLayerView.subImage.swapchain = ColorSwapchain.Handle; + projectionLayerView.subImage.imageRect = imageRect; + projectionLayerView.subImage.imageArrayIndex = viewIndex; + } + } + + void BeginFrame() + { + FrameBegun = false; + + if (!XrContext->SessionRunning) + { + ShouldRender = false; + return; + } + + XrFrameWaitInfo waitInfo = MakeXrStruct(XR_TYPE_FRAME_WAIT_INFO); + XrFrameState frameState = MakeXrStruct(XR_TYPE_FRAME_STATE); + XrCheck(xrWaitFrame(XrContext->SessionHandle, &waitInfo, &frameState), "xrWaitFrame"); + ShouldRender = frameState.shouldRender; + PredictedDisplayTime = frameState.predictedDisplayTime; + + SyncActionsAndUpdateInputSources(); + + XrFrameBeginInfo beginInfo = MakeXrStruct(XR_TYPE_FRAME_BEGIN_INFO); + XrCheck(xrBeginFrame(XrContext->SessionHandle, &beginInfo), "xrBeginFrame"); + FrameBegun = true; + + if (ShouldRender && XrContext->SessionRunning) + { + LocateViews(); + PopulateViews(); + } + } + + void EndFrame() + { + // xrEndFrame() must be paired 1:1 with a preceding xrBeginFrame(); calling it + // when BeginFrame() bailed out early (session not yet running) is an OpenXR + // contract violation that can hang the Quest runtime's frame-pacing state. + if (!FrameBegun) + { + return; + } + FrameBegun = false; + + XrFrameEndInfo endInfo = MakeXrStruct(XR_TYPE_FRAME_END_INFO); + endInfo.displayTime = PredictedDisplayTime; + endInfo.environmentBlendMode = BlendMode; + + XrCompositionLayerProjection layer = MakeXrStruct(XR_TYPE_COMPOSITION_LAYER_PROJECTION); + const XrCompositionLayerBaseHeader* layers[1]{}; + + if (ShouldRender && XrContext->SessionRunning) + { + // The app has finished rendering into the images PopulateViews() acquired + // via xrAcquireSwapchainImage/xrWaitSwapchainImage; they must be released + // before being referenced by a composition layer submitted to xrEndFrame, + // or the runtime rejects the layer as invalid. Images must always be + // released once acquired regardless of the warm-up gate below. + XrSwapchainImageReleaseInfo releaseInfo = MakeXrStruct(XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO); + XrCheck(xrReleaseSwapchainImage(ColorSwapchain.Handle, &releaseInfo), "xrReleaseSwapchainImage(color)"); + XrCheck(xrReleaseSwapchainImage(DepthSwapchain.Handle, &releaseInfo), "xrReleaseSwapchainImage(depth)"); + + if (SwapchainWarmupFramesRemaining > 0) + { + // See SwapchainWarmupFramesRemaining's declaration: don't composite this + // frame yet, just let the runtime keep showing whatever it last had + // (typically the loading/pre-XR view) while NativeXr's async per-eye + // framebuffer creation catches up for this swapchain image slot. + --SwapchainWarmupFramesRemaining; + endInfo.layerCount = 0; + endInfo.layers = nullptr; + } + else + { + layer.space = XrContext->ReferenceSpace; + layer.viewCount = static_cast(ProjectionLayerViews.size()); + layer.views = ProjectionLayerViews.data(); + layers[0] = reinterpret_cast(&layer); + endInfo.layerCount = 1; + endInfo.layers = layers; + } + } + else + { + endInfo.layerCount = 0; + endInfo.layers = nullptr; + } + + XrCheck(xrEndFrame(XrContext->SessionHandle, &endInfo), "xrEndFrame"); + } + + std::unique_ptr GetNextFrame( + bool& shouldEndSession, + bool& shouldRestartSession, + std::function(void*)> /*deletedTextureAsyncCallback*/) + { + if (!XrContext->Initialized) + { + Initialize(); + } + + // Pump events until the session reaches a runnable state (or exits). + for (int attempt = 0; attempt < 100 && !XrContext->SessionRunning; ++attempt) + { + bool ignoredEnd = false; + bool ignoredRestart = false; + PollEvents(ignoredEnd, ignoredRestart); + if (ignoredEnd) + { + shouldEndSession = true; + shouldRestartSession = ignoredRestart; + return nullptr; + } + if (XrContext->SessionRunning) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + } + + PollEvents(shouldEndSession, shouldRestartSession); + if (shouldEndSession) + { + return nullptr; + } + + BeginFrame(); + return std::make_unique(*this); + } + + void RequestEndSession() + { + SessionEndRequested = true; + } + + void ClearSwapchainViews(const float color[4]) const + { + if (!ShouldRender || !XrContext->SessionRunning) + { + return; + } + + for (uint32_t viewIndex = 0; viewIndex < ActiveFrameViews.size(); ++viewIndex) + { + const auto& view = ActiveFrameViews[viewIndex]; + const auto colorTexture = static_cast(reinterpret_cast(view.ColorTexturePointer)); + const auto depthTexture = static_cast(reinterpret_cast(view.DepthTexturePointer)); + + GLuint framebuffer{}; + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glFramebufferTextureLayer( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + colorTexture, + 0, + static_cast(viewIndex)); + glFramebufferTextureLayer( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + depthTexture, + 0, + static_cast(viewIndex)); + + glViewport(0, 0, static_cast(view.ColorTextureSize.Width), static_cast(view.ColorTextureSize.Height)); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glClearColor(color[0], color[1], color[2], color[3]); + glClearDepthf(1.f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &framebuffer); + } + } + }; + + struct System::Session::Frame::Impl + { + Session::Impl& SessionImpl; + + explicit Impl(Session::Impl& sessionImpl) + : SessionImpl{sessionImpl} + { + } + }; + + System::Session::Frame::Frame(Session::Impl& sessionImpl) + : Views{sessionImpl.ActiveFrameViews} + , InputSources{sessionImpl.InputSources} + , FeaturePointCloud{sessionImpl.FeaturePointCloud} + , EyeTrackerSpace{sessionImpl.EyeTrackerSpace} + , UpdatedSceneObjects{} + , RemovedSceneObjects{} + , UpdatedPlanes{} + , RemovedPlanes{} + , UpdatedMeshes{} + , RemovedMeshes{} + , UpdatedImageTrackingResults{} + , IsTracking{sessionImpl.XrContext->SessionRunning} + , m_impl{std::make_unique(sessionImpl)} + { + } + + System::Session::Frame::~Frame() {} + + void System::Session::Frame::GetHitTestResults(std::vector&, Ray, HitTestTrackableType) const {} + + Anchor System::Session::Frame::CreateAnchor(Pose, NativeTrackablePtr) const + { + throw std::runtime_error("Anchors are not supported on the Android OpenXR backend yet."); + } + + Anchor System::Session::Frame::DeclareAnchor(NativeAnchorPtr) const + { + throw std::runtime_error("Anchors are not supported on the Android OpenXR backend yet."); + } + + void System::Session::Frame::UpdateAnchor(Anchor&) const {} + + void System::Session::Frame::DeleteAnchor(Anchor&) const {} + + System::Session::Frame::SceneObject& System::Session::Frame::GetSceneObjectByID(System::Session::Frame::SceneObject::Identifier) const + { + throw std::runtime_error("Scene object detection is not supported on the Android OpenXR backend."); + } + + System::Session::Frame::Plane& System::Session::Frame::GetPlaneByID(System::Session::Frame::Plane::Identifier) const + { + throw std::runtime_error("Plane detection is not supported on the Android OpenXR backend."); + } + + System::Session::Frame::Mesh& System::Session::Frame::GetMeshByID(System::Session::Frame::Mesh::Identifier) const + { + throw std::runtime_error("Mesh detection is not supported on the Android OpenXR backend."); + } + + System::Session::Frame::ImageTrackingResult& System::Session::Frame::GetImageTrackingResultByID( + System::Session::Frame::ImageTrackingResult::Identifier) const + { + throw std::runtime_error("Image tracking is not supported on the Android OpenXR backend."); + } + + void System::Session::Frame::Render() + { + m_impl->SessionImpl.EndFrame(); + } + + System::System(const char* appName) + : m_impl{std::make_unique(appName ? appName : "")} + { + } + + System::~System() = default; + + bool System::IsInitialized() const + { + return m_impl->IsInitialized(); + } + + bool System::TryInitialize() + { + return m_impl->TryInitialize(); + } + + arcana::task System::IsSessionSupportedAsync(SessionType sessionType) + { + if (sessionType != SessionType::IMMERSIVE_VR) + { + return arcana::task_from_result(false); + } + + try + { + System probe("Loony Quest"); + return arcana::task_from_result(probe.TryInitialize()); + } + catch (...) + { + return arcana::task_from_result(false); + } + } + + uintptr_t System::GetNativeXrContext() + { + return reinterpret_cast(m_impl->XrContext.get()); + } + + std::string System::GetNativeXrContextType() + { + return "OpenXR"; + } + + arcana::task, std::exception_ptr> System::Session::CreateAsync( + System& system, + void* graphicsDevice, + void* commandQueue, + std::function windowProvider) + { + (void)commandQueue; + return arcana::task_from_result( + std::make_shared(system, graphicsDevice, commandQueue, std::move(windowProvider))); + } + + System::Session::Session(System& system, void* graphicsDevice, void*, std::function windowProvider) + : m_impl{std::make_unique(*system.m_impl, graphicsDevice, std::move(windowProvider))} + { + } + + System::Session::~Session() = default; + + std::unique_ptr System::Session::GetNextFrame( + bool& shouldEndSession, + bool& shouldRestartSession, + std::function(void*)> deletedTextureAsyncCallback) + { + return m_impl->GetNextFrame(shouldEndSession, shouldRestartSession, std::move(deletedTextureAsyncCallback)); + } + + void System::Session::RequestEndSession() + { + m_impl->RequestEndSession(); + } + + void System::Session::SetDepthsNearFar(float depthNear, float depthFar) + { + m_impl->DepthNearZ = depthNear; + m_impl->DepthFarZ = depthFar; + } + + void System::Session::SetPlaneDetectionEnabled(bool) const {} + + bool System::Session::TrySetFeaturePointCloudEnabled(bool) const + { + return false; + } + + bool System::Session::TrySetPreferredPlaneDetectorOptions(const GeometryDetectorOptions&) + { + return false; + } + + bool System::Session::TrySetMeshDetectorEnabled(const bool) + { + return false; + } + + bool System::Session::TrySetPreferredMeshDetectorOptions(const GeometryDetectorOptions&) + { + return false; + } + + std::vector* System::Session::GetImageTrackingScores() const + { + return nullptr; + } + + void System::Session::CreateAugmentedImageDatabase(const std::vector&) const {} +} diff --git a/Plugins/NativeXr/Source/NativeXrImpl.cpp b/Plugins/NativeXr/Source/NativeXrImpl.cpp index 0855b87ca..b5724b834 100644 --- a/Plugins/NativeXr/Source/NativeXrImpl.cpp +++ b/Plugins/NativeXr/Source/NativeXrImpl.cpp @@ -276,6 +276,23 @@ namespace Babylon bgfx::overrideInternal(colorTexture, reinterpret_cast(viewConfig.ColorTexturePointer)); bgfx::overrideInternal(depthTexture, reinterpret_cast(viewConfig.DepthTexturePointer)); }).then(m_runtimeScheduler, m_sessionState->CancellationSource, [this, thisRef{shared_from_this()}, colorTexture, depthTexture, colorTextureFormat, requiresAppClear, &viewConfig]() { + // This continuation calls GetActiveEncoder() (via frameBuffer.Clear() below), which + // is only valid while the render thread's current frame is open (see + // DeviceImpl::StartRenderingCurrentFrame/FinishRenderingCurrentFrame). Without pinning + // a FrameCompletionScope here (mirroring ScheduleFrame's outer scope a few lines up), + // this JS-thread hop can run after the render thread has already closed the frame and + // nulled the encoder, dereferencing a null bgfx::Encoder* and crashing (observed as a + // SIGSEGV in bgfx::EncoderImpl::discard on first XR framebuffer creation). + // + // IMPORTANT: the scope must be acquired HERE, inside the lambda body, not as a + // capture-list initializer. Capture initializers are evaluated eagerly when the + // .then(...) call is constructed (i.e. while still inside BeginUpdate(), during + // the *previous* render-thread frame), not when this continuation actually runs. + // Acquiring it that early held a FrameCompletionScope open across the AfterRenderScheduler + // hop, which permanently pinned m_pendingFrameScopes above zero (AfterRenderScheduler is + // only pumped once FinishRenderingCurrentFrame's own pendingScopes==0 wait succeeds), + // deadlocking every subsequent frame. + Graphics::FrameCompletionScope frameScope{m_sessionState->GraphicsContext.AcquireFrameCompletionScope()}; const auto eyeCount = std::max(static_cast(1), static_cast(viewConfig.ViewTextureSize.Depth)); // TODO (rgerd): Remove old framebuffers from resource table? viewConfig.FrameBuffers.resize(eyeCount); diff --git a/Plugins/NativeXr/Source/XRSession.cpp b/Plugins/NativeXr/Source/XRSession.cpp index ac67583e8..cc4eb59d3 100644 --- a/Plugins/NativeXr/Source/XRSession.cpp +++ b/Plugins/NativeXr/Source/XRSession.cpp @@ -26,10 +26,25 @@ #include "XRSession.h" #include "XRFrame.h" +#include + namespace Babylon { namespace { + constexpr const char* GENERIC_TOUCH_CONTROLLER_PROFILE = "generic-trigger-squeeze-touchpad-thumbstick"; + constexpr const char* OCULUS_TOUCH_CONTROLLER_PROFILE = "oculus-touch"; + + const char* WebXRProfileFromInteractionProfile(const std::string& interactionProfileName) + { + if (interactionProfileName.find("touch_controller") != std::string::npos) + { + return OCULUS_TOUCH_CONTROLLER_PROFILE; + } + + return GENERIC_TOUCH_CONTROLLER_PROFILE; + } + void SetXRGamepadObjectData(Napi::Object& jsInputSource, Napi::Object& jsGamepadObject, xr::System::Session::Frame::InputSource& inputSource) { auto env = jsInputSource.Env(); @@ -201,7 +216,7 @@ namespace Babylon jsInputSource.Set("targetRayMode", TARGET_RAY_MODE); auto profiles = Napi::Array::New(env, 1); - Napi::Value string = Napi::String::New(env, "generic-trigger-squeeze-touchpad-thumbstick"); + Napi::Value string = Napi::String::New(env, WebXRProfileFromInteractionProfile(inputSource.InteractionProfileName)); profiles.Set(uint32_t{ 0 }, string); jsInputSource.Set("profiles", profiles);