diff --git a/runtime/windows-openvr/REV_CAPI_Audio.cpp b/runtime/windows-openvr/REV_CAPI_Audio.cpp index e21c8e9..abf770f 100644 --- a/runtime/windows-openvr/REV_CAPI_Audio.cpp +++ b/runtime/windows-openvr/REV_CAPI_Audio.cpp @@ -46,13 +46,17 @@ ovrResult AudioEndPointToGuid(char* deviceStrBuffer, int deviceStrSize, GUID* de } PROPVARIANT pv; + PropVariantInit(&pv); hr = pPropertyStore->GetValue(PKEY_AudioEndpoint_GUID, &pv); if (FAILED(hr)) { return ovrError_AccessDenied; } - hr = IIDFromString(pv.pwszVal, deviceGuid); + hr = pv.vt == VT_LPWSTR && pv.pwszVal + ? IIDFromString(pv.pwszVal, deviceGuid) + : E_INVALIDARG; + PropVariantClear(&pv); if (FAILED(hr)) return ovrError_RuntimeException; return ovrSuccess; @@ -83,6 +87,7 @@ ovrResult GetDefaultAudioEndpoint(EDataFlow endpoint, WCHAR deviceStrBuffer[OVR_ if (FAILED(hr)) return ovrError_AudioComError; wcscpy_s(deviceStrBuffer, OVR_AUDIO_MAX_DEVICE_STR_SIZE, pGuid); + CoTaskMemFree(pGuid); return ovrSuccess; } diff --git a/runtime/windows-openvr/REV_CAPI_Vk.cpp b/runtime/windows-openvr/REV_CAPI_Vk.cpp index cdb848c..ac29fc6 100644 --- a/runtime/windows-openvr/REV_CAPI_Vk.cpp +++ b/runtime/windows-openvr/REV_CAPI_Vk.cpp @@ -44,7 +44,7 @@ ovr_GetInstanceExtensionsVk( uint32_t* inoutExtensionNamesSize) { if (!inoutExtensionNamesSize) - ovrError_InvalidParameter; + return ovrError_InvalidParameter; std::stringstream extensions; uint32_t required = vr::VRCompositor()->GetVulkanInstanceExtensionsRequired(nullptr, 0); @@ -79,7 +79,7 @@ ovr_GetDeviceExtensionsVk( uint32_t* inoutExtensionNamesSize) { if (!inoutExtensionNamesSize) - ovrError_InvalidParameter; + return ovrError_InvalidParameter; std::stringstream extensions; // xrizer cannot answer this query until ovr_GetSessionPhysicalDeviceVk has diff --git a/runtime/windows-openxr/InputManager.cpp b/runtime/windows-openxr/InputManager.cpp index 5da4220..b155391 100644 --- a/runtime/windows-openxr/InputManager.cpp +++ b/runtime/windows-openxr/InputManager.cpp @@ -30,17 +30,46 @@ InputManager::InputManager(XrInstance instance) { device->GetActiveSets(m_ActionSets); - std::vector bindings; - XrPath profile = device->GetSuggestedBindings(bindings); - if (!profile) + // Suggest every profile a device supports and let the runtime pick the + // one matching the connected controllers. A runtime that can't remap a + // single profile to other hardware leaves those controllers without any + // binding, so they never report a pose or a button. + std::vector profiles; + device->GetSuggestedBindings(profiles); + for (const ProfileBindings& profile : profiles) + { + XrInteractionProfileSuggestedBinding suggestedBinding = XR_TYPE(INTERACTION_PROFILE_SUGGESTED_BINDING); + suggestedBinding.countSuggestedBindings = (uint32_t)profile.Bindings.size(); + suggestedBinding.suggestedBindings = profile.Bindings.data(); + suggestedBinding.interactionProfile = profile.Profile; + XrResult rs = xrSuggestInteractionProfileBindings(instance, &suggestedBinding); + // A rejected profile must not cost the others their bindings. + TraceXrResult("xrSuggestInteractionProfileBindings", rs); + } + } +} + +void InputManager::UpdateInteractionProfiles(XrSession session) +{ + std::lock_guard actionLock(m_ActionMutex); + + const XrPath touch = GetXrPath("/interaction_profiles/oculus/touch_controller"); + const XrPath index = GetXrPath("/interaction_profiles/valve/index_controller"); + const XrPath wmr = GetXrPath("/interaction_profiles/microsoft/motion_controller"); + for (int i = 0; i < ovrHand_Count; i++) + { + XrInteractionProfileState state = XR_TYPE(INTERACTION_PROFILE_STATE); + if (XR_FAILED(xrGetCurrentInteractionProfile(session, s_SubActionPaths[i], &state))) continue; - XrInteractionProfileSuggestedBinding suggestedBinding = XR_TYPE(INTERACTION_PROFILE_SUGGESTED_BINDING); - suggestedBinding.countSuggestedBindings = (uint32_t)bindings.size(); - suggestedBinding.suggestedBindings = bindings.data(); - suggestedBinding.interactionProfile = profile; - XrResult rs = xrSuggestInteractionProfileBindings(instance, &suggestedBinding); - assert(XR_SUCCEEDED(rs)); + for (InputDevice* device : m_InputDevices) + device->SetInteractionProfile((ovrHandType)i, state.interactionProfile); + + // 0 = no binding (the controller won't track), 1 = Touch, 2 = Index, + // 3 = WMR, 4 = a profile RiftLift doesn't suggest. + const XrPath profile = state.interactionProfile; + TraceOculusValue(i == ovrHand_Left ? "interactionProfile.left" : "interactionProfile.right", + profile == XR_NULL_PATH ? 0 : profile == touch ? 1 : profile == index ? 2 : profile == wmr ? 3 : 4); } } @@ -428,8 +457,19 @@ InputManager::OculusTouch::OculusTouch(XrInstance instance) , m_Pose(this, XR_ACTION_TYPE_POSE_INPUT, "pose", "Controller pose", true) , m_Vibration(this, XR_ACTION_TYPE_VIBRATION_OUTPUT, "vibration", "Vibration", true) { - if (Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE)) - m_Trackpad_Buttons = Action(this, XR_ACTION_TYPE_FLOAT_INPUT, "trackpad-buttons", "Trackpad Buttons", true); + // The WMR profile is suggested on every runtime, so its trackpad split is too. + m_Trackpad_Buttons = Action(this, XR_ACTION_TYPE_FLOAT_INPUT, "trackpad-buttons", "Trackpad Buttons", true); +} + +void InputManager::OculusTouch::SetInteractionProfile(ovrHandType hand, XrPath profile) +{ + m_WmrBound[hand] = profile != XR_NULL_PATH && + profile == GetXrPath("/interaction_profiles/microsoft/motion_controller"); +} + +bool InputManager::OculusTouch::UsesTrackpadButtons(ovrHandType hand) const +{ + return Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE) || m_WmrBound[hand]; } InputManager::OculusTouch::~OculusTouch() @@ -456,20 +496,40 @@ void InputManager::OculusTouch::UpdateHaptics(XrSession session, XrDuration disp } } -XrPath InputManager::OculusTouch::GetSuggestedBindings(std::vector& outBindings) const +void InputManager::OculusTouch::GetSuggestedBindings(std::vector& outProfiles) const +{ + if (Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE)) + { + outProfiles.push_back(GetLayoutBindings(Layout_WMR)); + return; + } + + // Always offer Touch: nearly every OpenXR game suggests it, so newer + // controllers ship bindings for it even where the runtime can't convert + // the Index profile to them. Monado doesn't convert Touch to WMR + // controllers, so offer WMR's own profile as well. + outProfiles.push_back(GetLayoutBindings(Layout_Touch)); + if (Runtime::Get().UseHack(Runtime::HACK_VALVE_INDEX_PROFILE)) + outProfiles.push_back(GetLayoutBindings(Layout_Index)); + outProfiles.push_back(GetLayoutBindings(Layout_WMR)); +} + +InputManager::ProfileBindings InputManager::OculusTouch::GetLayoutBindings(Layout layout) const { std::string prefixes[ovrHand_Count] = { "/user/hand/left", "/user/hand/right" }; + ProfileBindings result; + std::vector& outBindings = result.Bindings; #define ADD_BINDING(action, path) outBindings.push_back(XrActionSuggestedBinding{ action, GetXrPath(path) }) - if (Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE)) + if (layout == Layout_WMR) { ADD_BINDING(m_Button_Enter, "/user/hand/left/input/menu/click"); ADD_BINDING(m_Button_Home, "/user/hand/right/input/menu/click"); } else { - if (Runtime::Get().UseHack(Runtime::HACK_VALVE_INDEX_PROFILE)) + if (layout == Layout_Index) { ADD_BINDING(m_Button_Enter, "/user/hand/left/input/trackpad/force"); ADD_BINDING(m_Button_AX, "/user/hand/left/input/a/click"); @@ -495,7 +555,7 @@ XrPath InputManager::OculusTouch::GetSuggestedBindings(std::vector& outSpaces) const @@ -574,7 +635,7 @@ void InputManager::OculusTouch::GetInputState(XrSession session, ovrControllerTy ovrHandType hand = (ovrHandType)i; unsigned int buttons = 0, touches = 0; - if (Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE)) + if (UsesTrackpadButtons(hand)) { if (m_Button_AX.GetDigital(session, hand) || m_Button_BY.GetDigital(session, hand)) { @@ -632,7 +693,7 @@ void InputManager::OculusTouch::GetInputState(XrSession session, ovrControllerTy inputState->HandTriggerNoDeadzone[i] = m_HandTrigger.GetAnalog(session, hand); // Derive gestures from touch flags - if (!Runtime::Get().UseHack(Runtime::HACK_WMR_PROFILE) || inputState->HandTriggerNoDeadzone[i] > 0.5f) + if (!UsesTrackpadButtons(hand) || inputState->HandTriggerNoDeadzone[i] > 0.5f) { if (!(touches & ovrTouch_RIndexTrigger)) touches |= ovrTouch_RIndexPointing; @@ -784,8 +845,11 @@ InputManager::XboxGamepad::~XboxGamepad() { } -XrPath InputManager::XboxGamepad::GetSuggestedBindings(std::vector& outBindings) const +void InputManager::XboxGamepad::GetSuggestedBindings(std::vector& outProfiles) const { + ProfileBindings result; + std::vector& outBindings = result.Bindings; + #define ADD_BINDING(action, path) outBindings.push_back(XrActionSuggestedBinding{ action, GetXrPath(std::string("/user/gamepad") + path) }) ADD_BINDING(m_Button_A, "/input/a/click"); @@ -811,7 +875,8 @@ XrPath InputManager::XboxGamepad::GetSuggestedBindings(std::vector actionLock(m_ActionMutex); + // A new session reports its bound profiles again through events. + for (InputDevice* device : m_InputDevices) + for (int i = 0; i < ovrHand_Count; i++) + device->SetInteractionProfile((ovrHandType)i, XR_NULL_PATH); + for (XrSpace space : m_ActionSpaces) CHK_XR(xrDestroySpace(space)); m_ActionSpaces.clear(); diff --git a/runtime/windows-openxr/InputManager.h b/runtime/windows-openxr/InputManager.h index 6de2767..76d12a4 100644 --- a/runtime/windows-openxr/InputManager.h +++ b/runtime/windows-openxr/InputManager.h @@ -13,6 +13,12 @@ class Runtime; class InputManager { public: + struct ProfileBindings + { + XrPath Profile; + std::vector Bindings; + }; + class InputDevice { public: @@ -25,7 +31,8 @@ class InputManager virtual void GetInputState(XrSession session, ovrControllerType controllerType, ovrInputState* inputState) = 0; // Bindings - virtual XrPath GetSuggestedBindings(std::vector& outBindings) const { return XR_NULL_PATH; } + virtual void GetSuggestedBindings(std::vector& outProfiles) const { } + virtual void SetInteractionProfile(ovrHandType hand, XrPath profile) { } virtual void GetActionSpaces(XrSession session, std::vector& outSpaces) const { } virtual void GetActiveSets(std::vector& outSets) const { } @@ -73,7 +80,8 @@ class InputManager virtual ovrControllerType GetType() const override; virtual bool IsConnected() const override; virtual void GetInputState(XrSession session, ovrControllerType controllerType, ovrInputState* inputState) override; - virtual XrPath GetSuggestedBindings(std::vector& outBindings) const override; + virtual void GetSuggestedBindings(std::vector& outProfiles) const override; + virtual void SetInteractionProfile(ovrHandType hand, XrPath profile) override; virtual void GetActionSpaces(XrSession session, std::vector& outSpaces) const override; virtual void GetActiveSets(std::vector& outSets) const override; @@ -83,6 +91,10 @@ class InputManager virtual void UpdateHaptics(XrSession session, XrDuration displayPeriod) override; private: + enum Layout { Layout_Touch, Layout_Index, Layout_WMR }; + ProfileBindings GetLayoutBindings(Layout layout) const; + bool UsesTrackpadButtons(ovrHandType hand) const; + Action m_Button_AX; Action m_Button_BY; Action m_Button_Thumb; @@ -99,8 +111,11 @@ class InputManager Action m_HandTrigger; Action m_Thumbstick; - // For WMR profile hack + // Splits WMR's shared trackpad click into A/X and B/Y Action m_Trackpad_Buttons; + // The runtime bound the WMR profile to this hand. Guarded by the + // InputManager's action mutex, like every other input read. + bool m_WmrBound[ovrHand_Count] = {}; Action m_Pose; Action m_Vibration; @@ -141,7 +156,7 @@ class InputManager virtual ovrControllerType GetType() const override { return ovrControllerType_XBox; } virtual bool IsConnected() const override { return true; } virtual void GetInputState(XrSession session, ovrControllerType controllerType, ovrInputState* inputState) override; - virtual XrPath GetSuggestedBindings(std::vector& outBindings) const override; + virtual void GetSuggestedBindings(std::vector& outProfiles) const override; virtual void GetActiveSets(std::vector& outSets) const override; virtual ovrResult SetVibration(XrSession session, ovrControllerType controllerType, float frequency, float amplitude) override; @@ -172,6 +187,7 @@ class InputManager ~InputManager(); ovrResult AttachSession(XrSession session); + void UpdateInteractionProfiles(XrSession session); ovrResult SyncInputState(XrSession session, XrDuration displayPeriod); ovrResult SetControllerVibration(ovrSession session, ovrControllerType controllerType, float frequency, float amplitude); diff --git a/runtime/windows-openxr/REV_CAPI.cpp b/runtime/windows-openxr/REV_CAPI.cpp index 6374694..2e1b888 100644 --- a/runtime/windows-openxr/REV_CAPI.cpp +++ b/runtime/windows-openxr/REV_CAPI.cpp @@ -331,6 +331,12 @@ OVR_PUBLIC_FUNCTION(ovrResult) ovr_GetSessionStatus(ovrSession session, ovrSessi session->SessionStatus = status; break; } + case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: + // The runtime picked (or dropped) a controller profile: the input + // translation follows it, and "none" means the hands won't track. + if (session->Input) + session->Input->UpdateInteractionProfiles(session->Session); + break; case XR_TYPE_EVENT_DATA_VISIBILITY_MASK_CHANGED_KHR: { const XrEventDataVisibilityMaskChangedKHR& maskChange = diff --git a/runtime/windows-openxr/REV_CAPI_Vk.cpp b/runtime/windows-openxr/REV_CAPI_Vk.cpp index 7229ed6..7e50b53 100644 --- a/runtime/windows-openxr/REV_CAPI_Vk.cpp +++ b/runtime/windows-openxr/REV_CAPI_Vk.cpp @@ -32,7 +32,7 @@ ovr_GetInstanceExtensionsVk( uint32_t* inoutExtensionNamesSize) { if (!inoutExtensionNamesSize) - ovrError_InvalidParameter; + return ovrError_InvalidParameter; if (Runtime::Get().Supports(XR_KHR_VULKAN_ENABLE_EXTENSION_NAME)) { @@ -64,7 +64,7 @@ ovr_GetDeviceExtensionsVk( uint32_t* inoutExtensionNamesSize) { if (!inoutExtensionNamesSize) - ovrError_InvalidParameter; + return ovrError_InvalidParameter; if (Runtime::Get().Supports(XR_KHR_VULKAN_ENABLE_EXTENSION_NAME)) { @@ -99,7 +99,7 @@ ovr_GetSessionPhysicalDeviceVk( XR_FUNCTION(session->Instance, GetVulkanGraphicsDeviceKHR); if (!out_physicalDevice) - ovrError_InvalidParameter; + return ovrError_InvalidParameter; VulkanLibrary = LoadLibraryW(L"vulkan-1.dll"); if (!VulkanLibrary) diff --git a/runtime/windows-openxr/Session.cpp b/runtime/windows-openxr/Session.cpp index c087f66..3a1c05d 100644 --- a/runtime/windows-openxr/Session.cpp +++ b/runtime/windows-openxr/Session.cpp @@ -29,13 +29,17 @@ ovrResult ovrHmdStruct::InitSession(XrInstance instance) SystemProperties = XR_TYPE(SYSTEM_PROPERTIES); SystemColorSpace = XR_TYPE(SYSTEM_COLOR_SPACE_PROPERTIES_FB); - // Initialize view structures + // Initialize view structures. Chain the EPIC fov struct only when its + // extension is enabled: a struct from a disabled extension is invalid, + // and SteamVR fails xrEnumerateViewConfigurationViews over it. The view + // fallback below derives the fov without it. + const bool epicFov = Runtime::Get().Supports(XR_EPIC_VIEW_CONFIGURATION_FOV_EXTENSION_NAME); for (int i = 0; i < ovrEye_Count; i++) { ViewConfigs[i] = XR_TYPE(VIEW_CONFIGURATION_VIEW); ViewFov[i] = XR_TYPE(VIEW_CONFIGURATION_VIEW_FOV_EPIC); ViewPoses[i] = XR_TYPE(VIEW); - ViewConfigs[i].next = &ViewFov[i]; + ViewConfigs[i].next = epicFov ? &ViewFov[i] : nullptr; } XrSystemGetInfo systemInfo = XR_TYPE(SYSTEM_GET_INFO); diff --git a/runtime/windows-openxr/Swapchain.cpp b/runtime/windows-openxr/Swapchain.cpp index 878a001..60e573f 100644 --- a/runtime/windows-openxr/Swapchain.cpp +++ b/runtime/windows-openxr/Swapchain.cpp @@ -97,6 +97,13 @@ ovrResult ovrTextureSwapChainData::Init(XrSession session, const ovrTextureSwapC CHK_XR(created); TraceOculusValue("xrCreateSwapchain.createdMipCount", createInfo.mipCount); + // GE-Proton's WineOpenXR learns a swapchain's image count only when its + // images are first enumerated, and refuses an acquire before that with + // XR_ERROR_CALL_ORDER_INVALID. Asking for the count first is valid on + // every runtime. + uint32_t imageCount = 0; + CHK_XR(xrEnumerateSwapchainImages(Swapchain, 0, &imageCount, nullptr)); + XrSwapchainImageAcquireInfo acqInfo = XR_TYPE(SWAPCHAIN_IMAGE_ACQUIRE_INFO); CHK_XR(xrAcquireSwapchainImage(Swapchain, &acqInfo, &CurrentIndex)); diff --git a/tests/native/openxr_input.cpp b/tests/native/openxr_input.cpp index 57bdb6d..8fc1d42 100644 --- a/tests/native/openxr_input.cpp +++ b/tests/native/openxr_input.cpp @@ -103,6 +103,8 @@ class InputManager { class OculusTouch : public InputDevice { public: void GetInputState(XrSession, ovrControllerType, ovrInputState*) override; + bool UsesTrackpadButtons(ovrHandType hand) const; + bool m_WmrBound[ovrHand_Count] = {}; Action m_Button_Enter{Enter, false}, m_Button_Home{Home, false}; Action m_Button_AX{AX}, m_Button_BY{BY}, m_Button_Thumb{Thumb}; Action m_Touch_AX{TouchAX}, m_Touch_BY{TouchBY}, m_Touch_Thumb{TouchThumb}; @@ -216,6 +218,20 @@ int main() trackpadY = 1; check(sample() == ovrButton_Y, "WMR upper trackpad click is one button"); + // Monado binds the WMR profile without being the WMR runtime: its shared + // trackpad click must still be one button, never the X+Y Menu chord. + reset(); + touch.m_WmrBound[ovrHand_Left] = true; + states[AX][0].pressed = states[BY][0].pressed = true; + check(sample() == ovrButton_X, "bound WMR lower trackpad click is one button"); + trackpadY = 1; + check(sample() == ovrButton_Y, "bound WMR upper trackpad click is one button"); + touch.m_WmrBound[ovrHand_Left] = false; + touch.m_WmrBound[ovrHand_Right] = true; + trackpadY = 0; + check(sample() == chord, "a WMR right hand must not change the left hand"); + touch.m_WmrBound[ovrHand_Right] = false; + #ifdef NDEBUG reset(); states[AX][0].pressed = states[BY][0].pressed = true; diff --git a/tests/test_openxr_input.py b/tests/test_openxr_input.py index 6e82618..ede4668 100644 --- a/tests/test_openxr_input.py +++ b/tests/test_openxr_input.py @@ -38,6 +38,7 @@ def test_openxr_menu_input(tmp_path: Path, release: bool): for signature in ( "bool InputManager::Action::GetDigital(", "void InputManager::OculusTouch::GetInputState(", + "bool InputManager::OculusTouch::UsesTrackpadButtons(", "ovrResult InputManager::GetInputState(", ) )