I'm in love with VR and with nix. I want to do my godot and other VR dev in Linux with the ability to easily share the output. SteamVR itself has a pretty simple usability story on nixos as of late and can be installed/configured basically by adding programs.steam.enabled = true; to your system derivation.

The system I'm integrating has an nvidia 1080ti, and I'm currently building nixos from the 22.11 branch, with godot_4 from unstable.

My primary usecase here is to support godot 4 VR dev with support for simply hitting the play button for quick iteration. This is working now, however there are two changes that need to be made to the default install to get this working.

First, we need our SteamVR to support OpenXR as this is the future of VR dev on godot and because it allows us to easily target non-steam headsets without steam being installed on the user's system. SteamVR is interestingly implemented as a steam "app" itself and uses the standard steam installation and upgrade flows that games themselves use.

Currently, the only version of SteamVR that supports OpenXR (at least on Linux, AFAIK) is in the beta release channel. To install this version, find SteamVR in your library, right click on it, select "properties, then "Betas" on the left and choose "beta" from the dropdown list to install it onto your system.

The integration with OpenXR is implemented at two important points. The OpenXR spec provides a well-known location for configuring OpenXR-compatible runtime libraries on a Linux system by specifying the config at $XDG_CONFIG_HOME/openxr/1/active_runtime.json. When initializing an OpenXR session, this file is read to determine which dynamic library to load into the process. This config is automatically dropped by the SteamVR install process.

