xrxs deep dive
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!
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 (cartridges), download the game code, inquire about game rooms (realms), 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.
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 cartridges 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 cartridges, 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 cartridges 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 cartridges 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.
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.