Skip to content

Fix OpenXR controller bindings on non-SteamVR runtimes, and Revive call bugs - #21

Merged
Villagers654 merged 3 commits into
Villagers654:mainfrom
marshmallow-mia:openxr-fixes-from-echoxr
Oct 24, 2026
Merged

Villagers654 merged 3 commits into
Villagers654:mainfrom
marshmallow-mia:openxr-fixes-from-echoxr

Conversation

@marshmallow-mia

Copy link
Copy Markdown
Contributor

What this fixes

Controllers that never track (Monado, WMR controllers, newer SteamVR controllers).
The OpenXR backend suggested a single controller profile: Index on SteamVR, Touch everywhere else. A runtime that can't
convert that one profile leaves the controllers unbound, so the hands never move. On Monado with WMR controllers, main's
bridge gets no profile on either hand (left: (null), right: (null) in Monado's binding log).

  • Touch is now always suggested, Index on SteamVR as before, and the WMR profile everywhere. The WMR runtime keeps only
    its own profile.
  • A rejected suggestion is traced and no longer costs the other profiles.
  • An explicit OpenXR launch can use SteamVR's OpenXR runtime now, so SteamVR gets all three suggestions as well and
    binds Touch controllers to the Touch profile directly.

The trackpad split follows the bound profile. WMR's trackpad click is bound to both A/X and B/Y and split by where
the thumb is on the trackpad. Main only binds WMR on the WMR runtime, where the split already runs. Now that the WMR
profile is suggested on every runtime, the split has to run wherever WMR is bound, or one left click would read as X+Y,
which the Menu chord turns into Menu. The backend follows which profile the runtime bound to each hand
(XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED) and splits the click whenever WMR is bound. The bound profiles show
in the trace as interactionProfile.left/right.

Null arguments falling through. Three Vulkan entry points in the OpenXR backend and two in the OpenVR backend
evaluated ovrError_InvalidParameter without returning it. With a null size pointer, ovr_GetInstanceExtensionsVk and
ovr_GetDeviceExtensionsVk crash with an access violation in main's DLLs (both backends); they now return the error.

Two runtime calls made spec-conformant. These come from EchoXR, where they showed up with other runtime versions.
Neither could be reproduced on the runtimes tested here (SteamVR 2.16.7, Monado, WiVRn 26.9, GE-Proton 11-3), so they
are checked for "no regression" only:

  • InitSession chained XrViewConfigurationViewFovEPIC even when XR_EPIC_view_configuration_fov isn't enabled, which
    a runtime may reject with XR_ERROR_VALIDATION_FAILURE. It is now chained only when the extension is on; the fallback
    already derives the field of view without it.
  • The first swapchain image was acquired right after xrCreateSwapchain. A wineopenxr that only learns the image count
    when the images are enumerated refuses that with XR_ERROR_CALL_ORDER_INVALID, so the count is now asked for first.

OpenVR audio endpoint cleanup. The OpenVR backend's endpoint lookup now reads the GUID only from a string property,
clears the property and frees the device ID, as the OpenXR backend already does.

Testing

On 0.11.1.2

The three commits apply to 0.11.1.2 unchanged. Upstream's runtime changes since the last base (swapchain retries,
SteamVR for explicit OpenXR launches, the OpenVR input lock) don't touch the same code, and the image-count query
still runs right before the first acquire, after the new retries.

  • tests/test_openxr_input.py passes; so does the full suite on Linux (Python 3.10 + PySide6 6.7, 3.12 + 6.11 and
    3.12 + 6.12) and on macOS.
  • Fork CI on the ported branch: the CI and Windows workflows pass, Python and native jobs alike.
  • Not verified on 0.11.1.2:
    • Physical WMR or Index controllers: only Monado's simulated WMR controllers were at hand.
    • Swapchains other than D3D11, and the 32-bit OpenXR DLL. Under Proton the OpenXR path is D3D11 only, and no owned
      game is known to use OpenGL, Vulkan or a 32-bit build.
  • Runtime tests with the fork CI's MSVC builds of main and of this branch. Platform shim: stop hanging games in Meta's uninitialized implementation #20's shim is in both payloads, since the
    test games need it to get past sign-in.
    • Monado, simulated headset with WMR controllers:
      • Main's bridge: no profile on either hand (left: (null), right: (null)).
      • This branch: /interaction_profiles/microsoft/motion_controller on both (interactionProfile.left/right 3 in
        the trace). The trailer renders with both (5,013 and 5,025 frames).
    • Blocks (OpenVR backend through XRizer) renders on Monado with both DLLs.
    • A Wine harness calling both CI builds:
      • Null size pointers: main crashes with an access violation in all four calls; this branch returns
        ovrError_InvalidParameter.
      • Audio endpoints: both return the same GUIDs, with no memory growth over 2,000 calls.
    • The OpenXR core validation layer reports nothing but its own start and end. No XR_ERROR_CALL_ORDER_INVALID.
    • Quest 3 through Steam Link into SteamVR (SteamVR's vrserver.txt):
      • Main's bridge hands SteamVR two bindings, Index ("knuckles") and gamepad. There is none for Touch, which is
        what the Quest's controllers are through Steam Link.
      • This branch hands it three: Index, gamepad and oculus_touch. SteamVR logs "Unable to find input profile for
        holographic_controller" for the WMR suggestion and loads the other three.
      • The trailer renders with both (7,677 and 7,831 frames). Blocks renders with both OpenVR DLLs. Lone Echo renders
        with both bridges (2,711 and 2,561 frames in its first 40 s).
      • With the controllers held, this branch binds /interaction_profiles/oculus/touch_controller on both hands
        (interactionProfile.left/right 1), in the trailer and in Lone Echo 3.17.4.
      • Controls by hand in Lone Echo, with both bridges: pointing and the trigger in its menu, grip to grab, both
        sticks, A, B, X and Y. X+Y (the Menu chord) opens the pause menu; through Steam Link the left menu button opens
        SteamVR's dashboard instead. In the trailer the hands and fingers follow the controllers.
      • With this branch SteamVR also logs "Invalid input type click" for the Touch profile's left menu and right
        system paths, the two buttons it keeps for its dashboard. The rest of the Touch binding loads. Main's bridge
        doesn't suggest Touch there, so it doesn't get this line.
    • Quest 3 on WiVRn 26.9:
      • The trailer renders with both (7,468 and 7,936 frames). Blocks renders with this branch's OpenVR DLL.
      • While the left controller was awake, main's bridge bound it to /interaction_profiles/oculus/touch_controller.
        That's expected: main already suggests Touch outside SteamVR.
      • Lone Echo with the controllers held: this branch binds Touch on both hands (interactionProfile.left/right 1)
        and follows WiVRn when it drops and restores the controllers (0 and back to 1, matching WiVRn's own log).
        With both bridges the trigger, grip, both sticks, A, B, X and Y work, and the pause menu opens with the left
        menu button and with X+Y.
      • Earlier the same evening the trailer's third level stalled on WiVRn in 5 of 6 runs, with main's code as well.
        It no longer reproduces, and the cause wasn't found.
    • Windows 10, Virtual Desktop's OpenXR runtime: the merge of all branches renders the trailer (5,010 frames).
      Main's build hangs there before rendering because of its shim (Platform shim: stop hanging games in Meta's uninitialized implementation #20), so the bridges weren't compared on Windows.

Earlier, on 7289ef6

All runtime tests used the fork CI's MSVC builds of main and of this branch (the platform shim fix from
fix/platform-shim-uninitialized swapped in, since the test games need it to get past sign-in).

  • Lone Echo II Trailer Experience:
    • Monado, simulated headset with WMR controllers: main's bridge renders but no profile is bound on either hand; this
      branch binds /interaction_profiles/microsoft/motion_controller on both.
    • Quest 3 on WiVRn 26.9: renders with main's bridge (8,254 frames) and this branch's (8,273 frames).
    • Quest 3 on SteamVR through Steam Link: renders with main's bridge (5,159 frames) and this branch's (7,406 frames).
    • No OpenXR error in any trace, no XR_ERROR_CALL_ORDER_INVALID, nothing from the OpenXR core validation layer.
  • Blocks (Unity, OpenVR backend): renders with this branch's OpenVR DLL on Monado and WiVRn (through XRizer) and on
    SteamVR, as with main's.
  • A Wine harness calling both CI builds: main crashes on the null size pointers, this branch returns
    ovrError_InvalidParameter; both return the same audio device GUIDs.
  • tests/test_openxr_input.py covers the trackpad split with WMR bound on a non-WMR runtime.
  • Earlier, with the same source: controls checked by hand on the Quest 3 on both runtimes.

No UI change, so no screenshots.

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.
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.
…iring

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.

@Villagers654 Villagers654 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Villagers654
Villagers654 merged commit dbf2516 into Villagers654:main Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants