From 8fb5816bf42629b0a3253d361c3e900387464a46 Mon Sep 17 00:00:00 2001 From: marshmallow-mia Date: Thu, 8 Oct 2026 08:39:49 +0200 Subject: [PATCH 1/3] Return Revive's invalid-parameter errors and free audio endpoint data Three Vulkan entry points in the OpenXR backend and two in the OpenVR backend evaluated ovrError_InvalidParameter without returning it, so a null argument fell through to the rest of the call. The OpenVR backend's audio endpoint lookup now reads the endpoint GUID only from a string property, clears the property and frees the device id, as the OpenXR backend already does. --- runtime/windows-openvr/REV_CAPI_Audio.cpp | 7 ++++++- runtime/windows-openvr/REV_CAPI_Vk.cpp | 4 ++-- runtime/windows-openxr/REV_CAPI_Vk.cpp | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) 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/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) From 3c8f0d731b1043efca457d66dd51900790968694 Mon Sep 17 00:00:00 2001 From: marshmallow-mia Date: Thu, 8 Oct 2026 08:44:26 +0200 Subject: [PATCH 2/3] Suggest Touch, Index and WMR bindings and follow the bound profile The OpenXR backend suggested one controller profile: Index on SteamVR, Touch elsewhere. A runtime that can't convert that profile leaves the controllers without a binding, so they never track. Monado doesn't convert Touch to WMR controllers (real ones or its simulated ones), and SteamVR may not convert Index to newer controllers such as the Steam Frame's. Touch is now always suggested, Index on SteamVR as before, and WMR's own profile everywhere; the WMR runtime keeps only its own. A rejected suggestion is traced and no longer costs the other profiles. WMR's trackpad click is bound to both A/X and B/Y and split by trackpad height. That split ran only on the WMR runtime, so with WMR bound elsewhere one left trackpad click read as X+Y, which the Menu chord turns into Menu. The backend now tracks which profile the runtime bound to each hand (INTERACTION_PROFILE_CHANGED) and splits the click whenever WMR is bound. The bound profiles are traced as interactionProfile.left/right. An explicit OpenXR launch can now use SteamVR's OpenXR runtime as well, so SteamVR gets all three suggestions and binds Touch controllers to the Touch profile directly. --- runtime/windows-openxr/InputManager.cpp | 120 +++++++++++++++++++----- runtime/windows-openxr/InputManager.h | 24 ++++- runtime/windows-openxr/REV_CAPI.cpp | 6 ++ tests/native/openxr_input.cpp | 16 ++++ tests/test_openxr_input.py | 1 + 5 files changed, 138 insertions(+), 29 deletions(-) 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/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(", ) ) From 6683cdb03159088fda0c24ebea2d2afb98b5fecc Mon Sep 17 00:00:00 2001 From: marshmallow-mia Date: Thu, 8 Oct 2026 08:44:51 +0200 Subject: [PATCH 3/3] Chain the EPIC fov struct only when enabled; count images before acquiring InitSession chained XrViewConfigurationViewFovEPIC to every view config even when XR_EPIC_view_configuration_fov isn't enabled. A struct from a disabled extension is invalid, and SteamVR fails xrEnumerateViewConfigurationViews with XR_ERROR_VALIDATION_FAILURE over it; the view fallback already derives the fov without it. A new swapchain's first image was acquired straight after xrCreateSwapchain. GE-Proton's WineOpenXR learns the image count only when the images are first enumerated and refuses that acquire with XR_ERROR_CALL_ORDER_INVALID, so the count is asked for first. --- runtime/windows-openxr/Session.cpp | 8 ++++++-- runtime/windows-openxr/Swapchain.cpp | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) 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));