A Weird Imagination

Lightweight multiseat X

Posted in

The problem#

I hosted a LAN party1 a little while ago and ended up needing to loan out multiple computers to guests in the interest of having no one try to lug their desktop over. As it turns out, I don't keep multiple of spare gaming-ready laptops around, so I needed to get more computers somehow.

The solution#

My desktop has three screens attached to it (two monitors plus a projector), so given an extra keyboard and mouse (or two), it should be possible to run multiple instances of the game on it at the same time to let multiple people play using the same computer.

The script from this forum post makes it easy to set up multi-pointer X so a second keyboard and mouse will get its own mouse cursor. Then each keyboard and mouse pair can interact with its own instance of the game.

As an additional aid, I wrote monitor-lock.py which allows you to assign a mouse to a monitor, so it cannot be moved off that monitor to prevent accidentally interacting with the other player's instance of the game.

The basic usage is that you first run it with no arguments to get the available screens and pointers getting an output something like this:

$ ./monitor-lock.py 
...
Available screens:
screen 0: {'x': 0, 'y': 0, 'width': 3840, 'height': 2160}
screen 1: {'x': 3840, 'y': 0, 'width': 1920, 'height': 1200}
screen 2: {'x': 3840, 'y': 1200, 'width': 1920, 'height': 1080}

Available pointers:
device 2: Virtual core pointer
device 17: second pointer

USAGE: ./monitor-lock.py [device] [screen]

and then in a screen session (so you don't have to worry about accidentally doing this on a monitor you've locked your pointer away from), run

./monitor-lock.py 2 0

and

./monitor-lock.py 17 1

to lock the primary pointer to the first screen and the second pointer to the second screen.

Just use Ctrl+C to kill the process when you want the pointer to be able to move freely again.

The details#

Real multiseat X#

Xorg does support running multiple different logins using different subsets of the devices attached to a computer. For a setup where you want the computer to really act like multiple computers, Xorg multiseat is a more appropriate solution. But it's more complicated to set up and is overkill for a setup where each user is only ever interacting with a single application and permissions are irrelevant.

Multi-pointer X#

As of XInput2, Xorg supports multiple mouse pointers. In practice, most applications act very strangely when this feature is used, it seems to work fine to have multiple instances of a keyboard and mouse game running and interact with them simultaneously with different pointers. Which is enough for what we want to do. As mentioned above, this script makes this very simple to set up.

Associating a mouse with a monitor#

The next question is just how to abuse to feature to make it look as much like a real multiseat setup as possible. Which means a given pointer should only be able to interact with its assigned part of the desktop. As most games run full screen on a single monitor, this is equivalent to just keeping each pointer on its own monitor.

The mechanism I used for this is pointer barriers, a feature which is intended for making certain areas of the screen slower for a mouse to move through to make easier to interact with or to detect the mouse moving through a certain area. But they can be used more simply to simply block the mouse from moving past a certain line on the screen.

The basic idea is to just construct four barriers, one on each edge of a given screen, then the mouse can't leave that screen. (Finding the screen boundaries is straightforward: xinerama.query_screens(window).) Additionally, barriers can be tied to a certain mouse pointer and can be made one-way. That means, the barriers for a given screen can be made to apply to only the pointer that belongs on that screen and that pointer can be allowed to move onto that screen but not off of it.

One last tweak is that instead of constraining the mouse cursor to the full width of the screen, I actually limit it to one pixel less on either side as I have my window manager (xfwm4) set up switch desktops when the mouse cursor moves quickly enough to the left or right edge of the screen, and I don't want that to be triggered accidentally while playing a game.

CreatePointerBarrier in Python#

I implemented monitor-lock in Python as I thought that would be easier than writing it in C... but it turns out python-xlib does not support the CreatePointerBarrier() function, so I had to implement it.

I found the information I needed to implement it by searching for CreatePointerBarrier in /usr/include/X11/extensions/ and finding the xXFixesCreatePointerBarrierReq struct in xfixesproto.h2 and the of 31 for XFixesCreatePointerBarrier in xfixeswire.h. I also searched for CreatePointerBarrier in the source code of libxfixes and found the implementation in Cursor.c, which is the C equivalent of the code I wanted to write in Python. After that, I just looked at the rest of the python-xlib source to find examples of how other calls are implemented. The resulting translation to Python is the CreatePointerBarrier class and the create_pointer_barrier function in monitor-lock.py.

XFixes version#

The issue that took the most time to figure out was that once I appeared to be calling CreatePointerBarrier() correctly, it was still giving an error. Eventually, I figured out the reason was that python-xlib declared support for version 4 of XFixes, which apparently disables the version 5 features, including CreatePointerBarrier(). The fix was to explicitly declare support for version 5:

xfixes_version = xfixes.QueryVersion(
    display=window.display,
    opcode=window.display.get_extension_major(xfixes_extname),
    major_version=5, minor_version=0)

  1. We were playing Factorio, which is a great co-op LAN party game. And also does not require a particularly powerful computer to run. 

  2. Unfortunately, I was unable to find online-browseable versions of these source files. You should be able to install them on your own system by running apt install x11proto-dev; apt source libxfixes-dev or the equivalent package manager commands for you distribution. 

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.