From 2e195607e2263753470be1d09ce11b33dabc80aa Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 10 Mar 2024 21:02:57 +0100 Subject: [PATCH 01/20] Context: Maintain orientation of eye offset matrix. --- libraries/gpu/src/gpu/Context.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 8dee120555c..e5d1f87cd49 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -238,10 +238,14 @@ const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived(const Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const StereoState& _stereo, const Transform& xformView, Vec2 normalizedJitter) const { TransformCamera result = *this; Transform offsetTransform = xformView; - if (!_stereo._skybox) { - offsetTransform.postTranslate(-Vec3(_stereo._eyeViews[eye][3])); + glm::vec3 eyePosition = extractTranslation(_stereo._eyeViews[eye]); + glm::quat eyeOrientation = glmExtractRotation(_stereo._eyeViews[eye]); + if (!_stereo._skybox) + { + offsetTransform.postRotate(eyeOrientation).postTranslate(eyePosition); } else { // FIXME: If "skybox" the ipd is set to 0 for now, let s try to propose a better solution for this in the future + offsetTransform.postRotate(eyeOrientation); } result._projection = _stereo._eyeProjections[eye]; normalizedJitter.x *= 2.0f; From ace749b2e2f3155eacfeb24e1e1a88e91e71bc66 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 10 Mar 2024 21:02:42 +0100 Subject: [PATCH 02/20] Application: Maintain orientation of eye offset matrix. --- interface/src/Application.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f630bea8637..670814c1d8a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6971,16 +6971,9 @@ void Application::updateRenderArgs(float deltaTime) { // only when the display plugin changes (or in non-HMD modes when the user // changes the FOV manually, which right now I don't think they can. for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); + eyeOffsets[eye] = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; + eyeOffsets[eye][3][0] *= ipdScale; eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); }); From 9b43c0dd8fa9ee5e048bbe5a86aca9b0ec0ae664 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 10 Mar 2024 21:00:57 +0100 Subject: [PATCH 03/20] Application: Improve scoping. --- interface/src/Application.cpp | 60 ++++++++++++++++------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 670814c1d8a..dec19a6c2f7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6942,44 +6942,40 @@ void Application::updateRenderArgs(float deltaTime) { appRenderArgs._eyeToWorld = _myCamera.getTransform(); appRenderArgs._isStereo = false; - { + if (getActiveDisplayPlugin()->isStereo()) { auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; + float ipdScale = hmdInterface->getIPDScale() * sensorToWorldScale; auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); - if (getActiveDisplayPlugin()->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); - appRenderArgs._isStereo = true; - - auto& eyeOffsets = appRenderArgs._eyeOffsets; - auto& eyeProjections = appRenderArgs._eyeProjections; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - eyeOffsets[eye] = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); - // Apply IPD scaling - eyeOffsets[eye][3][0] *= ipdScale; - eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); - }); + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); + appRenderArgs._isStereo = true; + + auto& eyeOffsets = appRenderArgs._eyeOffsets; + auto& eyeProjections = appRenderArgs._eyeProjections; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + eyeOffsets[eye] = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); + // Apply IPD scaling + eyeOffsets[eye][3][0] *= ipdScale; + eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); + }); - // Configure the type of display / stereo - appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - } + // Configure the type of display / stereo + appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); } appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode(); From db6277c3e5bb5e115eb4ff0a5601f9b38e9447ca Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Mon, 12 Feb 2024 15:55:31 +0100 Subject: [PATCH 04/20] plugins: Add OpenXR plugin. Add OpenXrDisplayPlugin and OpenXrInputPlugin. Add controller bindings for the Valve Index controller. --- .../resources/controllers/openxr_index.json | 45 ++ plugins/CMakeLists.txt | 3 + plugins/openxr/CMakeLists.txt | 25 + plugins/openxr/src/OpenXrContext.cpp | 387 ++++++++++++ plugins/openxr/src/OpenXrContext.h | 82 +++ plugins/openxr/src/OpenXrDisplayPlugin.cpp | 551 ++++++++++++++++++ plugins/openxr/src/OpenXrDisplayPlugin.h | 97 +++ plugins/openxr/src/OpenXrInputPlugin.cpp | 523 +++++++++++++++++ plugins/openxr/src/OpenXrInputPlugin.h | 104 ++++ plugins/openxr/src/OpenXrProvider.cpp | 59 ++ plugins/openxr/src/plugin.json | 4 + 11 files changed, 1880 insertions(+) create mode 100644 interface/resources/controllers/openxr_index.json create mode 100644 plugins/openxr/CMakeLists.txt create mode 100644 plugins/openxr/src/OpenXrContext.cpp create mode 100644 plugins/openxr/src/OpenXrContext.h create mode 100644 plugins/openxr/src/OpenXrDisplayPlugin.cpp create mode 100644 plugins/openxr/src/OpenXrDisplayPlugin.h create mode 100644 plugins/openxr/src/OpenXrInputPlugin.cpp create mode 100644 plugins/openxr/src/OpenXrInputPlugin.h create mode 100644 plugins/openxr/src/OpenXrProvider.cpp create mode 100644 plugins/openxr/src/plugin.json diff --git a/interface/resources/controllers/openxr_index.json b/interface/resources/controllers/openxr_index.json new file mode 100644 index 00000000000..774b55981ea --- /dev/null +++ b/interface/resources/controllers/openxr_index.json @@ -0,0 +1,45 @@ +{ + "name": "OpenXR Index to Standard", + "channels": [ + { "from": "Index.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Index.RightHand", "to": "Standard.RightHand" }, + { "from": "Index.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }, + + { "from": "Index.A", "to": "Standard.RightPrimaryThumb", "peek": true }, + { "from": "Index.B", "to": "Standard.RightSecondaryThumb", "peek": true }, + { "from": "Index.X", "to": "Standard.LeftPrimaryThumb", "peek": true }, + { "from": "Index.Y", "to": "Standard.LeftSecondaryThumb", "peek": true}, + + { "from": "Index.A", "to": "Standard.A" }, + { "from": "Index.B", "to": "Standard.B" }, + { "from": "Index.X", "to": "Standard.X" }, + { "from": "Index.Y", "to": "Standard.Y" }, + + { "from": "Index.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, + { "from": "Index.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, + { "from": "Index.LeftThumbUp", "to": "Standard.LeftThumbUp" }, + + { "from": "Index.RightPrimaryThumbTouch", "to": "Standard.RightPrimaryThumbTouch" }, + { "from": "Index.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, + { "from": "Index.RightThumbUp", "to": "Standard.RightThumbUp" }, + + { "from": "Index.LY", "to": "Standard.LY" }, + { "from": "Index.LX", "to": "Standard.LX" }, + { "from": "Index.RY", "to": "Standard.RY" }, + { "from": "Index.RX", "to": "Standard.RX" }, + { "from": "Index.LSTouch", "to": "Standard.LSTouch" }, + { "from": "Index.RSTouch", "to": "Standard.RSTouch" }, + + { "from": "Index.RT", "to": "Standard.RT" }, + { "from": "Index.LT", "to": "Standard.LT" }, + { "from": "Index.RTClick", "to": "Standard.RTClick" }, + { "from": "Index.LTClick", "to": "Standard.LTClick" }, + { "from": "Index.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, + { "from": "Index.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" }, + { "from": "Index.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, + { "from": "Index.RightIndexPoint", "to": "Standard.RightIndexPoint" }, + + { "from": "Index.LeftApplicationMenu", "to": "Standard.Back" }, + { "from": "Index.RightApplicationMenu", "to": "Standard.Start" } + ] +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c68abefa770..5e22dfc41db 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -26,6 +26,9 @@ if (NOT SERVER_ONLY AND NOT ANDROID) add_subdirectory(${DIR}) endif() + set(DIR "openxr") + add_subdirectory(${DIR}) + set(DIR "hifiSdl2") add_subdirectory(${DIR}) diff --git a/plugins/openxr/CMakeLists.txt b/plugins/openxr/CMakeLists.txt new file mode 100644 index 00000000000..2e7eb9f821e --- /dev/null +++ b/plugins/openxr/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# Copyright 2024 Lubosz Sarnecki +# +# SPDX-License-Identifier: Apache-2.0 +# + +find_package(OpenXR REQUIRED) +if (NOT OpenXR_FOUND) + MESSAGE(FATAL_ERROR "OpenXR not found!") +endif() + +set(TARGET_NAME openxr) +setup_hifi_plugin(Gui Qml Multimedia) +link_hifi_libraries(shared task gl qml networking controllers ui + plugins display-plugins ui-plugins input-plugins + audio-client render-utils graphics shaders gpu render + material-networking model-networking model-baker hfm + model-serializers ktx image procedural ${PLATFORM_GL_BACKEND} OpenXR::openxr_loader) +include_hifi_library_headers(octree) +include_hifi_library_headers(script-engine) + +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + # Silence GCC warnings + target_compile_options(openxr PRIVATE -Wno-missing-field-initializers) +endif() diff --git a/plugins/openxr/src/OpenXrContext.cpp b/plugins/openxr/src/OpenXrContext.cpp new file mode 100644 index 00000000000..700d4887470 --- /dev/null +++ b/plugins/openxr/src/OpenXrContext.cpp @@ -0,0 +1,387 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include "OpenXrContext.h" +#include + +#include + +#include + +#define XR_USE_PLATFORM_XLIB +#define XR_USE_GRAPHICS_API_OPENGL +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(xr_context_cat) +Q_LOGGING_CATEGORY(xr_context_cat, "openxr.context") + +// Checks XrResult, returns false on errors and logs the error as qCritical. +bool xrCheck(XrInstance instance, XrResult result, const char* message) { + if (XR_SUCCEEDED(result)) + return true; + + char errorName[XR_MAX_RESULT_STRING_SIZE]; + if (instance != XR_NULL_HANDLE) { + xrResultToString(instance, result, errorName); + } else { + sprintf(errorName, "%d", result); + } + + qCCritical(xr_context_cat, "%s: %s", errorName, message); + + return false; +} + +// Extension functions must be loaded with xrGetInstanceProcAddr +static PFN_xrGetOpenGLGraphicsRequirementsKHR pfnGetOpenGLGraphicsRequirementsKHR = nullptr; +static bool initFunctionPointers(XrInstance instance) { + XrResult result = xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR", + (PFN_xrVoidFunction*)&pfnGetOpenGLGraphicsRequirementsKHR); + return xrCheck(instance, result, "Failed to get OpenGL graphics requirements function!"); +} + +OpenXrContext::OpenXrContext() { + _isSupported = initPreGraphics(); + if (!_isSupported) { + qCCritical(xr_context_cat, "Pre graphics init failed."); + } +} + +OpenXrContext::~OpenXrContext() { + XrResult res = xrDestroyInstance(_instance); + if (res != XR_SUCCESS) { + qCCritical(xr_context_cat, "Failed to destroy OpenXR instance"); + } + qCDebug(xr_context_cat, "Destroyed instance."); +} + +bool OpenXrContext::initInstance() { + uint32_t count = 0; + XrResult result = xrEnumerateInstanceExtensionProperties(nullptr, 0, &count, nullptr); + + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate number of extension properties")) + return false; + + std::vector properties; + for (uint32_t i = 0; i < count; i++) { + XrExtensionProperties props = { .type = XR_TYPE_EXTENSION_PROPERTIES }; + properties.push_back(props); + } + + result = xrEnumerateInstanceExtensionProperties(nullptr, count, &count, properties.data()); + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate extension properties")) + return false; + + bool openglSupported = false; + + qCInfo(xr_context_cat, "Runtime supports %d extensions:", count); + for (uint32_t i = 0; i < count; i++) { + qCInfo(xr_context_cat, "%s v%d", properties[i].extensionName, properties[i].extensionVersion); + if (strcmp(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME, properties[i].extensionName) == 0) { + openglSupported = true; + } + } + + if (!openglSupported) { + qCCritical(xr_context_cat, "Runtime does not support OpenGL!"); + return false; + } + + std::vector enabled = { XR_KHR_OPENGL_ENABLE_EXTENSION_NAME }; + + XrInstanceCreateInfo info = { + .type = XR_TYPE_INSTANCE_CREATE_INFO, + .applicationInfo = { + .applicationName = "overte", + .applicationVersion = 1, + .engineName = "overte", + .engineVersion = 0, + .apiVersion = XR_CURRENT_API_VERSION, + }, + .enabledExtensionCount = (uint32_t)enabled.size(), + .enabledExtensionNames = enabled.data(), + }; + + result = xrCreateInstance(&info, &_instance); + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to create XR instance.")) + return false; + + if (!initFunctionPointers(_instance)) + return false; + + xrStringToPath(_instance, "/user/hand/left", &_handPaths[0]); + xrStringToPath(_instance, "/user/hand/right", &_handPaths[1]); + + return true; +} + +bool OpenXrContext::initSystem() { + XrSystemGetInfo info = { + .type = XR_TYPE_SYSTEM_GET_INFO, + .formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY, + }; + + XrResult result = xrGetSystem(_instance, &info, &_systemId); + if (!xrCheck(_instance, result, "Failed to get system for HMD form factor.")) + return false; + + XrSystemProperties props = { + .type = XR_TYPE_SYSTEM_PROPERTIES, + }; + + result = xrGetSystemProperties(_instance, _systemId, &props); + if (!xrCheck(_instance, result, "Failed to get System properties")) + return false; + + _systemName = QString::fromUtf8(props.systemName); + + qCInfo(xr_context_cat, "System name : %s", props.systemName); + qCInfo(xr_context_cat, "Max layers : %d", props.graphicsProperties.maxLayerCount); + qCInfo(xr_context_cat, "Max swapchain size : %dx%d", props.graphicsProperties.maxSwapchainImageHeight, + props.graphicsProperties.maxSwapchainImageWidth); + qCInfo(xr_context_cat, "Orientation Tracking: %d", props.trackingProperties.orientationTracking); + qCInfo(xr_context_cat, "Position Tracking : %d", props.trackingProperties.positionTracking); + + return true; +} + +bool OpenXrContext::initGraphics() { + XrGraphicsRequirementsOpenGLKHR requirements = { .type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR }; + XrResult result = pfnGetOpenGLGraphicsRequirementsKHR(_instance, _systemId, &requirements); + return xrCheck(_instance, result, "Failed to get OpenGL graphics requirements!"); +} + +bool OpenXrContext::requestExitSession() { + XrResult result = xrRequestExitSession(_session); + return xrCheck(_instance, result, "Failed to request exit session!"); +} + +bool OpenXrContext::initSession() { + // TODO: Make cross platform + XrGraphicsBindingOpenGLXlibKHR binding = { + .type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR, + .xDisplay = XOpenDisplay(nullptr), + .glxDrawable = glXGetCurrentDrawable(), + .glxContext = glXGetCurrentContext(), + }; + + XrSessionCreateInfo info = { + .type = XR_TYPE_SESSION_CREATE_INFO, + .next = &binding, + .systemId = _systemId, + }; + + XrResult result = xrCreateSession(_instance, &info, &_session); + return xrCheck(_instance, result, "Failed to create session"); +} + +bool OpenXrContext::initSpaces() { + // TODO: Do xrEnumerateReferenceSpaces before assuming stage space is available. + XrReferenceSpaceCreateInfo stageSpaceInfo = { + .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, + .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE, + .poseInReferenceSpace = XR_INDENTITY_POSE, + }; + + XrResult result = xrCreateReferenceSpace(_session, &stageSpaceInfo, &_stageSpace); + if (!xrCheck(_instance, result, "Failed to create stage space!")) + return false; + + XrReferenceSpaceCreateInfo viewSpaceInfo = { + .type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO, + .referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW, + .poseInReferenceSpace = XR_INDENTITY_POSE, + }; + + result = xrCreateReferenceSpace(_session, &viewSpaceInfo, &_viewSpace); + return xrCheck(_instance, result, "Failed to create view space!"); +} + +#define ENUM_TO_STR(r) \ + case r: \ + return #r + +static std::string xrSessionStateStr(XrSessionState state) { + switch (state) { + ENUM_TO_STR(XR_SESSION_STATE_UNKNOWN); + ENUM_TO_STR(XR_SESSION_STATE_IDLE); + ENUM_TO_STR(XR_SESSION_STATE_READY); + ENUM_TO_STR(XR_SESSION_STATE_SYNCHRONIZED); + ENUM_TO_STR(XR_SESSION_STATE_VISIBLE); + ENUM_TO_STR(XR_SESSION_STATE_FOCUSED); + ENUM_TO_STR(XR_SESSION_STATE_STOPPING); + ENUM_TO_STR(XR_SESSION_STATE_LOSS_PENDING); + ENUM_TO_STR(XR_SESSION_STATE_EXITING); + default: { + std::ostringstream ss; + ss << "UNKNOWN STATE " << state; + return ss.str(); + } + } +} + +// Called before restarting a new session +void OpenXrContext::reset() { + _shouldQuit = false; + _lastSessionState = XR_SESSION_STATE_UNKNOWN; +} + +bool OpenXrContext::updateSessionState(XrSessionState newState) { + qCDebug(xr_context_cat, "Session state changed %s -> %s", xrSessionStateStr(_lastSessionState).c_str(), + xrSessionStateStr(newState).c_str()); + _lastSessionState = newState; + + switch (newState) { + // Don't run frame cycle but keep polling events + case XR_SESSION_STATE_IDLE: + case XR_SESSION_STATE_UNKNOWN: { + _shouldRunFrameCycle = false; + break; + } + + // Run frame cycle and poll events + case XR_SESSION_STATE_FOCUSED: + case XR_SESSION_STATE_SYNCHRONIZED: + case XR_SESSION_STATE_VISIBLE: { + _shouldRunFrameCycle = true; + break; + } + + // Begin the session + case XR_SESSION_STATE_READY: { + if (!_isSessionRunning) { + XrSessionBeginInfo session_begin_info = { + .type = XR_TYPE_SESSION_BEGIN_INFO, + .primaryViewConfigurationType = XR_VIEW_CONFIG_TYPE, + }; + XrResult result = xrBeginSession(_session, &session_begin_info); + if (!xrCheck(_instance, result, "Failed to begin session!")) + return false; + qCDebug(xr_context_cat, "Session started!"); + _isSessionRunning = true; + } + _shouldRunFrameCycle = true; + break; + } + + // End the session, don't render, but keep polling for events + case XR_SESSION_STATE_STOPPING: { + if (_isSessionRunning) { + XrResult result = xrEndSession(_session); + if (!xrCheck(_instance, result, "Failed to end session!")) + return false; + _isSessionRunning = false; + } + _shouldRunFrameCycle = false; + break; + } + + // Destroy session, skip run frame cycle, quit + case XR_SESSION_STATE_LOSS_PENDING: + case XR_SESSION_STATE_EXITING: { + XrResult result = xrDestroySession(_session); + if (!xrCheck(_instance, result, "Failed to destroy session!")) + return false; + _shouldQuit = true; + _shouldRunFrameCycle = false; + qCDebug(xr_context_cat, "Destroyed session"); + break; + } + default: + qCWarning(xr_context_cat, "Unhandled session state: %d", newState); + } + + return true; +} + +bool OpenXrContext::pollEvents() { + XrEventDataBuffer event = { .type = XR_TYPE_EVENT_DATA_BUFFER }; + XrResult result = xrPollEvent(_instance, &event); + while (result == XR_SUCCESS) { + switch (event.type) { + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: { + XrEventDataInstanceLossPending* instanceLossPending = (XrEventDataInstanceLossPending*)&event; + qCCritical(xr_context_cat, "Instance loss pending at %lu! Destroying instance.", instanceLossPending->lossTime); + _shouldQuit = true; + continue; + } + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: { + XrEventDataSessionStateChanged* sessionStateChanged = (XrEventDataSessionStateChanged*)&event; + if (!updateSessionState(sessionStateChanged->state)) { + return false; + } + break; + } + case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: { + for (int i = 0; i < HAND_COUNT; i++) { + XrInteractionProfileState state = { .type = XR_TYPE_INTERACTION_PROFILE_STATE }; + XrResult res = xrGetCurrentInteractionProfile(_session, _handPaths[i], &state); + if (!xrCheck(_instance, res, "Failed to get interaction profile")) + continue; + + uint32_t bufferCountOutput; + char profilePath[XR_MAX_PATH_LENGTH]; + res = xrPathToString(_instance, state.interactionProfile, XR_MAX_PATH_LENGTH, &bufferCountOutput, + profilePath); + if (!xrCheck(_instance, res, "Failed to get interaction profile path.")) + continue; + + qCInfo(xr_context_cat, "Controller %d: Interaction profile changed to '%s'", i, profilePath); + } + break; + } + default: + qCWarning(xr_context_cat, "Unhandled event type %d", event.type); + } + + event.type = XR_TYPE_EVENT_DATA_BUFFER; + result = xrPollEvent(_instance, &event); + } + + if (result != XR_EVENT_UNAVAILABLE) { + qCCritical(xr_context_cat, "Failed to poll events!"); + return false; + } + + return true; +} + +bool OpenXrContext::beginFrame() { + XrFrameBeginInfo info = { .type = XR_TYPE_FRAME_BEGIN_INFO }; + XrResult result = xrBeginFrame(_session, &info); + return xrCheck(_instance, result, "failed to begin frame!"); +} + +bool OpenXrContext::initPreGraphics() { + if (!initInstance()) { + return false; + } + + if (!initSystem()) { + return false; + } + + return true; +} + +bool OpenXrContext::initPostGraphics() { + if (!initGraphics()) { + return false; + } + + if (!initSession()) { + return false; + } + + if (!initSpaces()) { + return false; + } + + return true; +} diff --git a/plugins/openxr/src/OpenXrContext.h b/plugins/openxr/src/OpenXrContext.h new file mode 100644 index 00000000000..387bab388ea --- /dev/null +++ b/plugins/openxr/src/OpenXrContext.h @@ -0,0 +1,82 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "controllers/Pose.h" + +#include + +#include +#include + +#define HAND_COUNT 2 + +constexpr XrPosef XR_INDENTITY_POSE = { + .orientation = { .x = 0, .y = 0, .z = 0, .w = 1.0 }, + .position = { .x = 0, .y = 0, .z = 0 }, +}; + +constexpr XrViewConfigurationType XR_VIEW_CONFIG_TYPE = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + +class OpenXrContext { +public: + XrInstance _instance = XR_NULL_HANDLE; + XrSession _session = XR_NULL_HANDLE; + XrSystemId _systemId = XR_NULL_SYSTEM_ID; + + XrSpace _stageSpace = XR_NULL_HANDLE; + XrSpace _viewSpace = XR_NULL_HANDLE; + XrPath _handPaths[HAND_COUNT]; + + controller::Pose _lastHeadPose; + XrTime _lastPredictedDisplayTime; + // TODO: Enable C++17 and use std::optional + bool _lastPredictedDisplayTimeInitialized = false; + + bool _shouldQuit = false; + bool _shouldRunFrameCycle = false; + + bool _isSupported = false; + + QString _systemName; + bool _isSessionRunning = false; + +private: + XrSessionState _lastSessionState = XR_SESSION_STATE_UNKNOWN; + +public: + OpenXrContext(); + ~OpenXrContext(); + + bool initPostGraphics(); + bool beginFrame(); + bool pollEvents(); + bool requestExitSession(); + void reset(); + +private: + bool initPreGraphics(); + bool initInstance(); + bool initSystem(); + bool initGraphics(); + bool initSession(); + bool initSpaces(); + + bool updateSessionState(XrSessionState newState); +}; + +inline static glm::vec3 xrVecToGlm(const XrVector3f& v) { + return glm::vec3(v.x, v.y, v.z); +} + +inline static glm::quat xrQuatToGlm(const XrQuaternionf& q) { + return glm::quat(q.w, q.x, q.y, q.z); +} + +bool xrCheck(XrInstance instance, XrResult result, const char* message); \ No newline at end of file diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.cpp b/plugins/openxr/src/OpenXrDisplayPlugin.cpp new file mode 100644 index 00000000000..e952fe671cd --- /dev/null +++ b/plugins/openxr/src/OpenXrDisplayPlugin.cpp @@ -0,0 +1,551 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include "OpenXrDisplayPlugin.h" +#include + +#include "ViewFrustum.h" + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(xr_display_cat) +Q_LOGGING_CATEGORY(xr_display_cat, "openxr.display") + +constexpr GLint XR_PREFERRED_COLOR_FORMAT = GL_SRGB8_ALPHA8; + +OpenXrDisplayPlugin::OpenXrDisplayPlugin(std::shared_ptr c) { + _context = c; +} + +bool OpenXrDisplayPlugin::isSupported() const { + return _context->_isSupported; +} + +// Slightly differs from glm::ortho +inline static glm::mat4 fovToProjection(const XrFovf fov, const float near, const float far) { + const float left = tanf(fov.angleLeft); + const float right = tanf(fov.angleRight); + const float down = tanf(fov.angleDown); + const float up = tanf(fov.angleUp); + + const float width = right - left; + const float height = up - down; + + const float m11 = 2 / width; + const float m22 = 2 / height; + const float m33 = -(far + near) / (far - near); + + const float m31 = (right + left) / width; + const float m32 = (up + down) / height; + const float m43 = -(far * (near + near)) / (far - near); + + // clang-format off + const float mat[16] = { + m11, 0 , 0 , 0, + 0 , m22, 0 , 0, + m31, m32, m33, -1, + 0 , 0 , m43, 0, + }; + // clang-format on + + return glm::make_mat4(mat); +} + +glm::mat4 OpenXrDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + if (!_viewsInitialized) { + return baseProjection; + } + + ViewFrustum frustum; + frustum.setProjection(baseProjection); + return fovToProjection(_views[(eye == Left) ? 0 : 1].fov, frustum.getNearClip(), frustum.getFarClip()); +} + +// TODO: This apparently wasn't right in the OpenVR plugin, but this is what it basically did. +glm::mat4 OpenXrDisplayPlugin::getCullingProjection(const glm::mat4& baseProjection) const { + return getEyeProjection(Left, baseProjection); +} + +// TODO: This should not be explicilty known by the application. +// Let's just render as fast as we can and OpenXR will dictate the pace. +float OpenXrDisplayPlugin::getTargetFrameRate() const { + return std::numeric_limits::max(); +} + +bool OpenXrDisplayPlugin::initViews() { + XrInstance instance = _context->_instance; + XrSystemId systemId = _context->_systemId; + + XrResult result = xrEnumerateViewConfigurationViews(instance, systemId, XR_VIEW_CONFIG_TYPE, 0, &_viewCount, nullptr); + if (!xrCheck(instance, result, "Failed to get view configuration view count!")) { + qCCritical(xr_display_cat, "Failed to get view configuration view count!"); + return false; + } + + assert(_viewCount != 0); + + for (uint32_t i = 0; i < _viewCount; i++) { + XrView view = { .type = XR_TYPE_VIEW }; + _views.push_back(view); + + XrViewConfigurationView viewConfig = { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW }; + _viewConfigs.push_back(viewConfig); + } + + _swapChains.resize(_viewCount); + _swapChainLengths.resize(_viewCount); + _swapChainIndices.resize(_viewCount); + _images.resize(_viewCount); + + result = xrEnumerateViewConfigurationViews(instance, systemId, XR_VIEW_CONFIG_TYPE, _viewCount, &_viewCount, + _viewConfigs.data()); + if (!xrCheck(instance, result, "Failed to enumerate view configuration views!")) { + qCCritical(xr_display_cat, "Failed to enumerate view configuration views!"); + return false; + } + + return true; +} + +#define ENUM_TO_STR(r) \ + case r: \ + return #r + +static std::string glFormatStr(GLenum source) { + switch (source) { + ENUM_TO_STR(GL_RGBA16); + ENUM_TO_STR(GL_RGBA16F); + ENUM_TO_STR(GL_SRGB8_ALPHA8); + default: { + // TODO: Enable C++20 for std::format + std::ostringstream ss; + ss << "0x" << std::hex << source; + return ss.str(); + } + } +} + +static int64_t chooseSwapChainFormat(XrInstance instance, XrSession session, int64_t preferred) { + uint32_t formatCount; + XrResult result = xrEnumerateSwapchainFormats(session, 0, &formatCount, nullptr); + if (!xrCheck(instance, result, "Failed to get number of supported swapchain formats")) + return -1; + + qCInfo(xr_display_cat, "Runtime supports %d swapchain formats", formatCount); + std::vector formats(formatCount); + + result = xrEnumerateSwapchainFormats(session, formatCount, &formatCount, formats.data()); + if (!xrCheck(instance, result, "Failed to enumerate swapchain formats")) + return -1; + + int64_t chosen = formats[0]; + + for (uint32_t i = 0; i < formatCount; i++) { + qCInfo(xr_display_cat, "Supported GL format: %s", glFormatStr(formats[i]).c_str()); + if (formats[i] == preferred) { + chosen = formats[i]; + qCInfo(xr_display_cat, "Using preferred swapchain format %s", glFormatStr(chosen).c_str()); + break; + } + } + if (chosen != preferred) { + qCWarning(xr_display_cat, "Falling back to non preferred swapchain format %s", glFormatStr(chosen).c_str()); + } + + return chosen; +} + +bool OpenXrDisplayPlugin::initSwapChains() { + XrInstance instance = _context->_instance; + XrSession session = _context->_session; + + int64_t format = chooseSwapChainFormat(instance, session, XR_PREFERRED_COLOR_FORMAT); + + for (uint32_t i = 0; i < _viewCount; i++) { + _images[i].clear(); + + XrSwapchainCreateInfo info = { + .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, + .createFlags = 0, + .usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT, + .format = format, + .sampleCount = _viewConfigs[i].recommendedSwapchainSampleCount, + .width = _viewConfigs[i].recommendedImageRectWidth, + .height = _viewConfigs[i].recommendedImageRectHeight, + .faceCount = 1, + .arraySize = 1, + .mipCount = 1, + }; + + XrResult result = xrCreateSwapchain(session, &info, &_swapChains[i]); + if (!xrCheck(instance, result, "Failed to create swapchain!")) + return false; + + result = xrEnumerateSwapchainImages(_swapChains[i], 0, &_swapChainLengths[i], nullptr); + if (!xrCheck(instance, result, "Failed to enumerate swapchains")) + return false; + + for (uint32_t j = 0; j < _swapChainLengths[i]; j++) { + XrSwapchainImageOpenGLKHR image = { .type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR }; + _images[i].push_back(image); + } + result = xrEnumerateSwapchainImages(_swapChains[i], _swapChainLengths[i], &_swapChainLengths[i], + (XrSwapchainImageBaseHeader*)_images[i].data()); + if (!xrCheck(instance, result, "Failed to enumerate swapchain images")) + return false; + } + + return true; +} + +bool OpenXrDisplayPlugin::initLayers() { + for (uint32_t i = 0; i < _viewCount; i++) { + XrCompositionLayerProjectionView layer = { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + .subImage = { + .swapchain = _swapChains[i], + .imageRect = { + .offset = { + .x = 0, + .y = 0, + }, + .extent = { + .width = (int32_t)_viewConfigs[i].recommendedImageRectWidth, + .height = (int32_t)_viewConfigs[i].recommendedImageRectHeight, + }, + }, + .imageArrayIndex = 0, + }, + }; + _projectionLayerViews.push_back(layer); + }; + + return true; +} + +void OpenXrDisplayPlugin::init() { + Plugin::init(); + + if (!initViews()) { + qCCritical(xr_display_cat, "View init failed."); + return; + } + + for (const XrViewConfigurationView& view : _viewConfigs) { + assert(view.recommendedImageRectWidth != 0); + qCDebug(xr_display_cat, "Swapchain dimensions: %dx%d", view.recommendedImageRectWidth, view.recommendedImageRectHeight); + // TODO: Don't render side-by-side but use multiview (texture arrays). This probably won't work with GL. + _renderTargetSize.x = view.recommendedImageRectWidth * 2; + _renderTargetSize.y = view.recommendedImageRectHeight; + } + + emit deviceConnected(getName()); +} + +const QString OpenXrDisplayPlugin::getName() const { + return QString("OpenXR: %1").arg(_context->_systemName); +} + +bool OpenXrDisplayPlugin::internalActivate() { + _context->reset(); + return HmdDisplayPlugin::internalActivate(); +} + +void OpenXrDisplayPlugin::internalDeactivate() { + // We can get into a state where activate -> deactivate -> activate is called in a chain. + // We are probably gonna have a bad time then. At least check if the session is already running. + // This happens when the application decides to switch display plugins back and forth. This should + // prbably be fixed there. + if (_context->_isSessionRunning) { + if (!_context->requestExitSession()) { + qCCritical(xr_display_cat, "Failed to request exit session"); + } else { + // Poll events until runtime wants to quit + while (!_context->_shouldQuit) { + _context->pollEvents(); + } + } + } + HmdDisplayPlugin::internalDeactivate(); +} + +void OpenXrDisplayPlugin::customizeContext() { + gl::initModuleGl(); + HmdDisplayPlugin::customizeContext(); + + if (!_context->initPostGraphics()) { + qCCritical(xr_display_cat, "Post graphics init failed."); + return; + } + + if (!initSwapChains()) { + qCCritical(xr_display_cat, "Swap chain init failed."); + return; + } + + if (!initLayers()) { + qCCritical(xr_display_cat, "Layer init failed."); + return; + } + + // Create swap chain images for _compositeFramebuffer + for (size_t i = 0; i < _swapChainLengths[0]; ++i) { + gpu::TexturePointer texture = + gpu::Texture::createRenderBuffer(gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y, + gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT)); + _compositeSwapChain.push_back(texture); + } +} + +void OpenXrDisplayPlugin::uncustomizeContext() { + _compositeSwapChain.clear(); + _projectionLayerViews.clear(); + for (uint32_t i = 0; i < _viewCount; i++) { + _images[i].clear(); + } + HmdDisplayPlugin::uncustomizeContext(); +} + +void OpenXrDisplayPlugin::resetSensors() { +} + +bool OpenXrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + _context->pollEvents(); + + if (_context->_shouldQuit) { + QMetaObject::invokeMethod(qApp, "quit"); + return false; + } + + if (!_context->_shouldRunFrameCycle) { + qCWarning(xr_display_cat, "beginFrameRender: Shoudln't run frame cycle. Skipping renderin frame %d", frameIndex); + return true; + } + + // Wait for present thread + // Actually wait for xrEndFrame to happen. + bool haveFrameToSubmit = true; + { + std::unique_lock lock(_haveFrameMutex); + haveFrameToSubmit = _haveFrameToSubmit; + } + + while (haveFrameToSubmit) { + std::this_thread::sleep_for(std::chrono::microseconds(10)); + { + std::unique_lock lock(_haveFrameMutex); + haveFrameToSubmit = _haveFrameToSubmit; + } + } + + _lastFrameState = { .type = XR_TYPE_FRAME_STATE }; + XrResult result = xrWaitFrame(_context->_session, nullptr, &_lastFrameState); + + if (!xrCheck(_context->_instance, result, "xrWaitFrame failed")) + return false; + + if (!_context->beginFrame()) + return false; + + _context->_lastPredictedDisplayTime = _lastFrameState.predictedDisplayTime; + _context->_lastPredictedDisplayTimeInitialized = true; + + std::vector eye_views(_viewCount); + for (uint32_t i = 0; i < _viewCount; i++) { + eye_views[i].type = XR_TYPE_VIEW; + } + + // TODO: Probably shouldn't call xrLocateViews twice. Use only view space views? + XrViewLocateInfo eyeViewLocateInfo = { + .type = XR_TYPE_VIEW_LOCATE_INFO, + .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + .displayTime = _lastFrameState.predictedDisplayTime, + .space = _context->_viewSpace, + }; + + XrViewState eyeViewState = { .type = XR_TYPE_VIEW_STATE }; + + result = xrLocateViews(_context->_session, &eyeViewLocateInfo, &eyeViewState, _viewCount, &_viewCount, eye_views.data()); + if (!xrCheck(_context->_instance, result, "Could not locate views")) + return false; + + for (uint32_t i = 0; i < 2; i++) { + vec3 eyePosition = xrVecToGlm(eye_views[i].pose.position); + quat eyeOrientation = xrQuatToGlm(eye_views[i].pose.orientation); + _eyeOffsets[i] = controller::Pose(eyePosition, eyeOrientation).getMatrix(); + } + + _lastViewState = { .type = XR_TYPE_VIEW_STATE }; + + XrViewLocateInfo viewLocateInfo = { + .type = XR_TYPE_VIEW_LOCATE_INFO, + .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, + .displayTime = _lastFrameState.predictedDisplayTime, + .space = _context->_stageSpace, + }; + + result = xrLocateViews(_context->_session, &viewLocateInfo, &_lastViewState, _viewCount, &_viewCount, _views.data()); + if (!xrCheck(_context->_instance, result, "Could not locate views")) + return false; + + for (uint32_t i = 0; i < _viewCount; i++) { + _projectionLayerViews[i].pose = _views[i].pose; + _projectionLayerViews[i].fov = _views[i].fov; + } + + _viewsInitialized = true; + + XrSpaceLocation headLocation = { + .type = XR_TYPE_SPACE_LOCATION, + .pose = XR_INDENTITY_POSE, + }; + xrLocateSpace(_context->_viewSpace, _context->_stageSpace, _lastFrameState.predictedDisplayTime, &headLocation); + + glm::vec3 headPosition = xrVecToGlm(headLocation.pose.position); + glm::quat headOrientation = xrQuatToGlm(headLocation.pose.orientation); + _context->_lastHeadPose = controller::Pose(headPosition, headOrientation); + + _currentRenderFrameInfo = FrameInfo(); + _currentRenderFrameInfo.renderPose = _context->_lastHeadPose.getMatrix(); + _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + + return HmdDisplayPlugin::beginFrameRender(frameIndex); +} + +void OpenXrDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { + OpenGLDisplayPlugin::submitFrame(newFrame); + { + std::unique_lock lock(_haveFrameMutex); + _haveFrameToSubmit = true; + } +} + +void OpenXrDisplayPlugin::compositeLayers() { + if (!_context->_shouldRunFrameCycle) { + return; + } + + if (_lastFrameState.shouldRender) { + _compositeFramebuffer->setRenderBuffer(0, _compositeSwapChain[_swapChainIndices[0]]); + HmdDisplayPlugin::compositeLayers(); + } +} + +void OpenXrDisplayPlugin::hmdPresent() { + if (!_context->_shouldRunFrameCycle) { + qCWarning(xr_display_cat, "hmdPresent: Shoudln't run frame cycle. Skipping renderin frame %d", + _currentFrame->frameIndex); + return; + } + + if (_lastFrameState.shouldRender) { + // TODO: Use multiview swapchain + for (uint32_t i = 0; i < 2; i++) { + XrSwapchainImageAcquireInfo acquireInfo = { .type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; + + XrResult result = xrAcquireSwapchainImage(_swapChains[i], &acquireInfo, &_swapChainIndices[i]); + if (!xrCheck(_context->_instance, result, "failed to acquire swapchain image!")) + return; + + XrSwapchainImageWaitInfo waitInfo = { .type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, .timeout = 1000 }; + result = xrWaitSwapchainImage(_swapChains[i], &waitInfo); + if (!xrCheck(_context->_instance, result, "failed to wait for swapchain image!")) + return; + } + + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + + glCopyImageSubData(glTexId, GL_TEXTURE_2D, 0, 0, 0, 0, _images[0][_swapChainIndices[0]].image, GL_TEXTURE_2D, 0, 0, 0, + 0, _renderTargetSize.x / 2, _renderTargetSize.y, 1); + + glCopyImageSubData(glTexId, GL_TEXTURE_2D, 0, _renderTargetSize.x / 2, 0, 0, _images[1][_swapChainIndices[1]].image, + GL_TEXTURE_2D, 0, 0, 0, 0, _renderTargetSize.x / 2, _renderTargetSize.y, 1); + + for (uint32_t i = 0; i < 2; i++) { + XrSwapchainImageReleaseInfo releaseInfo = { .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + XrResult result = xrReleaseSwapchainImage(_swapChains[i], &releaseInfo); + if (!xrCheck(_context->_instance, result, "failed to release swapchain image!")) { + assert(false); + return; + } + } + } + + endFrame(); + + _presentRate.increment(); + + { + std::unique_lock lock(_haveFrameMutex); + _haveFrameToSubmit = false; + } +} + +bool OpenXrDisplayPlugin::endFrame() { + XrCompositionLayerProjection projectionLayer = { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, + .layerFlags = 0, + .space = _context->_stageSpace, + .viewCount = _viewCount, + .views = _projectionLayerViews.data(), + }; + + std::vector layers = { + (const XrCompositionLayerBaseHeader*)&projectionLayer, + }; + + XrFrameEndInfo info = { + .type = XR_TYPE_FRAME_END_INFO, + .displayTime = _lastFrameState.predictedDisplayTime, + .environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE, + .layerCount = (uint32_t)layers.size(), + .layers = layers.data(), + }; + + if ((_lastViewState.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) == 0) { + qCWarning(xr_display_cat, "Not submitting layers because orientation is invalid."); + info.layerCount = 0; + } + + if (!_lastFrameState.shouldRender) { + info.layerCount = 0; + } + + XrResult result = xrEndFrame(_context->_session, &info); + if (!xrCheck(_context->_instance, result, "failed to end frame!")) { + return false; + } + + return true; +} + +void OpenXrDisplayPlugin::postPreview() { +} + +bool OpenXrDisplayPlugin::isHmdMounted() const { + return true; +} + +void OpenXrDisplayPlugin::updatePresentPose() { +} + +int OpenXrDisplayPlugin::getRequiredThreadCount() const { + return HmdDisplayPlugin::getRequiredThreadCount(); +} + +QRectF OpenXrDisplayPlugin::getPlayAreaRect() { + return QRectF(0, 0, 10, 10); +} + +DisplayPlugin::StencilMaskMeshOperator OpenXrDisplayPlugin::getStencilMaskMeshOperator() { + return nullptr; +} diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.h b/plugins/openxr/src/OpenXrDisplayPlugin.h new file mode 100644 index 00000000000..e36dba44f54 --- /dev/null +++ b/plugins/openxr/src/OpenXrDisplayPlugin.h @@ -0,0 +1,97 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include "OpenXrContext.h" + +#include "gpu/gl/GLBackend.h" + +#include + +#define XR_USE_PLATFORM_XLIB +#define XR_USE_GRAPHICS_API_OPENGL +#include +#include + +class OpenXrDisplayPlugin : public HmdDisplayPlugin { +public: + OpenXrDisplayPlugin(std::shared_ptr c); + bool isSupported() const override; + const QString getName() const override; + bool getSupportsAutoSwitch() override final { return true; } + + glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const override; + glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const override; + + void init() override; + + float getTargetFrameRate() const override; + bool hasAsyncReprojection() const override { return true; } + + void customizeContext() override; + void uncustomizeContext() override; + + void resetSensors() override; + bool beginFrameRender(uint32_t frameIndex) override; + void submitFrame(const gpu::FramePointer& newFrame) override; + void cycleDebugOutput() override { _lockCurrentTexture = !_lockCurrentTexture; } + + int getRequiredThreadCount() const override; + + QRectF getPlayAreaRect() override; + + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::MESH; } + virtual StencilMaskMeshOperator getStencilMaskMeshOperator() override; + + glm::mat4 getSensorResetMatrix() const { return glm::mat4(1.0f); } + +protected: + bool internalActivate() override; + void internalDeactivate() override; + void updatePresentPose() override; + + void compositeLayers() override; + void hmdPresent() override; + bool isHmdMounted() const override; + void postPreview() override; + +private: + std::vector _compositeSwapChain; + + XrViewState _lastViewState; + + std::shared_ptr _context; + + uint32_t _viewCount = 0; + std::vector _projectionLayerViews; + + std::vector _views; + // TODO: Enable C++17 and use std::optional + bool _viewsInitialized = false; + + std::vector _viewConfigs; + + std::vector _swapChains; + std::vector _swapChainLengths; + std::vector _swapChainIndices; + std::vector> _images; + + XrFrameState _lastFrameState; + + bool initViews(); + bool initSwapChains(); + bool initLayers(); + bool endFrame(); + + bool _haveFrameToSubmit = false; + std::mutex _haveFrameMutex; +}; diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp new file mode 100644 index 00000000000..f5eaabff0b3 --- /dev/null +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -0,0 +1,523 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include "OpenXrInputPlugin.h" + +#include "AvatarConstants.h" +#include "PathUtils.h" + +#include "controllers/UserInputMapper.h" + +Q_DECLARE_LOGGING_CATEGORY(xr_input_cat) +Q_LOGGING_CATEGORY(xr_input_cat, "openxr.input") + +OpenXrInputPlugin::OpenXrInputPlugin(std::shared_ptr c) { + _context = c; + _inputDevice = std::make_shared(_context); +} + +// TODO: Make a config UI +static const QString XR_CONFIGURATION_LAYOUT = QString(""); + +void OpenXrInputPlugin::calibrate() { +} + +bool OpenXrInputPlugin::uncalibrate() { + return true; +} + +bool OpenXrInputPlugin::isSupported() const { + return _context->_isSupported; +} + +void OpenXrInputPlugin::setConfigurationSettings(const QJsonObject configurationSettings) { +} + +QJsonObject OpenXrInputPlugin::configurationSettings() { + return QJsonObject(); +} + +QString OpenXrInputPlugin::configurationLayout() { + return XR_CONFIGURATION_LAYOUT; +} + +bool OpenXrInputPlugin::activate() { + InputPlugin::activate(); + + loadSettings(); + + // register with UserInputMapper + auto userInputMapper = DependencyManager::get(); + userInputMapper->registerDevice(_inputDevice); + _registeredWithInputMapper = true; + + return true; +} + +void OpenXrInputPlugin::deactivate() { + InputPlugin::deactivate(); + + _inputDevice->_poseStateMap.clear(); + + // unregister with UserInputMapper + auto userInputMapper = DependencyManager::get(); + userInputMapper->removeDevice(_inputDevice->_deviceID); + _registeredWithInputMapper = false; + + saveSettings(); +} + +void OpenXrInputPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + if (_context->_shouldQuit) { + deactivate(); + return; + } + + auto userInputMapper = DependencyManager::get(); + userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); }); + + if (_inputDevice->_trackedControllers == 0 && _registeredWithInputMapper) { + userInputMapper->removeDevice(_inputDevice->_deviceID); + _registeredWithInputMapper = false; + _inputDevice->_poseStateMap.clear(); + } + + if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { + userInputMapper->registerDevice(_inputDevice); + _registeredWithInputMapper = true; + } +} + +void OpenXrInputPlugin::loadSettings() { +} + +void OpenXrInputPlugin::saveSettings() const { +} + +OpenXrInputPlugin::InputDevice::InputDevice(std::shared_ptr c) : controller::InputDevice("Index") { + _context = c; +} + +void OpenXrInputPlugin::InputDevice::focusOutEvent() { + _axisStateMap.clear(); + _buttonPressedMap.clear(); +}; + +bool OpenXrInputPlugin::InputDevice::triggerHapticPulse(float strength, float duration, uint16_t index) { + if (index > 2) { + return false; + } + + std::unique_lock locker(_lock); + + // TODO: convert duration and strength to openxr values. + if (!_actions.at("/output/haptic")->applyHaptic(0, XR_MIN_HAPTIC_DURATION, XR_FREQUENCY_UNSPECIFIED, 0.5f)) { + qCCritical(xr_input_cat, "Failed to apply haptic feedback!"); + } + + return true; +} + +bool OpenXrInputPlugin::Action::init(XrActionSet actionSet) { + XrInstance instance = _context->_instance; + XrActionCreateInfo info = { + .type = XR_TYPE_ACTION_CREATE_INFO, + .actionType = _type, + .countSubactionPaths = HAND_COUNT, + .subactionPaths = _context->_handPaths, + }; + + QString name = QString::fromStdString(_path); + name.replace("/input/", ""); + name.replace("/", "-"); + strcpy(info.actionName, name.toUtf8().data()); + name.replace("-", " "); + strcpy(info.localizedActionName, name.toUtf8().data()); + + XrResult result = xrCreateAction(actionSet, &info, &_action); + if (!xrCheck(instance, result, "Failed to create action")) + return false; + + // Pose actions need spaces + if (_type == XR_ACTION_TYPE_POSE_INPUT) { + if (!createPoseSpaces()) { + return false; + } + } + + return true; +} + +const std::vector HAND_PATHS = { "left", "right" }; + +std::vector OpenXrInputPlugin::Action::getBindings() { + assert(_action != XR_NULL_HANDLE); + + std::vector bindings; + for (uint32_t i = 0; i < HAND_COUNT; i++) { + XrPath path; + std::string pathString = "/user/hand/" + HAND_PATHS[i] + _path; + xrStringToPath(_context->_instance, pathString.c_str(), &path); + XrActionSuggestedBinding binding = { .action = _action, .binding = path }; + bindings.push_back(binding); + } + return bindings; +} + +XrActionStateFloat OpenXrInputPlugin::Action::getFloat(uint32_t handId) { + XrActionStateFloat state = { + .type = XR_TYPE_ACTION_STATE_FLOAT, + }; + + XrActionStateGetInfo info = { + .type = XR_TYPE_ACTION_STATE_GET_INFO, + .action = _action, + .subactionPath = _context->_handPaths[handId], + }; + + XrResult result = xrGetActionStateFloat(_context->_session, &info, &state); + xrCheck(_context->_instance, result, "Failed to get float state!"); + + return state; +} + +XrActionStateBoolean OpenXrInputPlugin::Action::getBool(uint32_t handId) { + XrActionStateBoolean state = { + .type = XR_TYPE_ACTION_STATE_BOOLEAN, + }; + + XrActionStateGetInfo info = { + .type = XR_TYPE_ACTION_STATE_GET_INFO, + .action = _action, + .subactionPath = _context->_handPaths[handId], + }; + + XrResult result = xrGetActionStateBoolean(_context->_session, &info, &state); + xrCheck(_context->_instance, result, "Failed to get float state!"); + + return state; +} + +XrSpaceLocation OpenXrInputPlugin::Action::getPose(uint32_t handId) { + XrActionStatePose state = { + .type = XR_TYPE_ACTION_STATE_POSE, + }; + XrActionStateGetInfo info = { + .type = XR_TYPE_ACTION_STATE_GET_INFO, + .action = _action, + .subactionPath = _context->_handPaths[handId], + }; + + XrResult result = xrGetActionStatePose(_context->_session, &info, &state); + xrCheck(_context->_instance, result, "failed to get pose value!"); + + XrSpaceLocation location = { + .type = XR_TYPE_SPACE_LOCATION, + }; + + if (_context->_lastPredictedDisplayTimeInitialized) { + result = xrLocateSpace(_poseSpaces[handId], _context->_stageSpace, _context->_lastPredictedDisplayTime, &location); + xrCheck(_context->_instance, result, "Failed to locate hand space!"); + } + + return location; +} + +bool OpenXrInputPlugin::Action::applyHaptic(uint32_t handId, XrDuration duration, float frequency, float amplitude) { + XrHapticVibration vibration = { + .type = XR_TYPE_HAPTIC_VIBRATION, + .duration = duration, + .frequency = frequency, + .amplitude = amplitude, + }; + + XrHapticActionInfo haptic_action_info = { + .type = XR_TYPE_HAPTIC_ACTION_INFO, + .action = _action, + .subactionPath = _context->_handPaths[handId], + }; + XrResult result = xrApplyHapticFeedback(_context->_session, &haptic_action_info, (const XrHapticBaseHeader*)&vibration); + + return xrCheck(_context->_instance, result, "Failed to apply haptic feedback!"); +} + +bool OpenXrInputPlugin::Action::createPoseSpaces() { + assert(_action != XR_NULL_HANDLE); + + for (int hand = 0; hand < HAND_COUNT; hand++) { + XrActionSpaceCreateInfo info = { + .type = XR_TYPE_ACTION_SPACE_CREATE_INFO, + .action = _action, + .subactionPath = _context->_handPaths[hand], + .poseInActionSpace = XR_INDENTITY_POSE, + }; + + XrResult result = xrCreateActionSpace(_context->_session, &info, &_poseSpaces[hand]); + if (!xrCheck(_context->_instance, result, "Failed to create hand pose space")) + return false; + } + + return true; +} + +bool OpenXrInputPlugin::InputDevice::initBindings(const std::string& profileName, + const std::vector& actionsToBind) { + XrPath profilePath; + XrResult result = xrStringToPath(_context->_instance, profileName.c_str(), &profilePath); + if (!xrCheck(_context->_instance, result, "Failed to get interaction profile")) + return false; + + std::vector bindings; + for (const std::string& path : actionsToBind) { + std::vector actionBindings = _actions.at(path)->getBindings(); + bindings.insert(std::end(bindings), std::begin(actionBindings), std::end(actionBindings)); + } + + const XrInteractionProfileSuggestedBinding suggestedBinding = { + .type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING, + .interactionProfile = profilePath, + .countSuggestedBindings = (uint32_t)bindings.size(), + .suggestedBindings = bindings.data(), + }; + + result = xrSuggestInteractionProfileBindings(_context->_instance, &suggestedBinding); + + return xrCheck(_context->_instance, result, "Failed to suggest bindings"); +} + +controller::Input::NamedVector OpenXrInputPlugin::InputDevice::getAvailableInputs() const { + using namespace controller; + + // clang-format off + QVector availableInputs{ + // Poses + makePair(LEFT_HAND, "LeftHand"), + makePair(RIGHT_HAND, "RightHand"), + makePair(HEAD, "Head"), + // Sticks + makePair(LX, "LX"), + makePair(LY, "LY"), + makePair(RX, "RX"), + makePair(RY, "RY"), + // Face buttons + makePair(A, "A"), + makePair(B, "B"), + makePair(X, "X"), + makePair(Y, "Y"), + // Triggers + makePair(RT, "RT"), + makePair(LT, "LT"), + makePair(RT_CLICK, "RTClick"), + makePair(LT_CLICK, "LTClick"), + // Menu buttons + // TODO: Add this to button channel + // Input::NamedPair(Input(_deviceID, LEFT_APP_MENU, ChannelType::BUTTON), "LeftApplicationMenu"), + // Input::NamedPair(Input(_deviceID, RIGHT_APP_MENU, ChannelType::BUTTON), "RightApplicationMenu"), + }; + // clang-format on + + return availableInputs; +} + +QString OpenXrInputPlugin::InputDevice::getDefaultMappingConfig() const { + return PathUtils::resourcesPath() + "/controllers/openxr_index.json"; +} + +bool OpenXrInputPlugin::InputDevice::initActions() { + if (_actionsInitialized) + return true; + + assert(_context->_session != XR_NULL_HANDLE); + + XrInstance instance = _context->_instance; + + XrActionSetCreateInfo actionSetInfo = { + .type = XR_TYPE_ACTION_SET_CREATE_INFO, + .actionSetName = "action_set", + .localizedActionSetName = "Action Set", + .priority = 0, + }; + XrResult result = xrCreateActionSet(instance, &actionSetInfo, &_actionSet); + if (!xrCheck(instance, result, "Failed to create action set.")) + return false; + + // clang-format off + std::map actionsToInit = { + { "/input/thumbstick/x", XR_ACTION_TYPE_FLOAT_INPUT }, + { "/input/thumbstick/y", XR_ACTION_TYPE_FLOAT_INPUT }, + { "/input/a/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/b/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/trigger/value", XR_ACTION_TYPE_FLOAT_INPUT }, + { "/input/trigger/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/output/haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT }, + { "/input/grip/pose", XR_ACTION_TYPE_POSE_INPUT }, + { "/input/select/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/system/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + }; + // clang-format on + + for (const auto& pathAndType : actionsToInit) { + std::shared_ptr action = std::make_shared(_context, pathAndType.second, pathAndType.first); + if (!action->init(_actionSet)) { + qCCritical(xr_input_cat, "Creating action %s failed!", pathAndType.first.c_str()); + } else { + _actions.emplace(pathAndType.first, action); + } + } + + // Khronos Simple Controller + std::vector simpleBindings = { + "/input/grip/pose", + "/input/select/click", + "/output/haptic", + }; + + if (!initBindings("/interaction_profiles/khr/simple_controller", simpleBindings)) { + qCCritical(xr_input_cat, "Failed to init bindings."); + } + + // Valve Index Controller + // clang-format off + std::vector indexBindings = { + "/input/grip/pose", + "/input/thumbstick/x", + "/input/thumbstick/y", + "/input/a/click", + "/input/b/click", + "/input/trigger/value", + "/output/haptic", + "/input/system/click", + }; + // clang-format on + + if (!initBindings("/interaction_profiles/valve/index_controller", indexBindings)) { + qCCritical(xr_input_cat, "Failed to init bindings."); + } + + XrSessionActionSetsAttachInfo attachInfo = { + .type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO, + .countActionSets = 1, + .actionSets = &_actionSet, + }; + result = xrAttachSessionActionSets(_context->_session, &attachInfo); + if (!xrCheck(_context->_instance, result, "Failed to attach action set")) + return false; + + _actionsInitialized = true; + + return true; +} + +void OpenXrInputPlugin::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + _poseStateMap.clear(); + _buttonPressedMap.clear(); + _trackedControllers = 2; + + if (_context->_session == XR_NULL_HANDLE) { + return; + } + + if (!initActions()) { + qCCritical(xr_input_cat, "Could not initialize actions!"); + return; + } + + const XrActiveActionSet active_actionset = { + .actionSet = _actionSet, + }; + + XrActionsSyncInfo syncInfo = { + .type = XR_TYPE_ACTIONS_SYNC_INFO, + .countActiveActionSets = 1, + .activeActionSets = &active_actionset, + }; + + XrInstance instance = _context->_instance; + XrSession session = _context->_session; + + XrResult result = xrSyncActions(session, &syncInfo); + xrCheck(instance, result, "failed to sync actions!"); + + glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + for (int i = 0; i < HAND_COUNT; i++) { + XrSpaceLocation handLocation = _actions.at("/input/grip/pose")->getPose(i); + bool locationValid = (handLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0; + if (locationValid) { + vec3 translation = xrVecToGlm(handLocation.pose.position); + quat rotation = xrQuatToGlm(handLocation.pose.orientation); + auto pose = controller::Pose(translation, rotation); + glm::mat4 handOffset = i == 0 ? glm::toMat4(leftRotationOffset) : glm::toMat4(rightRotationOffset); + _poseStateMap[i == 0 ? controller::LEFT_HAND : controller::RIGHT_HAND] = + pose.postTransform(handOffset).transform(sensorToAvatar); + } + } + + glm::mat4 defaultHeadOffset = createMatFromQuatAndPos(-DEFAULT_AVATAR_HEAD_ROT, -DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET); + _poseStateMap[controller::HEAD] = _context->_lastHeadPose.postTransform(defaultHeadOffset).transform(sensorToAvatar); + + std::map axesToUpdate[2] = { + { + { controller::LX, "/input/thumbstick/x" }, + { controller::LY, "/input/thumbstick/y" }, + { controller::LT, "/input/trigger/value" }, + }, + { + { controller::RX, "/input/thumbstick/x" }, + { controller::RY, "/input/thumbstick/y" }, + { controller::RT, "/input/trigger/value" }, + }, + }; + + for (uint32_t i = 0; i < HAND_COUNT; i++) { + for (const auto& channelAndPath : axesToUpdate[i]) { + _axisStateMap[channelAndPath.first].value = _actions.at(channelAndPath.second)->getFloat(i).currentState; + + // if (_axisStateMap[channelAndPath.first].value != 0) { + // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d): %f", i, channelAndPath.second.c_str(), channelAndPath.first, + // (double)_axisStateMap[channelAndPath.first].value); + // } + } + } + + // TODO: Figure out why LEFT_APP_MENU is misssing in StandardButtonChannel + std::map buttonsToUpdate[2] = { + { + { controller::X, "/input/a/click" }, + { controller::Y, "/input/b/click" }, + { controller::LT_CLICK, "/input/trigger/click" }, + //{ LEFT_APP_MENU, "/input/system/click" }, + }, + { + { controller::A, "/input/a/click" }, + { controller::B, "/input/b/click" }, + { controller::RT_CLICK, "/input/trigger/click" }, + //{ RIGHT_APP_MENU, "/input/system/click" }, + }, + }; + + for (uint32_t i = 0; i < HAND_COUNT; i++) { + for (const auto& channelAndPath : buttonsToUpdate[i]) { + if (_actions.at(channelAndPath.second)->getBool(i).currentState == XR_TRUE) { + _buttonPressedMap.insert(channelAndPath.first); + // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d)", i, channelAndPath.second.c_str(), channelAndPath.first); + } + } + } +} \ No newline at end of file diff --git a/plugins/openxr/src/OpenXrInputPlugin.h b/plugins/openxr/src/OpenXrInputPlugin.h new file mode 100644 index 00000000000..f1a578867da --- /dev/null +++ b/plugins/openxr/src/OpenXrInputPlugin.h @@ -0,0 +1,104 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include "plugins/InputPlugin.h" +#include "controllers/InputDevice.h" +#include "OpenXrContext.h" + +#define HAND_COUNT 2 + +class OpenXrInputPlugin : public InputPlugin { + Q_OBJECT +public: + OpenXrInputPlugin(std::shared_ptr c); + bool isSupported() const override; + const QString getName() const override { return "OpenXR"; } + + bool isHandController() const override { return true; } + bool configurable() override { return true; } + + QString configurationLayout() override; + void setConfigurationSettings(const QJsonObject configurationSettings) override; + QJsonObject configurationSettings() override; + void calibrate() override; + bool uncalibrate() override; + bool isHeadController() const override { return true; } + + bool activate() override; + void deactivate() override; + + QString getDeviceName() override { return _context.get()->_systemName; } + + void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + + void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + + virtual void saveSettings() const override; + virtual void loadSettings() override; + +private: + class Action { + public: + Action(std::shared_ptr c, XrActionType type, const std::string& path) { + _context = c; + _path = path; + _type = type; + } + + bool init(XrActionSet actionSet); + std::vector getBindings(); + XrActionStateFloat getFloat(uint32_t handId); + XrActionStateBoolean getBool(uint32_t handId); + XrSpaceLocation getPose(uint32_t handId); + bool applyHaptic(uint32_t handId, XrDuration duration, float frequency, float amplitude); + + private: + bool createPoseSpaces(); + XrAction _action = XR_NULL_HANDLE; + std::shared_ptr _context; + std::string _path; + XrActionType _type; + XrSpace _poseSpaces[HAND_COUNT] = { XR_NULL_HANDLE, XR_NULL_HANDLE }; + }; + + class InputDevice : public controller::InputDevice { + public: + InputDevice(std::shared_ptr c); + + private: + controller::Input::NamedVector getAvailableInputs() const override; + QString getDefaultMappingConfig() const override; + void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + void focusOutEvent() override; + bool triggerHapticPulse(float strength, float duration, uint16_t index) override; + + mutable std::recursive_mutex _lock; + template + void withLock(F&& f) { + std::unique_lock locker(_lock); + f(); + } + + friend class OpenXrInputPlugin; + + uint32_t _trackedControllers = 0; + XrActionSet _actionSet; + std::map> _actions; + std::shared_ptr _context; + bool _actionsInitialized = false; + + bool initActions(); + bool initBindings(const std::string& profileName, const std::vector& actionsToBind); + }; + + bool _registeredWithInputMapper = false; + std::shared_ptr _context; + std::shared_ptr _inputDevice; +}; diff --git a/plugins/openxr/src/OpenXrProvider.cpp b/plugins/openxr/src/OpenXrProvider.cpp new file mode 100644 index 00000000000..c2a87dff670 --- /dev/null +++ b/plugins/openxr/src/OpenXrProvider.cpp @@ -0,0 +1,59 @@ +// +// Overte OpenXR Plugin +// +// Copyright 2024 Lubosz Sarnecki +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include "plugins/RuntimePlugin.h" +#include "OpenXrDisplayPlugin.h" +#include "OpenXrInputPlugin.h" + +class OpenXrProvider : public QObject, public DisplayProvider, InputProvider { + Q_OBJECT + Q_PLUGIN_METADATA(IID DisplayProvider_iid FILE "plugin.json") + Q_INTERFACES(DisplayProvider) + Q_PLUGIN_METADATA(IID InputProvider_iid FILE "plugin.json") + Q_INTERFACES(InputProvider) + +public: + OpenXrProvider(QObject* parent = nullptr) : QObject(parent) {} + virtual ~OpenXrProvider() {} + std::shared_ptr context = std::make_shared(); + + virtual DisplayPluginList getDisplayPlugins() override { + static std::once_flag once; + std::call_once(once, [&] { + DisplayPluginPointer plugin(std::make_shared(context)); + if (plugin->isSupported()) { + _displayPlugins.push_back(plugin); + } + }); + + return _displayPlugins; + } + + virtual InputPluginList getInputPlugins() override { + static std::once_flag once; + + std::call_once(once, [&] { + InputPluginPointer plugin(std::make_shared(context)); + if (plugin->isSupported()) { + _inputPlugins.push_back(plugin); + } + }); + + return _inputPlugins; + } + + virtual void destroyInputPlugins() override { _inputPlugins.clear(); } + + virtual void destroyDisplayPlugins() override { _displayPlugins.clear(); } + +private: + DisplayPluginList _displayPlugins; + InputPluginList _inputPlugins; +}; + +#include "OpenXrProvider.moc" diff --git a/plugins/openxr/src/plugin.json b/plugins/openxr/src/plugin.json new file mode 100644 index 00000000000..5a3df6e7360 --- /dev/null +++ b/plugins/openxr/src/plugin.json @@ -0,0 +1,4 @@ +{ + "name":"OpenXR", + "version":1 +} From 3dd7dcc91bf69cdc877f5a553593620e995bbdfb Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Thu, 14 Mar 2024 16:31:20 +0100 Subject: [PATCH 05/20] OpenXrContext: Improve errors when runtime is not available. --- plugins/openxr/src/OpenXrContext.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/plugins/openxr/src/OpenXrContext.cpp b/plugins/openxr/src/OpenXrContext.cpp index 700d4887470..9b50057362e 100644 --- a/plugins/openxr/src/OpenXrContext.cpp +++ b/plugins/openxr/src/OpenXrContext.cpp @@ -49,11 +49,14 @@ static bool initFunctionPointers(XrInstance instance) { OpenXrContext::OpenXrContext() { _isSupported = initPreGraphics(); if (!_isSupported) { - qCCritical(xr_context_cat, "Pre graphics init failed."); + qCWarning(xr_context_cat, "OpenXR is not supported."); } } OpenXrContext::~OpenXrContext() { + if (_instance == XR_NULL_HANDLE) { + return; + } XrResult res = xrDestroyInstance(_instance); if (res != XR_SUCCESS) { qCCritical(xr_context_cat, "Failed to destroy OpenXR instance"); @@ -65,7 +68,13 @@ bool OpenXrContext::initInstance() { uint32_t count = 0; XrResult result = xrEnumerateInstanceExtensionProperties(nullptr, 0, &count, nullptr); - if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate number of extension properties")) + // Since this is the first OpenXR call we do, check here if RUNTIME_UNAVAILABLE is returned. + if (result == XR_ERROR_RUNTIME_UNAVAILABLE) { + qCCritical(xr_context_cat, "XR_ERROR_RUNTIME_UNAVAILABLE: Is XR_RUNTIME_JSON set correctly?"); + return false; + } + + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate number of extensions.")) return false; std::vector properties; @@ -75,7 +84,7 @@ bool OpenXrContext::initInstance() { } result = xrEnumerateInstanceExtensionProperties(nullptr, count, &count, properties.data()); - if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate extension properties")) + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to enumerate extensions.")) return false; bool openglSupported = false; @@ -109,7 +118,13 @@ bool OpenXrContext::initInstance() { }; result = xrCreateInstance(&info, &_instance); - if (!xrCheck(XR_NULL_HANDLE, result, "Failed to create XR instance.")) + + if (result == XR_ERROR_RUNTIME_FAILURE) { + qCCritical(xr_context_cat, "XR_ERROR_RUNTIME_FAILURE: Is the OpenXR runtime up and running?"); + return false; + } + + if (!xrCheck(XR_NULL_HANDLE, result, "Failed to create OpenXR instance.")) return false; if (!initFunctionPointers(_instance)) From eda123aca7fb1da028570c7978efa3b1e6cc9254 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sat, 16 Mar 2024 15:55:42 +0100 Subject: [PATCH 06/20] OpenGlDisplayPlugin: Add possibility to present frames only once. Don't present frame more than once in OpenXrDisplayPlugin. --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 7 +++++++ .../src/display-plugins/OpenGLDisplayPlugin.h | 2 ++ plugins/openxr/src/OpenXrDisplayPlugin.cpp | 1 + 3 files changed, 10 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 6ea10f6327a..93f1b933d82 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -746,6 +746,13 @@ void OpenGLDisplayPlugin::present(const std::shared_ptr& } gpu::Backend::freeGPUMemSize.set(gpu::gl::getFreeDedicatedMemory()); + + // Drop current frame after presenting it once. + // This is required for the OpenXR frame cycle, since we call xrEndFrame after presenting. + // xrEndFrame must not be called multiple times. + if (_presentOnlyOnce) { + _currentFrame.reset(); + } } else if (alwaysPresent()) { refreshRateController->clockEndTime(); internalPresent(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 0df0d9ac3e8..a7ce2180f21 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -206,6 +206,8 @@ class OpenGLDisplayPlugin : public DisplayPlugin { QImage getScreenshot(float aspectRatio); QImage getSecondaryCameraScreenshot(); + bool _presentOnlyOnce = false; + private: static Setting::Handle _extraLinearToSRGBConversionSetting; }; diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.cpp b/plugins/openxr/src/OpenXrDisplayPlugin.cpp index e952fe671cd..e15de872dd2 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.cpp +++ b/plugins/openxr/src/OpenXrDisplayPlugin.cpp @@ -23,6 +23,7 @@ constexpr GLint XR_PREFERRED_COLOR_FORMAT = GL_SRGB8_ALPHA8; OpenXrDisplayPlugin::OpenXrDisplayPlugin(std::shared_ptr c) { _context = c; + _presentOnlyOnce = true; } bool OpenXrDisplayPlugin::isSupported() const { From 6d84e25b0d01aba76e7ba69c1d302adb410f08af Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Wed, 13 Mar 2024 01:41:18 +0100 Subject: [PATCH 07/20] OpenXrInput: Improve mapping for the Index controller. * Add thumbstick click and touch. * Add face and trigger touch. Use left primary / secondary instead of face button names. --- .../resources/controllers/openxr_index.json | 15 +++--- plugins/openxr/src/OpenXrInputPlugin.cpp | 48 +++++++++++++++---- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/interface/resources/controllers/openxr_index.json b/interface/resources/controllers/openxr_index.json index 774b55981ea..15ee5b5d3bc 100644 --- a/interface/resources/controllers/openxr_index.json +++ b/interface/resources/controllers/openxr_index.json @@ -5,15 +5,10 @@ { "from": "Index.RightHand", "to": "Standard.RightHand" }, { "from": "Index.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }, - { "from": "Index.A", "to": "Standard.RightPrimaryThumb", "peek": true }, - { "from": "Index.B", "to": "Standard.RightSecondaryThumb", "peek": true }, - { "from": "Index.X", "to": "Standard.LeftPrimaryThumb", "peek": true }, - { "from": "Index.Y", "to": "Standard.LeftSecondaryThumb", "peek": true}, - - { "from": "Index.A", "to": "Standard.A" }, - { "from": "Index.B", "to": "Standard.B" }, - { "from": "Index.X", "to": "Standard.X" }, - { "from": "Index.Y", "to": "Standard.Y" }, + { "from": "Index.RightPrimaryThumb", "to": "Standard.RightPrimaryThumb" }, + { "from": "Index.RightSecondaryThumb", "to": "Standard.RightSecondaryThumb" }, + { "from": "Index.LeftPrimaryThumb", "to": "Standard.LeftPrimaryThumb" }, + { "from": "Index.LeftSecondaryThumb", "to": "Standard.LeftSecondaryThumb" }, { "from": "Index.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, { "from": "Index.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, @@ -27,6 +22,8 @@ { "from": "Index.LX", "to": "Standard.LX" }, { "from": "Index.RY", "to": "Standard.RY" }, { "from": "Index.RX", "to": "Standard.RX" }, + { "from": "Index.LS", "to": "Standard.LS" }, + { "from": "Index.RS", "to": "Standard.RS" }, { "from": "Index.LSTouch", "to": "Standard.LSTouch" }, { "from": "Index.RSTouch", "to": "Standard.RSTouch" }, diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp index f5eaabff0b3..58ab8808147 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.cpp +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -302,18 +302,29 @@ controller::Input::NamedVector OpenXrInputPlugin::InputDevice::getAvailableInput // Sticks makePair(LX, "LX"), makePair(LY, "LY"), + makePair(LS, "LS"), + makePair(LS_TOUCH, "LSTouch"), makePair(RX, "RX"), makePair(RY, "RY"), + makePair(RS, "RS"), + makePair(RS_TOUCH, "RSTouch"), // Face buttons - makePair(A, "A"), - makePair(B, "B"), - makePair(X, "X"), - makePair(Y, "Y"), + makePair(RIGHT_PRIMARY_THUMB, "RightPrimaryThumb"), + makePair(RIGHT_PRIMARY_THUMB_TOUCH, "RightPrimaryThumbTouch"), + makePair(RIGHT_SECONDARY_THUMB, "RightSecondaryThumb"), + makePair(RIGHT_SECONDARY_THUMB_TOUCH, "RightSecondaryThumbTouch"), + + makePair(LEFT_PRIMARY_THUMB, "LeftPrimaryThumb"), + makePair(LEFT_PRIMARY_THUMB_TOUCH, "LeftPrimaryThumbTouch"), + makePair(LEFT_SECONDARY_THUMB, "LeftSecondaryThumb"), + makePair(LEFT_SECONDARY_THUMB_TOUCH, "LeftSecondaryThumbTouch"), // Triggers makePair(RT, "RT"), makePair(LT, "LT"), makePair(RT_CLICK, "RTClick"), makePair(LT_CLICK, "LTClick"), + makePair(LEFT_PRIMARY_INDEX_TOUCH, "LeftPrimaryIndexTouch"), + makePair(RIGHT_PRIMARY_INDEX_TOUCH, "RightPrimaryIndexTouch"), // Menu buttons // TODO: Add this to button channel // Input::NamedPair(Input(_deviceID, LEFT_APP_MENU, ChannelType::BUTTON), "LeftApplicationMenu"), @@ -350,10 +361,15 @@ bool OpenXrInputPlugin::InputDevice::initActions() { std::map actionsToInit = { { "/input/thumbstick/x", XR_ACTION_TYPE_FLOAT_INPUT }, { "/input/thumbstick/y", XR_ACTION_TYPE_FLOAT_INPUT }, + { "/input/thumbstick/touch", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/thumbstick/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, { "/input/a/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/a/touch", XR_ACTION_TYPE_BOOLEAN_INPUT }, { "/input/b/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/b/touch", XR_ACTION_TYPE_BOOLEAN_INPUT }, { "/input/trigger/value", XR_ACTION_TYPE_FLOAT_INPUT }, { "/input/trigger/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, + { "/input/trigger/touch", XR_ACTION_TYPE_BOOLEAN_INPUT }, { "/output/haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT }, { "/input/grip/pose", XR_ACTION_TYPE_POSE_INPUT }, { "/input/select/click", XR_ACTION_TYPE_BOOLEAN_INPUT }, @@ -387,9 +403,15 @@ bool OpenXrInputPlugin::InputDevice::initActions() { "/input/grip/pose", "/input/thumbstick/x", "/input/thumbstick/y", + "/input/thumbstick/touch", + "/input/thumbstick/click", "/input/a/click", + "/input/a/touch", "/input/b/click", + "/input/b/touch", "/input/trigger/value", + "/input/trigger/click", + "/input/trigger/touch", "/output/haptic", "/input/system/click", }; @@ -499,15 +521,25 @@ void OpenXrInputPlugin::InputDevice::update(float deltaTime, const controller::I // TODO: Figure out why LEFT_APP_MENU is misssing in StandardButtonChannel std::map buttonsToUpdate[2] = { { - { controller::X, "/input/a/click" }, - { controller::Y, "/input/b/click" }, + { controller::LEFT_PRIMARY_THUMB, "/input/a/click" }, + { controller::LEFT_PRIMARY_THUMB_TOUCH, "/input/a/touch" }, + { controller::LEFT_SECONDARY_THUMB, "/input/b/click" }, + { controller::LEFT_SECONDARY_THUMB_TOUCH, "/input/b/touch" }, { controller::LT_CLICK, "/input/trigger/click" }, + { controller::LEFT_PRIMARY_INDEX_TOUCH, "/input/trigger/touch" }, + { controller::LS, "/input/thumbstick/click" }, + { controller::LS_TOUCH, "/input/thumbstick/touch" }, //{ LEFT_APP_MENU, "/input/system/click" }, }, { - { controller::A, "/input/a/click" }, - { controller::B, "/input/b/click" }, + { controller::RIGHT_PRIMARY_THUMB, "/input/a/click" }, + { controller::RIGHT_PRIMARY_THUMB_TOUCH, "/input/a/touch" }, + { controller::RIGHT_SECONDARY_THUMB, "/input/b/click" }, + { controller::RIGHT_SECONDARY_THUMB_TOUCH, "/input/b/touch" }, { controller::RT_CLICK, "/input/trigger/click" }, + { controller::RIGHT_PRIMARY_INDEX_TOUCH, "/input/trigger/touch" }, + { controller::RS, "/input/thumbstick/click" }, + { controller::RS_TOUCH, "/input/thumbstick/touch" }, //{ RIGHT_APP_MENU, "/input/system/click" }, }, }; From fd99eb1cb288c527deed9e2c5b03335ddf95dbf3 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 17 Mar 2024 10:39:41 +0100 Subject: [PATCH 08/20] controllers: openxr: Use actions for walking. This makes the Y thumbstick axis work. --- interface/resources/controllers/openxr_index.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/controllers/openxr_index.json b/interface/resources/controllers/openxr_index.json index 15ee5b5d3bc..7a1199e1a0c 100644 --- a/interface/resources/controllers/openxr_index.json +++ b/interface/resources/controllers/openxr_index.json @@ -18,8 +18,8 @@ { "from": "Index.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, { "from": "Index.RightThumbUp", "to": "Standard.RightThumbUp" }, - { "from": "Index.LY", "to": "Standard.LY" }, - { "from": "Index.LX", "to": "Standard.LX" }, + { "from": "Index.LY", "to": "Actions.TranslateZ", "filters": ["invert"] }, + { "from": "Index.LX", "to": "Actions.TranslateX" }, { "from": "Index.RY", "to": "Standard.RY" }, { "from": "Index.RX", "to": "Standard.RX" }, { "from": "Index.LS", "to": "Standard.LS" }, From 05ca2512d8d47807dd6d140abaec470542fe3e06 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 17 Mar 2024 11:09:52 +0100 Subject: [PATCH 09/20] controllers: Improve openxr_index.json mapping. --- .../resources/controllers/openxr_index.json | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/interface/resources/controllers/openxr_index.json b/interface/resources/controllers/openxr_index.json index 7a1199e1a0c..44e1ae0a97d 100644 --- a/interface/resources/controllers/openxr_index.json +++ b/interface/resources/controllers/openxr_index.json @@ -1,29 +1,26 @@ { - "name": "OpenXR Index to Standard", + "name": "OpenXR Index to Actions", "channels": [ { "from": "Index.LeftHand", "to": "Standard.LeftHand" }, { "from": "Index.RightHand", "to": "Standard.RightHand" }, { "from": "Index.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }, - { "from": "Index.RightPrimaryThumb", "to": "Standard.RightPrimaryThumb" }, - { "from": "Index.RightSecondaryThumb", "to": "Standard.RightSecondaryThumb" }, - { "from": "Index.LeftPrimaryThumb", "to": "Standard.LeftPrimaryThumb" }, - { "from": "Index.LeftSecondaryThumb", "to": "Standard.LeftSecondaryThumb" }, - + { "from": "Index.LeftPrimaryThumb", "to": "Actions.Down" }, { "from": "Index.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, + { "from": "Index.LeftSecondaryThumb", "to": "Actions.ContextMenu" }, { "from": "Index.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, - { "from": "Index.LeftThumbUp", "to": "Standard.LeftThumbUp" }, + { "from": "Index.RightPrimaryThumb", "to": "Actions.Up" }, { "from": "Index.RightPrimaryThumbTouch", "to": "Standard.RightPrimaryThumbTouch" }, + { "from": "Index.RightSecondaryThumb", "to": "Actions.Sprint" }, { "from": "Index.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, - { "from": "Index.RightThumbUp", "to": "Standard.RightThumbUp" }, { "from": "Index.LY", "to": "Actions.TranslateZ", "filters": ["invert"] }, { "from": "Index.LX", "to": "Actions.TranslateX" }, { "from": "Index.RY", "to": "Standard.RY" }, { "from": "Index.RX", "to": "Standard.RX" }, { "from": "Index.LS", "to": "Standard.LS" }, - { "from": "Index.RS", "to": "Standard.RS" }, + { "from": "Index.RS", "to": "Actions.CycleCamera" }, { "from": "Index.LSTouch", "to": "Standard.LSTouch" }, { "from": "Index.RSTouch", "to": "Standard.RSTouch" }, @@ -31,12 +28,8 @@ { "from": "Index.LT", "to": "Standard.LT" }, { "from": "Index.RTClick", "to": "Standard.RTClick" }, { "from": "Index.LTClick", "to": "Standard.LTClick" }, - { "from": "Index.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, - { "from": "Index.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" }, - { "from": "Index.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, - { "from": "Index.RightIndexPoint", "to": "Standard.RightIndexPoint" }, - { "from": "Index.LeftApplicationMenu", "to": "Standard.Back" }, - { "from": "Index.RightApplicationMenu", "to": "Standard.Start" } + { "from": "Index.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, + { "from": "Index.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" } ] } From 29e530506b5bf79cf9216cdf99acf23a34db70e8 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 17 Mar 2024 14:10:00 +0100 Subject: [PATCH 10/20] OpenXrInputPlugin: Improve haptic feedback mapping. --- plugins/openxr/src/OpenXrInputPlugin.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp index 58ab8808147..04582ae92f7 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.cpp +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -115,8 +115,15 @@ bool OpenXrInputPlugin::InputDevice::triggerHapticPulse(float strength, float du std::unique_lock locker(_lock); - // TODO: convert duration and strength to openxr values. - if (!_actions.at("/output/haptic")->applyHaptic(0, XR_MIN_HAPTIC_DURATION, XR_FREQUENCY_UNSPECIFIED, 0.5f)) { + // TODO: Haptic values in overte are always strengh 1.0 and duration only 13.0 or 16.0. So it's not really used. + // The duration does not seem to map to a time unit. 16ms seems quite short for a haptic vibration. + // Let's assume the duration is in 10 milliseconds. + // Let's also assume strength 1.0 is the middle value, which is 0.5 in OpenXR. + using namespace std::chrono; + nanoseconds durationNs = duration_cast(milliseconds(static_cast(duration * 10.0f))); + XrDuration xrDuration = durationNs.count(); + + if (!_actions.at("/output/haptic")->applyHaptic(index, xrDuration, XR_FREQUENCY_UNSPECIFIED, 0.5f * strength)) { qCCritical(xr_input_cat, "Failed to apply haptic feedback!"); } From dbd73058dd387d6010407915af35cc99850ee01b Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sun, 17 Mar 2024 22:43:40 +0100 Subject: [PATCH 11/20] OpenXrContext: Add Windows platform and bindings. --- plugins/openxr/src/OpenXrContext.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/plugins/openxr/src/OpenXrContext.cpp b/plugins/openxr/src/OpenXrContext.cpp index 9b50057362e..127ab7ae9a0 100644 --- a/plugins/openxr/src/OpenXrContext.cpp +++ b/plugins/openxr/src/OpenXrContext.cpp @@ -11,9 +11,15 @@ #include -#include +#if defined(Q_OS_LINUX) + #include + #define XR_USE_PLATFORM_XLIB +#elif defined(Q_OS_WIN) + #define XR_USE_PLATFORM_WIN32 +#else + #error "Unsupported platform" +#endif -#define XR_USE_PLATFORM_XLIB #define XR_USE_GRAPHICS_API_OPENGL #include #include @@ -178,14 +184,22 @@ bool OpenXrContext::requestExitSession() { } bool OpenXrContext::initSession() { - // TODO: Make cross platform +#if defined(Q_OS_LINUX) XrGraphicsBindingOpenGLXlibKHR binding = { .type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR, .xDisplay = XOpenDisplay(nullptr), .glxDrawable = glXGetCurrentDrawable(), .glxContext = glXGetCurrentContext(), }; - +#elif defined(Q_OS_WIN) + XrGraphicsBindingOpenGLWin32KHR binding = { + .type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR, + .hDC = wglGetCurrentDC(), + .hGLRC = wglGetCurrentContext(), + }; +#else + #error "Unsupported platform" +#endif XrSessionCreateInfo info = { .type = XR_TYPE_SESSION_CREATE_INFO, .next = &binding, From 5f1f4144761fc4ca8632b4bf001c2207329ad5cd Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sat, 25 May 2024 11:56:45 +0200 Subject: [PATCH 12/20] ports: Add openxr-loader. Copy from vcpkg repository. Add OpenXR to hifi-client-deps. --- cmake/ports/hifi-client-deps/CONTROL | 2 +- cmake/ports/openxr-loader/fix-jinja2.patch | 23 ++++++ .../fix-openxr-sdk-jsoncpp.patch | 30 +++++++ cmake/ports/openxr-loader/portfile.cmake | 79 +++++++++++++++++++ .../python3_8_compatibility.patch | 13 +++ cmake/ports/openxr-loader/vcpkg.json | 27 +++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 cmake/ports/openxr-loader/fix-jinja2.patch create mode 100644 cmake/ports/openxr-loader/fix-openxr-sdk-jsoncpp.patch create mode 100644 cmake/ports/openxr-loader/portfile.cmake create mode 100644 cmake/ports/openxr-loader/python3_8_compatibility.patch create mode 100644 cmake/ports/openxr-loader/vcpkg.json diff --git a/cmake/ports/hifi-client-deps/CONTROL b/cmake/ports/hifi-client-deps/CONTROL index afee6a5d487..c951768a4d9 100644 --- a/cmake/ports/hifi-client-deps/CONTROL +++ b/cmake/ports/hifi-client-deps/CONTROL @@ -1,4 +1,4 @@ Source: hifi-client-deps Version: 0.1 Description: Collected dependencies for High Fidelity applications -Build-Depends: hifi-deps, aristo (windows), glslang, liblo (windows), nlohmann-json, openvr ((linux&!arm)|windows), quazip (!android), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), sranipal (windows), vulkanmemoryallocator, discord-rpc (!android) +Build-Depends: hifi-deps, aristo (windows), glslang, liblo (windows), nlohmann-json, openvr ((linux&!arm)|windows), openxr-loader, quazip (!android), sdl2 (!android), spirv-cross (!android), spirv-tools (!android), sranipal (windows), vulkanmemoryallocator, discord-rpc (!android) diff --git a/cmake/ports/openxr-loader/fix-jinja2.patch b/cmake/ports/openxr-loader/fix-jinja2.patch new file mode 100644 index 00000000000..5d77cb4e461 --- /dev/null +++ b/cmake/ports/openxr-loader/fix-jinja2.patch @@ -0,0 +1,23 @@ +From d80c7dc3f4810fc49e4444590d39ef71e8a9b01c Mon Sep 17 00:00:00 2001 +From: Adam Johnson +Date: Sat, 19 Feb 2022 19:42:31 -0500 +Subject: [PATCH] Fix bad import in jinja2 + +--- + external/python/jinja2/utils.py | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/external/python/jinja2/utils.py b/external/python/jinja2/utils.py +index db9c5d06..f198e3ef 100644 +--- a/external/python/jinja2/utils.py ++++ b/external/python/jinja2/utils.py +@@ -639,4 +639,8 @@ def __repr__(self): + + + # Imported here because that's where it was in the past +-from markupsafe import Markup, escape, soft_unicode ++from markupsafe import Markup, escape ++try: ++ from markupsafe import soft_unicode ++except ImportError: ++ from markupsafe import soft_str as soft_unicode diff --git a/cmake/ports/openxr-loader/fix-openxr-sdk-jsoncpp.patch b/cmake/ports/openxr-loader/fix-openxr-sdk-jsoncpp.patch new file mode 100644 index 00000000000..758d55e0f83 --- /dev/null +++ b/cmake/ports/openxr-loader/fix-openxr-sdk-jsoncpp.patch @@ -0,0 +1,30 @@ +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index c75b145..386494c 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -89,7 +89,7 @@ if(NOT VULKAN_INCOMPATIBLE) + endif() + + find_package(Threads REQUIRED) +-find_package(JsonCpp) ++find_package(jsoncpp CONFIG REQUIRED) + + ### All options defined here + option(BUILD_LOADER "Build loader" ON) +diff --git a/src/loader/CMakeLists.txt b/src/loader/CMakeLists.txt +index 6a88cf4..0821a3d 100644 +--- a/src/loader/CMakeLists.txt ++++ b/src/loader/CMakeLists.txt +@@ -68,7 +68,11 @@ add_library(openxr_loader ${LIBRARY_TYPE} + ${openxr_loader_RESOURCE_FILE} + ) + if(BUILD_WITH_SYSTEM_JSONCPP) +- target_link_libraries(openxr_loader PRIVATE JsonCpp::JsonCpp) ++ if(BUILD_SHARED_LIBS) ++ target_link_libraries(openxr_loader PRIVATE jsoncpp_lib) ++ else() ++ target_link_libraries(openxr_loader PRIVATE jsoncpp_static) ++ endif() + else() + target_sources(openxr_loader + PRIVATE diff --git a/cmake/ports/openxr-loader/portfile.cmake b/cmake/ports/openxr-loader/portfile.cmake new file mode 100644 index 00000000000..4d1127e56e6 --- /dev/null +++ b/cmake/ports/openxr-loader/portfile.cmake @@ -0,0 +1,79 @@ + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO KhronosGroup/OpenXR-SDK + REF "release-${VERSION}" + SHA512 6efc7596e707f95366dbcdbac9bd7d0c20735a2175b4edf56a9e8a112cf0ab8b664069fe942313164a37119032ddbf5671bc88ab5f276005dd36e4a4dabba1c7 + HEAD_REF master + PATCHES + fix-openxr-sdk-jsoncpp.patch +) + +vcpkg_from_github( + OUT_SOURCE_PATH SDK_SOURCE_PATH + REPO KhronosGroup/OpenXR-SDK-Source + REF "release-${VERSION}" + SHA512 04bdb0f16078209b5edd175a3396f70e1ceb8cfa382c65b8fda388e565480e3844daf68e0d987e72ed8c21d3148af0b41a2170911ec1660565887e0e5ae6d2bf + HEAD_REF master + PATCHES + fix-openxr-sdk-jsoncpp.patch + fix-jinja2.patch +) + +vcpkg_from_github( + OUT_SOURCE_PATH HPP_SOURCE_PATH + REPO KhronosGroup/OpenXR-hpp + REF 63db9919822f8af6f7bf7416ba6a015d4617202e + SHA512 9e768f485d1631f8e74f35f028a64e2d64e33d362c53ae1c54427a10786e3befdd24089927319aa1a4b4c3e010247bd6cb3394bcee460c467c637ab6bc7bec90 + HEAD_REF master + PATCHES + python3_8_compatibility.patch +) + +# Weird behavior inside the OpenXR loader. On Windows they force shared libraries to use static crt, and +# vice-versa. Might be better in future iterations to patch the CMakeLists.txt for OpenXR +if (VCPKG_TARGET_IS_UWP OR VCPKG_TARGET_IS_WINDOWS) + if(VCPKG_LIBRARY_LINKAGE STREQUAL static) + set(DYNAMIC_LOADER OFF) + set(VCPKG_CRT_LINKAGE dynamic) + else() + set(DYNAMIC_LOADER ON) + set(VCPKG_CRT_LINKAGE static) + endif() +endif() + +vcpkg_find_acquire_program(PYTHON3) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + -DBUILD_API_LAYERS=OFF + -DBUILD_TESTS=OFF + -DBUILD_CONFORMANCE_TESTS=OFF + -DDYNAMIC_LOADER=${DYNAMIC_LOADER} + -DPYTHON_EXECUTABLE="${PYTHON3}" + -DBUILD_WITH_SYSTEM_JSONCPP=ON +) + +vcpkg_cmake_install() + +# Generate the OpenXR C++ bindings +set(ENV{OPENXR_REPO} "${SDK_SOURCE_PATH}") +vcpkg_execute_required_process( + COMMAND ${PYTHON3} "${HPP_SOURCE_PATH}/scripts/hpp_genxr.py" -quiet -registry "${SDK_SOURCE_PATH}/specification/registry/xr.xml" -o "${CURRENT_PACKAGES_DIR}/include/openxr" + WORKING_DIRECTORY "${HPP_SOURCE_PATH}" + LOGNAME "openxr-hpp" +) + +if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_cmake_config_fixup(PACKAGE_NAME OpenXR CONFIG_PATH cmake) +else() + vcpkg_cmake_config_fixup(PACKAGE_NAME OpenXR CONFIG_PATH lib/cmake/openxr) +endif() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") + +vcpkg_fixup_pkgconfig() +vcpkg_copy_pdbs() +file(INSTALL "${SOURCE_PATH}/LICENSE" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) diff --git a/cmake/ports/openxr-loader/python3_8_compatibility.patch b/cmake/ports/openxr-loader/python3_8_compatibility.patch new file mode 100644 index 00000000000..657bb2b7ab8 --- /dev/null +++ b/cmake/ports/openxr-loader/python3_8_compatibility.patch @@ -0,0 +1,13 @@ +diff --git a/scripts/hpp_genxr.py b/scripts/hpp_genxr.py +index ce419b0..23e1d3d 100644 +--- a/scripts/hpp_genxr.py ++++ b/scripts/hpp_genxr.py +@@ -36,7 +36,7 @@ from xrconventions import OpenXRConventions + from data import EXCLUDED_EXTENSIONS + + +-def makeREstring(strings: Iterable[str], default: typing.Optional[str] = None) -> str: ++def makeREstring(strings, default: typing.Optional[str] = None) -> str: + """Turn a list of strings into a regexp string matching exactly those strings.""" + if strings or default is None: + return f"^({'|'.join(re.escape(s) for s in strings)})$" diff --git a/cmake/ports/openxr-loader/vcpkg.json b/cmake/ports/openxr-loader/vcpkg.json new file mode 100644 index 00000000000..a45e3c9199f --- /dev/null +++ b/cmake/ports/openxr-loader/vcpkg.json @@ -0,0 +1,27 @@ +{ + "name": "openxr-loader", + "version": "1.0.31", + "description": "A royalty-free, open standard that provides high-performance access to Augmented Reality (AR) and Virtual Reality (VR)—collectively known as XR—platforms and devices", + "homepage": "https://github.com/KhronosGroup/OpenXR-SDK", + "license": "Apache-2.0", + "supports": "!uwp & !osx", + "dependencies": [ + "jsoncpp", + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + } + ], + "features": { + "vulkan": { + "description": "Vulkan functionality for OpenXR", + "dependencies": [ + "vulkan" + ] + } + } +} From 9e18985019d140cd980d2a6a4e1ed33fe8871983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Sun, 7 Jul 2024 15:55:20 +0200 Subject: [PATCH 13/20] OpenXr: Enable C++20 for OpenXR plugin. Enabling it globally causes issue with WebRTC. Co-authored-by: Lubosz Sarnecki --- plugins/openxr/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/openxr/CMakeLists.txt b/plugins/openxr/CMakeLists.txt index 2e7eb9f821e..9316a77f633 100644 --- a/plugins/openxr/CMakeLists.txt +++ b/plugins/openxr/CMakeLists.txt @@ -23,3 +23,5 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") # Silence GCC warnings target_compile_options(openxr PRIVATE -Wno-missing-field-initializers) endif() + +set_property(TARGET openxr PROPERTY CXX_STANDARD 20) \ No newline at end of file From 4e71b60ab8c034d8b89c57bb3f367627aa1dbec7 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Tue, 3 Sep 2024 23:20:56 +0200 Subject: [PATCH 14/20] GLMHelpers: Fix build with C++20 on GCC. When enabling C++20 the lerp function seems to be redefined on GCC (not on MSVC), don't redefine it using a CMake definition. --- libraries/shared/src/GLMHelpers.h | 2 ++ plugins/openxr/CMakeLists.txt | 3 +++ 2 files changed, 5 insertions(+) diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index cfb4bb63983..c74820e4a05 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -212,9 +212,11 @@ T toNormalizedDeviceScale(const T& value, const T& size) { #define ROLL(euler) euler.z // float - linear interpolate +#if !defined(DONT_REDEFINE_LERP) inline float lerp(float x, float y, float a) { return x * (1.0f - a) + (y * a); } +#endif // vec2 lerp - linear interpolate template diff --git a/plugins/openxr/CMakeLists.txt b/plugins/openxr/CMakeLists.txt index 9316a77f633..c54d5efcc20 100644 --- a/plugins/openxr/CMakeLists.txt +++ b/plugins/openxr/CMakeLists.txt @@ -22,6 +22,9 @@ include_hifi_library_headers(script-engine) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") # Silence GCC warnings target_compile_options(openxr PRIVATE -Wno-missing-field-initializers) + + # Fix build issue where lerp is already defined on C++20 / GCC + target_compile_definitions(openxr PRIVATE -DDONT_REDEFINE_LERP) endif() set_property(TARGET openxr PROPERTY CXX_STANDARD 20) \ No newline at end of file From b6c3c722ff1275be4dc0e75cd718258426a83c26 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Fri, 9 Aug 2024 20:12:12 -0700 Subject: [PATCH 15/20] OpenXr: Platform improvements. Add missing includes on Windows. Include GLX only on Linux. Move openxr_platform.h include to OpenXrContext.h. To make this possible, certain names from GLX/X11 need to be undefined in order to be now includable in OpenXrInput.h. Add Overte e.V. copyright. Co-authored-by: Lubosz Sarnecki --- plugins/openxr/CMakeLists.txt | 1 + plugins/openxr/src/OpenXrContext.cpp | 14 +----------- plugins/openxr/src/OpenXrContext.h | 25 +++++++++++++++++++++- plugins/openxr/src/OpenXrDisplayPlugin.cpp | 8 ++++++- plugins/openxr/src/OpenXrDisplayPlugin.h | 10 +-------- plugins/openxr/src/OpenXrInputPlugin.cpp | 1 + plugins/openxr/src/OpenXrInputPlugin.h | 1 + plugins/openxr/src/OpenXrProvider.cpp | 1 + 8 files changed, 37 insertions(+), 24 deletions(-) diff --git a/plugins/openxr/CMakeLists.txt b/plugins/openxr/CMakeLists.txt index c54d5efcc20..847a41880ea 100644 --- a/plugins/openxr/CMakeLists.txt +++ b/plugins/openxr/CMakeLists.txt @@ -1,5 +1,6 @@ # # Copyright 2024 Lubosz Sarnecki +# Copyright 2024 Overte e.V. # # SPDX-License-Identifier: Apache-2.0 # diff --git a/plugins/openxr/src/OpenXrContext.cpp b/plugins/openxr/src/OpenXrContext.cpp index 127ab7ae9a0..2664b49bcf0 100644 --- a/plugins/openxr/src/OpenXrContext.cpp +++ b/plugins/openxr/src/OpenXrContext.cpp @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // @@ -11,19 +12,6 @@ #include -#if defined(Q_OS_LINUX) - #include - #define XR_USE_PLATFORM_XLIB -#elif defined(Q_OS_WIN) - #define XR_USE_PLATFORM_WIN32 -#else - #error "Unsupported platform" -#endif - -#define XR_USE_GRAPHICS_API_OPENGL -#include -#include - Q_DECLARE_LOGGING_CATEGORY(xr_context_cat) Q_LOGGING_CATEGORY(xr_context_cat, "openxr.context") diff --git a/plugins/openxr/src/OpenXrContext.h b/plugins/openxr/src/OpenXrContext.h index 387bab388ea..eed18eeeb8b 100644 --- a/plugins/openxr/src/OpenXrContext.h +++ b/plugins/openxr/src/OpenXrContext.h @@ -2,19 +2,42 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // #pragma once -#include "controllers/Pose.h" #include +#include "gpu/gl/GLBackend.h" + +#if defined(Q_OS_LINUX) + #define XR_USE_PLATFORM_XLIB + #include + // Unsorted from glx.h conflicts with qdir.h + #undef Unsorted + // MappingPointer from X11 conflicts with one from controllers/Forward.h + #undef MappingPointer +#elif defined(Q_OS_WIN) + #define XR_USE_PLATFORM_WIN32 + #include + #include +#else + #error "Unimplemented platform" +#endif + + +#define XR_USE_GRAPHICS_API_OPENGL +#include + #include #include +#include "controllers/Pose.h" + #define HAND_COUNT 2 constexpr XrPosef XR_INDENTITY_POSE = { diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.cpp b/plugins/openxr/src/OpenXrDisplayPlugin.cpp index e15de872dd2..fd9177143a1 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.cpp +++ b/plugins/openxr/src/OpenXrDisplayPlugin.cpp @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // @@ -16,6 +17,11 @@ #include #include +#if defined(Q_OS_WIN) +#undef near +#undef far +#endif + Q_DECLARE_LOGGING_CATEGORY(xr_display_cat) Q_LOGGING_CATEGORY(xr_display_cat, "openxr.display") @@ -264,7 +270,7 @@ void OpenXrDisplayPlugin::internalDeactivate() { // We can get into a state where activate -> deactivate -> activate is called in a chain. // We are probably gonna have a bad time then. At least check if the session is already running. // This happens when the application decides to switch display plugins back and forth. This should - // prbably be fixed there. + // probably be fixed there. if (_context->_isSessionRunning) { if (!_context->requestExitSession()) { qCCritical(xr_display_cat, "Failed to request exit session"); diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.h b/plugins/openxr/src/OpenXrDisplayPlugin.h index e36dba44f54..ac83c8e0940 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.h +++ b/plugins/openxr/src/OpenXrDisplayPlugin.h @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // @@ -13,15 +14,6 @@ #include "OpenXrContext.h" -#include "gpu/gl/GLBackend.h" - -#include - -#define XR_USE_PLATFORM_XLIB -#define XR_USE_GRAPHICS_API_OPENGL -#include -#include - class OpenXrDisplayPlugin : public HmdDisplayPlugin { public: OpenXrDisplayPlugin(std::shared_ptr c); diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp index 04582ae92f7..c16581efb56 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.cpp +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/plugins/openxr/src/OpenXrInputPlugin.h b/plugins/openxr/src/OpenXrInputPlugin.h index f1a578867da..a3c29794e0f 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.h +++ b/plugins/openxr/src/OpenXrInputPlugin.h @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/plugins/openxr/src/OpenXrProvider.cpp b/plugins/openxr/src/OpenXrProvider.cpp index c2a87dff670..ba23a882cba 100644 --- a/plugins/openxr/src/OpenXrProvider.cpp +++ b/plugins/openxr/src/OpenXrProvider.cpp @@ -2,6 +2,7 @@ // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki +// Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // From c844d38f2783b2411402149e350f1da593ee3108 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Wed, 4 Sep 2024 21:17:26 +0200 Subject: [PATCH 16/20] OpenXr: Use C++20 std::optional. --- plugins/openxr/src/OpenXrContext.h | 5 ++--- plugins/openxr/src/OpenXrDisplayPlugin.cpp | 15 ++++++--------- plugins/openxr/src/OpenXrDisplayPlugin.h | 4 +--- plugins/openxr/src/OpenXrInputPlugin.cpp | 4 ++-- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/plugins/openxr/src/OpenXrContext.h b/plugins/openxr/src/OpenXrContext.h index eed18eeeb8b..82baf098f50 100644 --- a/plugins/openxr/src/OpenXrContext.h +++ b/plugins/openxr/src/OpenXrContext.h @@ -9,6 +9,7 @@ #pragma once +#include #include @@ -58,9 +59,7 @@ class OpenXrContext { XrPath _handPaths[HAND_COUNT]; controller::Pose _lastHeadPose; - XrTime _lastPredictedDisplayTime; - // TODO: Enable C++17 and use std::optional - bool _lastPredictedDisplayTimeInitialized = false; + std::optional _lastPredictedDisplayTime; bool _shouldQuit = false; bool _shouldRunFrameCycle = false; diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.cpp b/plugins/openxr/src/OpenXrDisplayPlugin.cpp index fd9177143a1..7d89875573d 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.cpp +++ b/plugins/openxr/src/OpenXrDisplayPlugin.cpp @@ -67,13 +67,13 @@ inline static glm::mat4 fovToProjection(const XrFovf fov, const float near, cons } glm::mat4 OpenXrDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { - if (!_viewsInitialized) { + if (!_views.has_value()) { return baseProjection; } ViewFrustum frustum; frustum.setProjection(baseProjection); - return fovToProjection(_views[(eye == Left) ? 0 : 1].fov, frustum.getNearClip(), frustum.getFarClip()); + return fovToProjection(_views.value()[(eye == Left) ? 0 : 1].fov, frustum.getNearClip(), frustum.getFarClip()); } // TODO: This apparently wasn't right in the OpenVR plugin, but this is what it basically did. @@ -101,7 +101,7 @@ bool OpenXrDisplayPlugin::initViews() { for (uint32_t i = 0; i < _viewCount; i++) { XrView view = { .type = XR_TYPE_VIEW }; - _views.push_back(view); + _views.value().push_back(view); XrViewConfigurationView viewConfig = { .type = XR_TYPE_VIEW_CONFIGURATION_VIEW }; _viewConfigs.push_back(viewConfig); @@ -363,7 +363,6 @@ bool OpenXrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return false; _context->_lastPredictedDisplayTime = _lastFrameState.predictedDisplayTime; - _context->_lastPredictedDisplayTimeInitialized = true; std::vector eye_views(_viewCount); for (uint32_t i = 0; i < _viewCount; i++) { @@ -399,17 +398,15 @@ bool OpenXrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { .space = _context->_stageSpace, }; - result = xrLocateViews(_context->_session, &viewLocateInfo, &_lastViewState, _viewCount, &_viewCount, _views.data()); + result = xrLocateViews(_context->_session, &viewLocateInfo, &_lastViewState, _viewCount, &_viewCount, _views.value().data()); if (!xrCheck(_context->_instance, result, "Could not locate views")) return false; for (uint32_t i = 0; i < _viewCount; i++) { - _projectionLayerViews[i].pose = _views[i].pose; - _projectionLayerViews[i].fov = _views[i].fov; + _projectionLayerViews[i].pose = _views.value()[i].pose; + _projectionLayerViews[i].fov = _views.value()[i].fov; } - _viewsInitialized = true; - XrSpaceLocation headLocation = { .type = XR_TYPE_SPACE_LOCATION, .pose = XR_INDENTITY_POSE, diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.h b/plugins/openxr/src/OpenXrDisplayPlugin.h index ac83c8e0940..01df5fcf274 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.h +++ b/plugins/openxr/src/OpenXrDisplayPlugin.h @@ -66,9 +66,7 @@ class OpenXrDisplayPlugin : public HmdDisplayPlugin { uint32_t _viewCount = 0; std::vector _projectionLayerViews; - std::vector _views; - // TODO: Enable C++17 and use std::optional - bool _viewsInitialized = false; + std::optional> _views; std::vector _viewConfigs; diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp index c16581efb56..f9823adf70e 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.cpp +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -228,8 +228,8 @@ XrSpaceLocation OpenXrInputPlugin::Action::getPose(uint32_t handId) { .type = XR_TYPE_SPACE_LOCATION, }; - if (_context->_lastPredictedDisplayTimeInitialized) { - result = xrLocateSpace(_poseSpaces[handId], _context->_stageSpace, _context->_lastPredictedDisplayTime, &location); + if (_context->_lastPredictedDisplayTime.has_value()) { + result = xrLocateSpace(_poseSpaces[handId], _context->_stageSpace, _context->_lastPredictedDisplayTime.value(), &location); xrCheck(_context->_instance, result, "Failed to locate hand space!"); } From 5f5ece9f786b160b6054577dd58dbf1526b2acce Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Wed, 4 Sep 2024 21:20:40 +0200 Subject: [PATCH 17/20] OpenXrInputPlugin: Use C++20 map iterators. --- plugins/openxr/src/OpenXrInputPlugin.cpp | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/openxr/src/OpenXrInputPlugin.cpp b/plugins/openxr/src/OpenXrInputPlugin.cpp index f9823adf70e..f867efd1c3b 100644 --- a/plugins/openxr/src/OpenXrInputPlugin.cpp +++ b/plugins/openxr/src/OpenXrInputPlugin.cpp @@ -385,12 +385,12 @@ bool OpenXrInputPlugin::InputDevice::initActions() { }; // clang-format on - for (const auto& pathAndType : actionsToInit) { - std::shared_ptr action = std::make_shared(_context, pathAndType.second, pathAndType.first); + for (const auto& [path, type] : actionsToInit) { + std::shared_ptr action = std::make_shared(_context, type, path); if (!action->init(_actionSet)) { - qCCritical(xr_input_cat, "Creating action %s failed!", pathAndType.first.c_str()); + qCCritical(xr_input_cat, "Creating action %s failed!", path.c_str()); } else { - _actions.emplace(pathAndType.first, action); + _actions.emplace(path, action); } } @@ -516,12 +516,12 @@ void OpenXrInputPlugin::InputDevice::update(float deltaTime, const controller::I }; for (uint32_t i = 0; i < HAND_COUNT; i++) { - for (const auto& channelAndPath : axesToUpdate[i]) { - _axisStateMap[channelAndPath.first].value = _actions.at(channelAndPath.second)->getFloat(i).currentState; + for (const auto& [channel, path] : axesToUpdate[i]) { + _axisStateMap[channel].value = _actions.at(path)->getFloat(i).currentState; - // if (_axisStateMap[channelAndPath.first].value != 0) { - // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d): %f", i, channelAndPath.second.c_str(), channelAndPath.first, - // (double)_axisStateMap[channelAndPath.first].value); + // if (_axisStateMap[channel].value != 0) { + // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d): %f", i, path.c_str(), channel, + // (double)_axisStateMap[channel].value); // } } } @@ -553,10 +553,10 @@ void OpenXrInputPlugin::InputDevice::update(float deltaTime, const controller::I }; for (uint32_t i = 0; i < HAND_COUNT; i++) { - for (const auto& channelAndPath : buttonsToUpdate[i]) { - if (_actions.at(channelAndPath.second)->getBool(i).currentState == XR_TRUE) { - _buttonPressedMap.insert(channelAndPath.first); - // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d)", i, channelAndPath.second.c_str(), channelAndPath.first); + for (const auto& [channel, path] : buttonsToUpdate[i]) { + if (_actions.at(path)->getBool(i).currentState == XR_TRUE) { + _buttonPressedMap.insert(channel); + // qCDebug(xr_input_cat, "🐸 Controller %d: %s (%d)", i, path.c_str(), channel); } } } From 80750176e3b2e6c0585abb21e37db76fdd49f494 Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Wed, 4 Sep 2024 21:51:33 +0200 Subject: [PATCH 18/20] OpenXrDisplayPlugin: Use C++20 std::format. --- plugins/openxr/src/OpenXrDisplayPlugin.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/openxr/src/OpenXrDisplayPlugin.cpp b/plugins/openxr/src/OpenXrDisplayPlugin.cpp index 7d89875573d..87c924c19a5 100644 --- a/plugins/openxr/src/OpenXrDisplayPlugin.cpp +++ b/plugins/openxr/src/OpenXrDisplayPlugin.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #if defined(Q_OS_WIN) #undef near @@ -131,12 +132,8 @@ static std::string glFormatStr(GLenum source) { ENUM_TO_STR(GL_RGBA16); ENUM_TO_STR(GL_RGBA16F); ENUM_TO_STR(GL_SRGB8_ALPHA8); - default: { - // TODO: Enable C++20 for std::format - std::ostringstream ss; - ss << "0x" << std::hex << source; - return ss.str(); - } + default: + return std::format("0x{:X}", source); } } From 26e0b7fba5c1d8cfbed1b8762d5c02373d5e5dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Thu, 5 Sep 2024 14:09:52 +0200 Subject: [PATCH 19/20] Install missing xcb/glx.h dependency. --- .github/workflows/pr_build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index 2bd3b629c40..31b06b61664 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -64,7 +64,7 @@ jobs: runner: [self_hosted, type-cx52, image-x86-app-docker-ce] arch: amd64 build_type: full - apt-dependencies: pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 # add missing dependencies to docker image when convenient + apt-dependencies: pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 libxcb-glx0-dev # add missing dependencies to docker image when convenient image: docker.io/overte/overte-full-build:0.1.1-ubuntu-20.04-amd64 # Android builds are currently failing #- os: ubuntu-18.04 @@ -75,6 +75,7 @@ jobs: runner: [self_hosted, type-cax41, image-arm-app-docker-ce] arch: aarch64 build_type: full + apt-dependencies: libxcb-glx0-dev # add missing dependencies to docker image when convenient image: docker.io/overte/overte-full-build:0.1.1-ubuntu-22.04-aarch64 fail-fast: false runs-on: ${{matrix.runner}} From 471798a8834fa92a3eac07a5777cfaf9afe5f6ed Mon Sep 17 00:00:00 2001 From: Lubosz Sarnecki Date: Sat, 7 Sep 2024 15:37:22 +0200 Subject: [PATCH 20/20] pr_build: Get GCC 13 on Ubuntu using the ppa:ubuntu-toolchain-r/test. --- .github/workflows/pr_build.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index 31b06b61664..8a269ca1ace 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -184,12 +184,21 @@ jobs: run: | if [[ "${{ matrix.os }}" =~ "Ubuntu" || "${{ matrix.os }}" =~ "Debian" ]]; then + echo "Adding Toolchain test PPA" + add-apt-repository ppa:ubuntu-toolchain-r/test + echo "Updating apt repository index" sudo apt update || exit 1 echo "Installing apt packages" sudo apt install -y ${{ matrix.apt-dependencies }} || exit 1 + echo "Installing gcc-13" + sudo apt install -y gcc-13 g++-13 || exit 1 + + # Set GCC 13 as default + update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100 --slave /usr/bin/g++ g++ /usr/bin/g++-13 --slave /usr/bin/gcov gcov /usr/bin/gcov-13 + else # macOS echo "Downloading MacOSX10.12 SDK.." curl --progress-bar -L -o macOS_SDK10.12.4.tar.xz "https://data.moto9000.moe/overte_packages/macOS_SDK10.12.4.tar.xz" || exit 1