xrxs deep dive
intro
The past few months, I've been working on xrxs in my spare time. It's the manifestation of an idea I've had for a while, a simple platform for online multiplayer gaming, more or less operating-system independent, and versatile enough to be used for many types of games or even collaborative applications (eg, a multi-user document editor or graphics program). Originally, I wanted to create this platform in-browser, and considered using Blazor since it offered compilation to WebAssembly and some neat features, but I got busy with work and tabled the idea soon after starting to learn the framework. After seeing Uxn start to take shape over the first half of 2021, I was enamored with the platform at first sight. In June of 2021 it occured to me to start fresh with my multiplayer online game platform idea. I'd been diving back into Plan9 and its related technologies, and it hit me that a rather elegant stack would be a 9p server, and what has come to be called the Varvara Computer as the basis for the client. The former abstracts away many of the painful things about network programming, and the latter provides a simple and portable basis for all the necessities of a gaming system. After many late nights and countless debugging sessions, I believe the system is ready for use. So without further ado, let's dive in!
server
The xrxs project page and the readme in the hack lab do a decent job of explaining the basics of how the server works, but long story short, the server is a 9p
server written in C, which exposes various resources to its clients as a filesystem. This includes "files" which can be read to inquire about the games that can be played (cartridge
s), download the game code, inquire about game rooms (realm
s), see who is in the same room with you, and get server-generated random numbers (ie, random numbers all the clients agree on); as well as those which can be both read from and written to to give commands to the server and see their success/failure status, and to read and write the shared game state.
The basic flow of it is like this. The server posts its 9p
filesystem on a network socket (multiplexed with the power of 9pserve to allow multiple simultaneous connections) and awaits clients. The client makes a connection, and "attaches" in 9p
terms. Upon attaching, the username of the user who ran the client is stored in a users table. The client can then read the carts
file to get a list of cartridges
. After choosing one, they can write to the ctl
file load CART
to load a cartridge named "CART" into the slot
file. Then, they read the slot
to download the cartridge. From that point on, the client is running in the context of the cartridge
, and would typically use the ctl
file to create or join a realm
, and then start reading and writing to the universe
and scope
files to interact with the game world (including other players).
When the client is done, before it unmounts the 9p
filesystem, it should issue a logout
command to the ctl
file to remove its username from the users table. If it doesn't, there's currently no way for the client to attach again with the same username until the server is restared. This is a bug I plan to fix with some sort of timeout, but I haven't gotten around to it yet.
client
This is where it gets fun. The client is the varvara
virtual machine, and it has no networking stack of its own. That's part of the beauty of 9p
, because it doesn't need one! varvara
can read and write files, which means it can speak 9p
. My initial tests proved that reads and writes to the 9p
filesystem through the varvara
VM worked, so after building out the server, I set to build the first client -- a uxn
ROM that acts as a cartridge
loader.
The assembly language of the uxn
virtual CPU is Uxntal, or tal for short. It's a simple language with 32 opcodes and postfix notation. Wrapping your head around it takes some getting used to, but once you get the hang of it, it's a very fun paradigm. Since the uxn
CPU is a stack machine, programming with uxntal
is all about pushing, popping, and swapping values on two stacks: a working stack, where computation is typically done; and a return stack, where values are typically placed temporarily to be retrieved later. I'm not going to dive too deep into the language itself, but other than the above link to the language spec (which contains some small demo code and other helpful reference materials) there is a great tutorial by Sejo at Compudanzas which goes over how to use the language and the resources of the varvara
VM step-by-step.
After getting the hang of basic operations, state handling, and drawing graphics, the first hurdle was listing the cartridge
s from the carts
file. I wrote the algorithm that parses the file three times before I got it to display the list. After that, it was pretty simple to get the keyboard and mouse controls to keep track of the "selected" cartridge
and scroll the list if it overflowed the viewing space.
What was actually pretty mind-bending was to properly render the scroll bar! One of those simple things you take for granted in modern computing. After a few false starts, though, I settled on a neat algorithm to render it. Basically, each tile is 8 pixels wide and 8 pixels tall. First, I count the number of cartridge
s, and compare it to the number of vertical tiles available to view them (ie, the height of the window/screen minus the grey borders). If the number of cartridge
s is less, we got it easy -- we can just draw the trough and forget the handle. But otherwise, we have some more math to do. First we take the quotient and store it, call it the ratio
(ie, the ratio of number of carts to available space, also the number of cartridge
s that should equal one tile on the scrollbar). This is in integer math, so it comes out to a whole number. Then, we take the difference
and divide it by the ratio
. This is the negative space in the scrollbar -- the room that the handle has to move around. Then, we subtract the negative space from the total height of the viewport to get the scrollbar-length
. So, at last, we can render the scrollbar. We already keep track of the list-top
, ie, the index of the cartridge
at the top of the viewport. So we divide list-top
by ratio
, and get the position of the scrollbar handle measured from the top, and then we draw as many tiles as we can until we either reach the scrollbar-length
or the bottom of the trough (which happens if ratio
is even but the difference
is odd).
So after selecting the cartridge
and confirming that this is the one we want (note the overall UI resemblence to steppenwolf!), we just write load SOMECART
to the ctl
file. I had some trouble actually composing the command too. It took me a few tries but I ended up writing the cartridge
name to a "variable" (really just a spot in memory indicated by a label) at the same time as we draw the highlighted cartridge
name on the interface, since we already have the string isolated in memory at that point. The trick to reduce the complexity and avoid copying it a second time was to just place that variable directly after another region of memory containing the command prefix "load " (note the space), without a null byte at the end (which would indicate the end of the string if it was present). So when you read the command string, it keeps going until the end of the cartridge
name.
After we tell the server what cartridge
we want, it's available in the slot
file. So we have to actually load it into memory now. How to do it? Well, thanks to Andy, there's a library function for that! It takes care of all the nitty-gritty to make sure we load the new cartridge
at the beginning of memory and start running it from a clean slate. But don't just take my word for it, check it out:
And here's another silly demo showing that the xrxs
client ROM can infinitely load itself!
I also made a standalone version that acts as a generalized uxn
bootloader along the same principles. It reads a list of ROM names from a file called index
, and appends ".rom" to the end of your selection and then loads that file. This version can be used as a bootloader for uxn
on handhelds, embedded systems, or desktop in cases where you just want to load a menu to select standalone, offline ROMs.
what's next?
Surely, there are bugs to fix and improvements to be made to both server and client, but where this really leads now is... GAMES! I've got a few games brewing in my head, and will be trying my hand at getting them prototyped in the coming months as time allows. In the meantime, I encourage others to do the same, and email me with any bug reports, feature requests, or general comments about the system!
Big props to Devine and Andy for the wonders that are uxn
and varvara
, and also to Sigrid for giving me solid directions on how to write a 9p
service.