๐ง Full-Res VR on Linux: Bigscreen Beyond 2E + VRChat on CachyOS
I moved my VR setup to Linux full-time. It took about a week of evenings, a couple of GPU lockups, and more Bluetooth debugging than I ever wanted. It now works well: one command starts everything, one command shuts it all down, and the headset runs at its full native resolution, which SteamVR on Linux can't do.
This guide covers everything I did, in the order I hit it, including the dead ends. Most of the Linux VR information online is scattered across Discord threads and GitHub issues, so I wanted it in one place.
My hardware
| Part | What I use |
|---|---|
| Headset | Bigscreen Beyond 2E (DisplayPort + USB) |
| Controllers | Valve Index controllers |
| Full-body tracking | 3ร Vive Tracker 3.0 |
| Base stations | 2ร SteamVR Base Station 2.0 |
| GPU | NVIDIA RTX 4070 Ti SUPER (open kernel modules) |
| OS | CachyOS, KDE Plasma on Wayland |
| Main app | VRChat (through Steam/Proton) |
What I ended up with
- Monado as the VR runtime instead of SteamVR. It drives the Beyond at 5088ร2544 @ 75 Hz; SteamVR on Linux is stuck at 3840ร1920 @ 90 Hz.
- xrizer, which lets OpenVR games like VRChat run on Monado.
- WayVR as the desktop overlay (it replaces XSOverlay/OVR Toolkit), with OVR Advanced Settings-style playspace dragging.
play-vr/stop-vrscripts that handle startup, shutdown, base station power, mic switching and backups.- Eye tracking via Baballonia + VRCFaceTracking (installed and running, but not calibrated yet at the time of writing).
If you're on AMD: a lot of this gets easier. Several of the worst problems below (the display corruption and the GPU lockups) are NVIDIA-specific.
Part 1: SteamVR first, and why I left it
I started where everyone does: SteamVR from Steam, the Bigscreen Beyond Utility, and VRChat through Proton. It mostly worked, but four things were wrong.
1.1 Rainbow static in the left eye (NVIDIA DSC bug)
Flickering pink/purple/blue "static" showed up around text and high-contrast edges in the left eye. This is a known bug in how NVIDIA's Linux driver handles Display Stream Compression (DSC) with the Beyond. Bigscreen and NVIDIA worked out kernel-side fixes, and CachyOS carries those patches in its kernel. My kernel package had been built a day before the patches landed.
Fix: update to a CachyOS kernel and NVIDIA driver that include the Beyond DSC patches. For me that was linux-cachyos 7.2.8 with NVIDIA 615.71.09. The static was gone after a reboot. If you've pinned your kernel in /etc/pacman.conf (IgnorePkg), unpin it before updating.
Note: the LTS kernel didn't have the patch when I checked. Use the main
linux-cachyoskernel.
1.2 Huge frame drops: check your supersampling
SteamVR had set supersampling to 184%, and the compositor was dropping around 1,300 frames a minute. Setting it back to 100% fixed that straight away. The Beyond's panels are already very high resolution, so start at 100% and only raise it if you have GPU headroom.
1.3 The Bigscreen Beyond Utility under Proton
The Beyond Utility is a Windows app, and it does run under Proton (it's on Steam). Some parts work and some don't:
- "Invalid SteamVR folder" warning: you can't fix this. It looks for
bin\win64\vrcmd.exe, which the Linux SteamVR build doesn't have. It only matters for the settings the Utility pushes into SteamVR, and you can edit those insteamvr.vrsettingsby hand. - Refresh rate: setting 75 Hz in the Utility does nothing on Linux. More on that below.
- Eye tracking: this doesn't work through the Utility under Proton. It can't see the Linux SteamVR or open the eye camera. Do not let the Utility flash eye-tracking firmware from Linux. The camera works fine natively (see Part 9).
1.4 The real deal-breaker: SteamVR on Linux only does 90 Hz
The Beyond 2E has two display modes:
| Mode | Resolution | Refresh |
|---|---|---|
| Native (preferred) | 5088ร2544 | 75 Hz |
| Reduced | 3840ร1920 | 90 Hz |
On Windows, Bigscreen ships their own SteamVR driver that enables the 75 Hz full-resolution mode. That driver is Windows-only. On Linux, SteamVR always targets 90 Hz. I tried:
steamvr.preferredRefreshRate = 75insteamvr.vrsettings: SteamVR strips it on startup.- Setting
display_frequency: 75in the headset's lighthouse config: ignored. - Choosing 75 Hz in the Beyond Utility: it can't reach SteamVR (see above).
None of them worked. So on SteamVR I was running a lower resolution and a tighter frame budget. VRChat was taking about 11.8 ms per frame on the GPU, and at 90 Hz the budget is 11.1 ms. That showed up as double images whenever I turned my head.
At 75 Hz the budget is 13.3 ms. Getting there meant switching runtimes.
1.5 Quality-of-life fixes I made along the way (SteamVR)
These still apply if you stay on SteamVR:
-
WayVR autostart: don't use
wayvr --install(WayVR itself warns that SteamVR-launched copies misbehave). Use a user systemd service instead (Part 5). -
OVR Advanced Settings crashed on start (segfault), both the Steam version and the AUR version. Qt5 can't create an OpenGL context on NVIDIA + KDE Wayland. Fix: run it under XWayland with EGL:
#!/usr/bin/env bash # ~/.local/bin/ovras-steamvr โ launcher for the AUR `ovras` package unset LD_LIBRARY_PATH export QT_QPA_PLATFORM=xcb QT_XCB_GL_INTEGRATION=xcb_egl cd /opt/AdvancedSettings || exit 1 exec /opt/AdvancedSettings/AdvancedSettings "$@"Point its
.vrmanifestat that wrapper and register the manifest inSteam/config/appconfig.jsonso SteamVR autolaunches it. You'll also needqt5-quickcontrols2andqt5-svg.
Part 2: Moving to Monado + xrizer
Monado is the open-source OpenXR runtime. You pick the display mode yourself, so the Beyond's 75 Hz mode just works. It can't run OpenVR games (VRChat is one) on its own. xrizer fills that gap: it translates OpenVR to OpenXR.
How it fits together:
VRChat (OpenVR, Proton) โโโบ xrizer โโโบ Monado (OpenXR) โโโบ Beyond 2E @ 75 Hz
โ
WayVR overlay (OpenXR) โโโโโโโโโโโโโโโโโโโ
โ
SteamVR's lighthouse driver (steamvr_lh) โโโบ Index controllers,
Vive trackers, base stations
Monado reuses SteamVR's own lighthouse driver for tracking. You still need SteamVR installed, but it never runs.
2.1 Packages
paru -S monado-git xrizer lib32-xrizer wayvr
- Use
monado-git, notmonado. The AURmonadorelease (25.1.0) crashed in its SteamVR lighthouse plugin with current SteamVR (2.17). The git version works. - You don't need
libsurvive. It's an alternative tracking driver; it didn't even build for me, andsteamvr_lhis better anyway. - xrizer installs to
/opt/xrizer.
2.2 Monado service settings (the important bit)
Monado runs as a user systemd service (monado.service, started by monado.socket). I configure it with a drop-in file:
# ~/.config/systemd/user/monado.service.d/beyond.conf
[Service]
# Use SteamVR's lighthouse driver for the Beyond, Index controllers and Vive trackers.
Environment=STEAMVR_LH_ENABLE=true
# Display mode 0 on the Beyond = 5088x2544 @ 75 Hz (full res).
Environment=XRT_COMPOSITOR_DESIRED_MODE=0
# KDE Wayland: lease the headset display through Wayland (the X11 path fails on KDE).
Environment=XRT_COMPOSITOR_FORCE_WAYLAND_DIRECT=true
# NVIDIA stability (see Part 3): graphics queue instead of compute, plus pacing workarounds.
Environment=XRT_COMPOSITOR_COMPUTE=false
Environment=XRT_COMPOSITOR_USE_PRESENT_WAIT=1
Environment=U_PACING_COMP_TIME_FRACTION_PERCENT=90
# Monado can't add lighthouse devices after startup. Give them 20 s to show up.
Environment=LH_DISCOVER_WAIT_MS=20000
Then systemctl --user daemon-reload.
What each setting does:
XRT_COMPOSITOR_DESIRED_MODE=0is how you get 75 Hz. Mode 0 is the first (preferred) mode the headset reports. Checkjournalctl --user -u monado.servicefor a line confirming 5088x2544@75.XRT_COMPOSITOR_FORCE_WAYLAND_DIRECT=trueis required on KDE Wayland. Without it, the X11 direct-mode path fails.LH_DISCOVER_WAIT_MSis the big gotcha. Monado's lighthouse driver only accepts devices that appear during the first few seconds after startup (3 s by default). A controller that's still powering up gets missed, and the only way to get it back is to restart Monado. 20 seconds gives everything time to connect. Turn on your controllers and trackers before starting Monado.
2.3 Switching runtimes
Two files decide which runtime apps use:
- OpenXR:
~/.config/openxr/1/active_runtime.json, a symlink to the runtime's JSON. - OpenVR:
~/.config/openvr/openvrpaths.vrpath, whose"runtime"entry points either to SteamVR or to/opt/xrizer.
I wrote two scripts to swap everything at once. vr-monado:
#!/usr/bin/env bash
# Switch VR stack to Monado + xrizer (75 Hz full-res Beyond). Undo with: vr-steamvr
set -euo pipefail
steam_cfg=~/.local/share/Steam/config
openvr_paths=~/.config/openvr/openvrpaths.vrpath
# SteamVR must not be running (it holds the headset and rewrites its settings on exit).
if pgrep -x vrserver >/dev/null; then
echo "Closing SteamVR..."
kill $(pgrep -x vrserver) $(pgrep -x vrcompositor) $(pgrep -x vrdashboard) 2>/dev/null || true
for _ in $(seq 1 10); do pgrep -x vrserver >/dev/null || break; sleep 1; done
kill -9 $(pgrep -x vrserver) $(pgrep -x vrcompositor) $(pgrep -x vrdashboard) 2>/dev/null || true
fi
# Don't let controller button presses auto-launch SteamVR while Monado owns the devices.
jq '.power.autoLaunchSteamVROnButtonPress = false' "$steam_cfg/steamvr.vrsettings" > "$steam_cfg/steamvr.vrsettings.tmp" \
&& mv "$steam_cfg/steamvr.vrsettings.tmp" "$steam_cfg/steamvr.vrsettings"
# OpenVR apps (VRChat) -> xrizer
jq '.runtime = ["/opt/xrizer"]' "$openvr_paths" > "$openvr_paths.tmp" && mv "$openvr_paths.tmp" "$openvr_paths"
# OpenXR apps (WayVR, xrizer) -> Monado
ln -sf /usr/share/openxr/1/openxr_monado.json ~/.config/openxr/1/active_runtime.json
echo "WAYVR_BACKEND=--openxr" > ~/.config/wayvr/backend.env
systemctl --user start monado.socket
systemctl --user restart monado.service # restart, not start: start is a no-op if it's already up
systemctl --user restart wayvr.service
vr-steamvr does the reverse: it stops Monado, points openvrpaths.vrpath back at ~/.local/share/Steam/steamapps/common/SteamVR, symlinks active_runtime.json to SteamVR's steamxr_linux64.json, sets WAYVR_BACKEND=--openvr, and removes the auto-launch override.
Back up openvrpaths.vrpath before you touch it.
Part 3: The NVIDIA GPU lockup (Xid 51), and how I made sure it never happens again
This was the scariest part. The first time I started Monado, my whole GPU locked up. dmesg showed:
NVRM: Xid 51 ... BAD_TSG
NVRM: Xid 154 ... GPU recovery action changed ... (PF FLR)
After that every Vulkan app (Steam's web helper, vulkaninfo) failed with NV_ERR_RESET_REQUIRED until I rebooted.
Cause: the monado-git package's install script grants monado-service the cap_sys_nice capability. With it, Monado asks the NVIDIA driver for a real-time-priority, compute-only GPU queue (the log says COMPUTE queue with QUEUE_GLOBAL_PRIORITY_REALTIME). On this driver that blew up. The way I'd stopped my test run (killing it with timeout while it was still holding the display) probably didn't help.
Fix (three parts):
-
Remove the capability. Monado then uses a normal MEDIUM-priority graphics queue:
sudo setcap -r /usr/bin/monado-service -
The compositor settings in
beyond.confabove (XRT_COMPOSITOR_COMPUTE=false,USE_PRESENT_WAIT=1, pacing at 90%). -
Always stop Monado cleanly with
systemctl --user stop monado.service. Neverkill -9ortimeoutit.
After that, Monado's log showed GRAPHICS queue with QUEUE_GLOBAL_PRIORITY_MEDIUM and it's been stable since.
The catch: every monado-git update puts the capability back. It's already happened to me once. Two safety nets:
A pacman hook that removes it automatically after every install or upgrade:
# /etc/pacman.d/hooks/90-monado-no-rtcap.hook
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = monado-git
Target = monado
[Action]
Description = Removing real-time priority from monado-service (NVIDIA Xid 51 workaround)...
When = PostTransaction
Exec = /usr/bin/sh -c 'if [ -n "$(getcap /usr/bin/monado-service)" ]; then setcap -r /usr/bin/monado-service; fi'
A check in my start script that refuses to launch if the capability is back (see Part 7).
Check it any time with getcap /usr/bin/monado-service. Empty output means you're safe.
Part 4: VRChat on Monado
VRChat runs under Proton inside Steam's container (pressure-vessel). By default the container can't see the host's OpenXR runtime or Monado's socket, so xrizer fails with ERROR_RUNTIME_UNAVAILABLE and VRChat silently opens in desktop mode.
Fix: set VRChat's Steam launch options to:
PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1 PRESSURE_VESSEL_FILESYSTEMS_RW=$XDG_RUNTIME_DIR/monado_comp_ipc %command%
PRESSURE_VESSEL_IMPORT_OPENXR_1_RUNTIMES=1exposes the host's OpenXR runtime (Monado) inside the container.PRESSURE_VESSEL_FILESYSTEMS_RW=โฆ/monado_comp_ipcgives the game access to Monado's IPC socket.
These options don't hurt anything when you're on SteamVR, so you can leave them set.
Other things to know:
- A black headset before any app starts is normal under Monado (I have WayVR's skybox turned off). It isn't broken.
- Dynamic resolution tools (e.g. OVR Dynamic Resolution) don't work on Monado. They change SteamVR's supersample setting and read SteamVR's frame timing. Under xrizer, the settings call quietly does nothing and the timing numbers are hard-coded placeholders. Monado's resolution is fixed when the service starts (
XRT_COMPOSITOR_SCALE_PERCENTAGE). If you need more performance, lower that. - Full-body trackers work through
steamvr_lhas long as they're on before Monado starts. You can give xrizer your tracker serial numbers withXRIZER_TRACKER_SERIALS. They're separated by semicolons, not commas.
Part 5: WayVR, a desktop overlay that works on both runtimes
WayVR (formerly WlxOverlay-S) gives you your Wayland desktop, a keyboard and a watch inside VR. It speaks both OpenVR (SteamVR) and OpenXR (Monado), so it's the one overlay that works in both setups.
5.1 Run it as a service
# ~/.config/systemd/user/wayvr.service
[Unit]
Description=WayVR overlay (waits for the VR runtime, restarts after it exits)
PartOf=graphical-session.target
After=graphical-session.target
[Service]
# --openvr for SteamVR, --openxr for Monado; switched by vr-monado / vr-steamvr
EnvironmentFile=%h/.config/wayvr/backend.env
ExecStart=/usr/bin/wayvr $WAYVR_BACKEND --wait
Restart=on-failure
RestartSec=5
I don't enable it at login. The start script starts it. Use Restart=on-failure, not Restart=always. When you change certain settings, WayVR relaunches itself outside the service. With always, the service copy kept failing against that stray copy, and it looped 90 times before I noticed.
5.2 Playspace drag on the B button (like OVR Advanced Settings)
OVR Advanced Settings is SteamVR-only, and I really missed grabbing and moving the playspace. WayVR has this built in (space_drag). On OpenXR it's bound to the left trackpad force by default, which is awkward.
Important: under OpenXR, WayVR does not use the actions_binding_*.json files (those are OpenVR-only). It reads ~/.config/wayvr/openxr_actions.json5. Copy the default from WayVR's assets and edit the Index section:
// Index controller (excerpt)
grab: {
// light squeeze grabs (the default threshold needed a hard squeeze)
left: "/user/hand/left/input/squeeze/force",
right: "/user/hand/right/input/squeeze/force",
threshold_left: [0.05, 0.15], // [release, activate]
threshold_right: [0.05, 0.15]
},
show_hide: {
// moved off left B (B is now space_drag)
double_click: true,
left: "/user/hand/left/input/system/click",
},
space_drag: {
// hold B on either hand to drag the playspace
left: "/user/hand/left/input/b/click",
right: "/user/hand/right/input/b/click",
},
In WayVR's saved config I also set space_gravity_enabled: false and left space_drag_unlocked / space_rotate_unlocked on, so you can move and rotate freely in 3D.
Gotcha: WayVR loads every file in
~/.config/wayvr/conf.d/. Don't keep.bakcopies in there. Put backups one folder up.
5.3 Other WayVR settings that helped on NVIDIA
capture_method: PipeWireCpu, use_passthrough: false, use_skybox: false.
Part 6: Base stations: turning them on and off without SteamVR
SteamVR wakes your base stations when it starts and puts them to sleep when it exits. Monado doesn't, so mine kept spinning (wear and that faint whine) until I unplugged them.
Base Station 2.0s are controlled over Bluetooth LE: you write one byte to a "power" characteristic. I first tried the AUR lighthouse tool (vr-lighthouse-bin). It found both stations but couldn't connect to them. Connecting through BlueZ directly (bluetoothctl, busctl) worked fine, so I wrote a small script that uses the system's Bluetooth stack directly and needs nothing extra installed.
What I learned:
| Value | Meaning |
|---|---|
write 1 |
wake up |
write 0 |
sleep |
read 0 |
asleep |
read 2 |
standby |
read 9 |
booting |
read 11 |
on |
- The power characteristic UUID is
00001525-1212-efde-1523-785feabcd124. - Don't send "on" to a station that's already on. It goes into an odd state and reads
1from then on. The script checks first. - BlueZ forgets unpaired LE devices shortly after they disconnect, so
Connectfails with "method doesn't exist". The fix is a short scan before retrying.
Find your stations' addresses with bluetoothctl --timeout 10 scan le, then bluetoothctl devices | grep LHB.
#!/usr/bin/env bash
# ~/.local/bin/basestations โ wake or sleep Lighthouse 2.0 base stations via BlueZ (busctl).
# Usage: basestations on|off|status
set -uo pipefail
MACS=(AA:BB:CC:DD:EE:01 AA:BB:CC:DD:EE:02) # <- your LHB-xxxxxxxx addresses
POWER_UUID=00001525-1212-efde-1523-785feabcd124
case "${1:-}" in
on) value=1 ;; off) value=0 ;; status) value= ;;
*) echo "usage: basestations on|off|status" >&2; exit 2 ;;
esac
dev_path() { echo "/org/bluez/hci0/dev_${1//:/_}"; }
prop() { busctl get-property org.bluez "$1" org.bluez.Device1 "$2" 2>/dev/null | awk '{print $2}'; }
# Connect and wait until GATT services are resolved. BlueZ forgets unpaired LE devices soon after
# they disconnect, so scan briefly whenever the device is unknown or the connect fails.
connect() {
local dev; dev=$(dev_path "$1")
for _ in 1 2 3; do
if [ "$(prop "$dev" Connected)" != true ] \
&& ! busctl call org.bluez "$dev" org.bluez.Device1 Connect >/dev/null 2>&1; then
bluetoothctl --timeout 6 scan le >/dev/null 2>&1
continue
fi
for _ in $(seq 1 20); do
[ "$(prop "$dev" ServicesResolved)" = true ] && return 0
sleep 0.5
done
done
return 1
}
power_char() {
local c
for c in $(busctl tree org.bluez --list 2>/dev/null | grep "^$(dev_path "$1")/service[0-9a-f]*/char[0-9a-f]*$"); do
if busctl get-property org.bluez "$c" org.bluez.GattCharacteristic1 UUID 2>/dev/null | grep -q "$POWER_UUID"; then
echo "$c"; return 0
fi
done
return 1
}
state_name() { case "$1" in 0) echo asleep ;; 2) echo standby ;; 11) echo on ;; 9) echo booting ;; *) echo "unknown ($1)" ;; esac; }
handle() {
local mac=$1 char state
if ! connect "$mac"; then echo "$mac: couldn't connect" >&2; return 1; fi
if ! char=$(power_char "$mac"); then echo "$mac: power control not found" >&2; return 1; fi
state=$(busctl call org.bluez "$char" org.bluez.GattCharacteristic1 ReadValue 'a{sv}' 0 | awk '{print $3}')
# Writing "on" to a station that's already on leaves it reporting 1; only wake it from sleep/standby.
if [ "$value" = 1 ] && [ "$state" != 0 ] && [ "$state" != 2 ]; then
echo "$mac: already $(state_name "$state")"
elif [ -n "$value" ]; then
busctl call org.bluez "$char" org.bluez.GattCharacteristic1 WriteValue 'aya{sv}' 1 "$value" 0 \
|| { echo "$mac: write failed" >&2; return 1; }
echo "$mac: $2"
else
echo "$mac: $(state_name "$state")"
fi
busctl call org.bluez "$(dev_path "$mac")" org.bluez.Device1 Disconnect >/dev/null 2>&1
}
rc=0
for mac in "${MACS[@]}"; do handle "$mac" "$1" || rc=1; done
exit $rc
Part 7: One command to start, one to stop
Monado has a lot of steps that have to happen in order: devices on first, wait for discovery, the right mic, and so on. So I wrapped everything into two scripts in ~/.local/bin.
play-vr
In order:
- Safety check: refuses to start if
monado-servicehas its real-time capability back (Part 3). - Wakes the base stations in the background. They spin up while Monado is still searching for devices.
- Switches to the
performancepower profile (powerprofilesctl) and saves the old one so it can be restored later. - Runs
vr-monado, then watches Monado's log until it has found the HMD, 2 controllers and 3 trackers. As soon as all of them show up it moves on, without waiting the full 20 seconds. - If something is missing, it asks: R restarts Monado (after you turn the missing device on), Enter carries on anyway. Monado can't add devices later, so this saves finding out once you're already in VRChat.
- Switches the default microphone to the Beyond's built-in mic (
pactl set-default-source). - Launches VRChat (
steam steam://rungameid/438100), then the companion apps: OSC-DreamChatbox, Baballonia and VRCFaceTracking.
The device-wait part, which is the trickiest:
monado_log() { journalctl --user -u monado.service --since "@$start" -o cat --no-pager 2>/dev/null; }
count() { grep -c "Found lighthouse $1" <<<"$log" || true; }
start_monado() {
start=$(date +%s)
~/.local/bin/vr-monado >/dev/null
all_found=false
for _ in $(seq 1 60); do
log=$(monado_log)
if [ "$(count HMD)" -ge 1 ] && [ "$(count controller)" -ge 2 ] && [ "$(count tracker)" -ge 3 ]; then
all_found=true; break
fi
grep -q 'Created device and' <<<"$log" && break # discovery window finished
systemctl --user is-active --quiet monado.service || { echo "Monado failed" >&2; exit 1; }
sleep 1
done
}
start_monado
while ! $all_found && [ -t 0 ]; do
read -r -p "Turn the missing ones on, then press R to restart Monado, or Enter to continue anyway: " answer || answer=""
case "$answer" in [rR]*) start_monado ;; *) break ;; esac
done
For the mic, find your device name with pactl list short sources. The Beyond's shows up as alsa_input.usb-Bigscreen_Beyond_<serial>-00.analog-stereo.
stop-vr
- Closes VRChat politely (TERM), waits up to 15 s, then force-kills it if needed. Does the same for DreamChatbox, Baballonia and VRCFaceTracking.
systemctl --user stop wayvr.service monado.service monado.socket. This is a clean stop, which matters on NVIDIA.- Kills any stray WayVR and leftover Proton/Wine processes from VRChat's prefix (
compatdata/438100). - Switches the mic back to my desk mic.
- Puts the base stations to sleep.
- Restores the previous power profile.
- Runs
backup-vr(next part).
Part 8: Backups and a cheat sheet
This setup lives in a lot of different files, and any update to monado-git, xrizer or WayVR could change one of them. So stop-vr takes a snapshot after every session:
backup-vr puts all of these into ~/vr-backups/vr-stack-<date>.tar.gz and keeps the newest 20:
- every script
- the Monado drop-in and the WayVR service
- the runtime selection files
- the whole WayVR config
steamvr.vrsettingsand appconfig- the OVRAS manifest
- the Beyond Utility settings
- the headset's lighthouse config backup
- Steam's
localconfig.vdf(where VRChat's launch options live) - the Baballonia settings
- the pacman hook
It also writes a STATE.txt with:
- package versions
- the active runtime
getcapoutput- default mic/output
- power profile
- unit states
Restore a single file with:
tar -xzf ~/vr-backups/vr-stack-<date>.tar.gz -C / home/<you>/<path-to-file>
Finally, I have a lot of commands to remember, so vr-help prints a one-page cheat sheet: every command, the headset button layout, and the fix for each problem I've hit. vr-help <word> searches it (for example vr-help tracker).
Part 9: Eye tracking with Baballonia (in progress)
The Beyond 2E's eye-tracking camera ("Bigeye") is a standard USB camera. On Linux it shows up as /dev/video0 (MJPG, 800ร400 @ 90 fps) and needs no special driver. The Beyond Utility can't use it under Proton, but Baballonia from Project Babble runs natively.
Skip the AUR packages and use the upstream build. The AUR baballonia depends on babbletrainer, which needs the AUR opencv4 package. That's a long compile, and it fights with the OpenCV 5 that Arch ships. The upstream Linux x64 release bundles everything it needs (.NET runtime, ONNX Runtime, the trainer, the calibration overlay), so you don't need any of that:
mkdir -p ~/.local/opt/baballonia
curl -LO https://github.com/Project-Babble/Baballonia/releases/download/v1.1.1.0rc6/Baballonia.x64.v1.1.1.0rc6.tar.xz
tar -xJf Baballonia.x64.v1.1.1.0rc6.tar.xz -C ~/.local/opt/baballonia
cat > ~/.local/bin/baballonia <<'EOF'
#!/usr/bin/env bash
cd ~/.local/opt/baballonia && exec ./Baballonia.Desktop "$@"
EOF
chmod +x ~/.local/bin/baballonia
To get the tracking into VRChat, install VRCFaceTracking (paru -S vrcft-avalonia-bin, then run vrcft) and add the Baballonia module from its module list.
Baballonia talks to the headset through xrizer, the same way VRChat does, so start it after Monado is up. play-vr handles that.
Setup steps:
- In Baballonia, choose the Bigeye camera for both eyes and crop each eye.
- In VRCFaceTracking, install the Baballonia module.
- In the headset, run Baballonia's calibration.
- In VRChat, enable OSC (Action Menu โ Options โ OSC) and use an avatar that supports eye tracking.
Status: installed and starting cleanly. I haven't run the in-headset calibration on Monado yet, and its overlay is an OpenVR app going through xrizer, so that's the part most likely to need work. I'll update this section once it's done.
Gotchas cheat sheet
| Symptom | Cause / fix |
|---|---|
| Rainbow static in one eye | NVIDIA DSC bug. Use a CachyOS kernel + NVIDIA driver that include the Beyond patches (main kernel, not LTS). |
| Stuck at 90 Hz on SteamVR | That's a Linux SteamVR limitation. Use Monado with XRT_COMPOSITOR_DESIRED_MODE=0. |
| GPU locks up, Xid 51, Vulkan dead until reboot | sudo setcap -r /usr/bin/monado-service, add the pacman hook, and always stop Monado cleanly. |
monado (AUR release) segfaults on start |
Use monado-git. |
| Controller or tracker missing | It was off during Monado's discovery window. Set LH_DISCOVER_WAIT_MS=20000, turn devices on first, restart Monado. |
| VRChat opens in desktop mode | Add the PRESSURE_VESSEL_* launch options. |
| Headset black under Monado | Normal until an app is running. |
| WayVR service restart-loops | Restart=on-failure, and kill stray wayvr processes before starting. |
| WayVR binding changes do nothing on Monado | Edit openxr_actions.json5, not actions_binding_*.json. |
| OVR Advanced Settings segfaults | QT_QPA_PLATFORM=xcb QT_XCB_GL_INTEGRATION=xcb_egl (SteamVR only; it doesn't work on Monado). |
| Base stations never sleep | Monado doesn't manage them. Use the BlueZ script. |
| Beyond Utility: "Invalid SteamVR folder" | Can't be fixed on Linux. Set things by hand. |
| Dynamic resolution doesn't work | Not supported on Monado + xrizer. Use XRT_COMPOSITOR_SCALE_PERCENTAGE. |
Credits
Huge thanks to the people behind Monado, xrizer, WayVR, Project Babble, VRCFaceTracking, the CachyOS team (for carrying the Beyond DSC patches), and everyone in the Linux VR community. Every piece of this setup is built on their work.