On my machine this file looks like this:

  "file_format_version" : "1.0.0",
  "runtime" : {
    "VALVE_runtime_is_steamvr" : true,
    "library_path" : "/home/josh/.local/share/Steam/steamapps/common/SteamVR/bin/linux64/vrclient.so",
    "name" : "SteamVR"

The library referenced is an implementation of the khronos group's OpenXR interface specification for some particular vendor/stack (e.g. monado being an OSS impl).

In godot 4, OpenXR functionality is included by default and does not need to be added as a separate plugin like in godot 3. However, because of how nixos handles dynamic dependencies, when loading this library we run into a number of failures in the godot logs (run it from the terminal to see them on stdout):

Error [GENERAL | xrEnumerateInstanceExtensionProperties | OpenXR-Loader] : RuntimeInterface::LoadRuntime skipping manifest file /home/josh/.
config/openxr/1/active_runtime.json, failed to load with message "libSDL2-2.0.so.0: cannot open shared object file: No such file or director

Because this library is installed as a steam app, the nixos machinations needed to map dependencies is not handled automatically. We ultimately need to manually patch this library so that it can find the necessary dependencies.

We're assisted by a nix package called steam-run which is a derivation that builds a virtual FHS filesystem with all the libraries and other deps necessary to run steam games linked in, effectively giving them the impression they're running on a standard Linux distro.

The steam-run command in this package is setup to run apps in this FHS environment so that they don't need to be patched themselves. Unfortunately the nixos godot package is not able to run in this FHS env because it needs a number of deps not available there, it simply fails to start if you try to steam-run godot.

If you're familiar with nix, you're probably also familiar with the runpath setting in ELF files (the format used for executables and library files on Linux). This allows specifying path(s) where runtime dependencies should be searched for. For example, if runpath on the above library was set to /usr/lib64, the runtime linker would look for the SDL lib at /usr/lib64/ibSDL2-2.0.so.0.

If we run ldd on the steam OpenXR library, we can see its dependencies and where the dynamic linker found them at, and those it could not find:

        linux-vdso.so.1 (0x00007ffdcef82000)
        libSDL2-2.0.so.0 => not found
        libGL.so.1 => not found
        librt.so.1 => /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/librt.so.1 (0x00007f16af3b4000)
        libdl.so.2 => /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libdl.so.2 (0x00007f16af3af000)
        /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib64/ld-linux-x86-64.so.2 (0x00007f16afa73000)
        libpthread.so.0 => /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libpthread.so.0 (0x00007f16af3aa000)
        libstdc++.so.6 => not found
        libm.so.6 => /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libm.so.6 (0x00007f16af2c8000)
        libc.so.6 => /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib/libc.so.6 (0x00007f16af0bf000)

You can see there are a number of deps that cannot currently be found, though this is missing "deps-of-deps" (e.g. SLD2 depends on libX11). This could get really messy as the ultimate tree of dependencies can be quite large, and patching every dependency would be a non-starter.

Luckily all of these missing deps, already patched with the proper runpaths, are linked into the steam-run FHS env, so basically all we need to do is set the runpath for the OpenXR interface lib to load the libraries from this FHS. We can do this by using the nix tool called patchelf to modify the library itself.

I currently do this with the following script:

#!/usr/bin/env bash
STOREPATH=$(nix-store -qR `which steam` | grep steam-fhs)/lib64
patchelf --set-rpath $STOREPATH $VRCLIENT

This finds the steam-fhs path in the nix store that the current steam is being run in. Once this rpath is set, we can run ldd again to take a look at the fully resolved dependency list, and at this point hitting the play button in godot should work properly (start SteamVR first as it won't auto-start like on windows).

        linux-vdso.so.1 (0x00007ffcaaab0000)
        libSDL2-2.0.so.0 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libSDL2-2.0.so.0 (0x00007ff3d3a8c000)
        libGL.so.1 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libGL.so.1 (0x00007ff3d39fe000)
        librt.so.1 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/librt.so.1 (0x00007ff3d39f9000)
        libdl.so.2 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libdl.so.2 (0x00007ff3d39f4000)
        /nix/store/vnwdak3n1w2jjil119j65k8mw1z23p84-glibc-2.35-224/lib64/ld-linux-x86-64.so.2 (0x00007ff3d4367000)
        libpthread.so.0 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libpthread.so.0 (0x00007ff3d39ef000)
        libstdc++.so.6 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libstdc++.so.6 (0x00007ff3d37d7000)
        libm.so.6 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libm.so.6 (0x00007ff3d36f7000)
        libc.so.6 => /nix/store/6sdhss95xxd674hrlm2b6qvm5bbnrkz7-steam-fhs/lib64/libc.so.6 (0x00007ff3d34ee000)
        libX11.so.6 => /nix/store/33qdhi8l6f4ixqzdc387w9gwdxrdsara-libX11-1.8.4/lib/libX11.so.6 (0x00007ff3d33a7000)
        libXext.so.6 => /nix/store/7wvl0fsdjf225qfkm55x8clwcbmx6mvn-libXext-1.3.4/lib/libXext.so.6 (0x00007ff3d3392000)
        libXcursor.so.1 => /nix/store/zp53b74y1a633ln8911306k72fr66da4-libXcursor-1.2.0/lib/libXcursor.so.1 (0x00007ff3d3383000)
        libXi.so.6 => /nix/store/4fadc9ggmmy1lm260lazra2b3is9ivfv-libXi-1.8/lib/libXi.so.6 (0x00007ff3d336f000)
        libXfixes.so.3 => /nix/store/fl75671jh474li11v36ar7305z1a4mzm-libXfixes-6.0.0/lib/libXfixes.so.3 (0x00007ff3d3367000)
        libXrandr.so.2 => /nix/store/c9c4xh22pfnyr2aravx7v4rvvznmcxcl-libXrandr-1.5.2/lib/libXrandr.so.2 (0x00007ff3d335a000)
        libXss.so.1 => /nix/store/1w8nic2428ppr4v3q5xhnqn0zqgp7i8f-libXScrnSaver-1.2.3/lib/libXss.so.1 (0x00007ff3d3355000)
        libGLX.so.0 => /nix/store/1yllc6r36zxgxmjmn2kqcs2vjqhlvyl9-libglvnd-1.5.0/lib/libGLX.so.0 (0x00007ff3d331f000)
        libGLdispatch.so.0 => /nix/store/1yllc6r36zxgxmjmn2kqcs2vjqhlvyl9-libglvnd-1.5.0/lib/libGLdispatch.so.0 (0x00007ff3d3267000)
        libgcc_s.so.1 => /nix/store/6plx60y4x4q2lfp6n7190kaihyxr7m1w-gcc-11.3.0-lib/lib/libgcc_s.so.1 (0x00007ff3d324d000)
        libxcb.so.1 => /nix/store/i9hwmlk4va2dxcca00cmy9vy38d3f5l1-libxcb-1.14/lib/libxcb.so.1 (0x00007ff3d3222000)
        libXrender.so.1 => /nix/store/rrs3p13wgrlp82i41cksgbcyfri8k6yg-libXrender-0.9.10/lib/libXrender.so.1 (0x00007ff3d3213000)
        libXau.so.6 => /nix/store/wmpqgysa9qmm6gr9smn3wrmnz2wr0pf5-libXau-1.0.9/lib/libXau.so.6 (0x00007ff3d320e000)
        libXdmcp.so.6 => /nix/store/89xgh7cmxmzclkpci1v4zbfr1idg0ha4-libXdmcp-1.1.3/lib/libXdmcp.so.6 (0x00007ff3d3206000)

I'm not sure if there's a way to hook the SteamVR installation process to handle this automatically, but for now I need to run this script anytime that SteamVR recieves an update. One last caveat, this is on an X11 system, I have no clue if it would work properly on wayland.