Skip to main content

WlxOverlay and the Linux Input Stack

·744 words·4 mins
Linux VR
Table of Contents

A view of WlxOverlay from a VR headset

What Is WlxOverlay?
#

WlxOverlay is a Linux OpenXR application that allows users to control and mirror their Wayland/X11 desktop in VR. VR is already niche, even on W*indows, so I was extremely pleased to find this software supporting the also niche Linux platform. And it’s being actively maintained in a modern programming language! Nice.

After figuring out how to actually connect my Quest 2 headset to my computer running Fedora linux (shoutout WiVRn), and attempting to run WlxOverlay, I quickly ran into an issue, Whenever I used the virtual keyboard, mouse functionality would then be broken until I restarted the application. After trying all the usual tricks like restarting and updating, the problems persisted, so I decided, hey I know how to code, why not fix this myself. But there was a small issue. This was a full fleged multi thousand line Rust application… and I had never written a line of Rust code before.

Fixing the Issue
#

Now a wiser person than I would probably have attempted to learn the language before delving into linux emulated input devices in Rust, but what I lacked in wisdom I made up for in pure stubbornness.

After stumbling around the code for a few hours and a lot of “What does this code do” queries to chatbots, I found the module where the hid inputs were being handled. The code was using uinput, a linux kernel module to create a virtual input device and send mouse and keyboard inputs through it to the desktop whenever the corresponding input was made from inside the VR headset. The fist thing I did was add a print statement to verify that the code was still trying to send mouse inputs, which it was, so the problems were somewhere lower than that.

This prompted me to look into the linux input stack. Essentially when an input device is connected, the following happens:

  1. Udev monitors /sys for new devices and when one is detected it is registered and identified. if it is an input device a character device file will be created eg: /dev/input/event0
  2. Evdev relays events from the device driver to the character device file
  3. libinput allows you to coordinate with Udev and libevdev (api for evdev), exposing processed input information that desktop environments can use

So to create a virtual device, uinput has to create an entry in /sys with the correct metadata so that udev will identify it as the correct input device, and it will also handle writing events from the caller of uinput to the character device file. From there libinput will pick it up and it will be treated as a normal input device.

Heres an example of running sudo libinput debug-events while continuously moving my virtual mouse:

event24  POINTER_MOTION_ABSOLUTE 121 +60.893s     31.45/ 95.61
event24  POINTER_MOTION_ABSOLUTE 122 +60.901s     31.22/ 96.95
event24  POINTER_MOTION_ABSOLUTE 123 +60.910s     30.98/ 98.47
event24  KEYBOARD_KEY                +62.001s    *** (-1) pressed
event24  KEYBOARD_KEY                +62.143s    *** (-1) released
event24  POINTER_BUTTON              +64.485s     BTN_LEFT (272) pressed, seat count: 1
event24  POINTER_BUTTON              +64.602s     BTN_LEFT (272) released, seat count: 0

As you can see, there are no more pointer events after using my virtual keyboard, but interestingly, virtual mouse clicks still go through. I was a bit stumped at this point so I decided to look through the rest of the existing implementation in WlxOverlay. Heres some adapted code below:

impl UInputProvider {
    fn try_new() -> Option<Self> {
        if let Ok(file) = File::create("/dev/uinput") {
            let handle = UInputHandle::new(file);

            let id = InputId {
                bustype: 0x03,
                vendor: 0x4711,
                product: 0x0829,
                version: 5,
            };

            // I'm not kidding this is what it was called
            let name = b"WlxOverlay-S Keyboard-Mouse Hybrid Thing\0";
        }
        /*
        more code
        ...
        */
        if handle.create(&id, name, 0, &abs_info).is_ok() {
            return Some(UInputProvider {
                handle,
                desktop_extent: Vec2::ZERO,
                desktop_origin: Vec2::ZERO,
                current_action: Default::default(),
                cur_modifiers: 0,
            });
        }
        None
    }
    fn mouse_move_internal(&mut self, pos: Vec2) {/*use handle.write() to send pointer movements*/}
    fn send_button_internal(&self, button: u16, down: bool) {/*use handle.write() to send mouse clicks*/}
    fn send_key(&self, key: VirtualKey, down: bool) {/*use handle.write() to send keyboard keys*/}
}

Thats right, instead of a virtual mouse and keyboard, one unholy “WlxOverlay-S Keyboard-Mouse Hybrid Thing\0” amalgamation device was being used to send both mouse and keyboard events. With the help of Claude, I split this into two handles with separate mouse/keyboard responsibilities. This ended up resolving the issue, and once I felt I understood the code I submitted a PR which was merged.

References
#

https://john-salamon.com/The_Linux_Input_Complex/