Skip to content

Preparing UVM for 3D Graphics

November 1, 2023

Since the beginning of this year, I’ve been casually working on UVM, a project to a minimalistic virtual machine that is portable and easy to target. As part of this project, I’ve also been working on a toy C compiler to make creating software for this VM easier. I first wrote about UVM on February 24th, and got a fairly mixed reaction on Hacker News. Many people couldn’t see the appeal/interest in creating yet another VM or bytecode format when there’s already technology such as the JVM and WASM out there.

I’m doing this in part for the learning experience, and also because I think it’s fun. The end goal is to end up with a VM that has a very simple architecture that’s easy to understand and a stable foundation to build software on, without having to worry about issues of portability. Part of my inspiration comes from retrocomputing. Computers used to be simple. You used to be able to power on a Commodore 64 and immediately write BASIC code. Programming today can be a lot more complex. It’s hard to understand the entire machine, and just setting up a new development project can sometimes feels tedious. I’d like UVM to be simple to use, but also capable of effectively using the capabilities of modern computers, without arbitrary restrictions.

Since I last wrote about UVM, I’ve spent a fair bit of time improving NCC, the toy C compiler, adding features like typedefs, structs, and a custom C preprocessor with macros. Writing a C compiler has made me realize that there’s more complexity to C than meets the eye. The compiler is still missing several features, but it’s already quite capable, fun to use and fairly well tested. I wrote several example C programs that can run on UVM, including a a 2.5D raycaster, and a pentatonic step sequencer. I had fun writing these programs. It’s a different experience being able to do graphics and audio in C without having to think about portability and without any boilerplate. I’ve also somehow convinced Oscar Toledo to port nanochess to UVM, and Abdul Bahajaj to write a simple BASIC interpreter with graphics capabilities. Oscar reported 3 bugs in the C compiler, which I quickly fixed.

The way graphics work in UVM is that there is a system call (an API call into the host VM), to create a window, which has an associated RGBA32 frame buffer. An entire frame of pixels can be copied and displayed into the window with another system call. Because each pixel is 32 bits, individual pixels can be manipulated with a single 32-bit store instruction. There’s also a memset32 instruction which makes it possible to fill rows of pixels really fast as this can use SIMD instructions internally. That’s all there is to UVM the graphics API. Everything else is done in software for now. I may eventually add another system call that does fast RGBA32 blitting with alpha blending (loosely inspired by the Amiga), so that UVM can render GUIs really fast, but I haven’t decided yet.

I thought it would be extra cool if I were able to do 3D graphics on UVM. It currently runs on an interpreter, but in release builds, the interpreter is able to hit about 500 MIPS (million instructions per second) on my MacBook Air M1, which should be comparable to the speed of a Pentium II, and sufficient for some basic rasterization or even just wireframe 3D graphics. I started by adding floating-point support to the VM and compiler so that I could do vector and matrix math. Then I used ChatGPT to implement some OpenGL-like functions to generate rotation and perspective matrices, and transform 3D points. I don’t think I saved any time doing that, because it took me a while to verify and fix up the output that came out of ChatGPT, but I did end up with working code.

To represent 3D vectors and 4×4 matrices, I created typedefs for floating-point arrays. In order to be able to use these comfortably in my C code, I also needed to add support in NCC for stack-allocating array variables. Without that, every array variable would need to be a global. This was a bit tricky to do because UVM doesn’t allow you to directly take the address of something on the stack (like a stack-allocated array) for performance reasons, and so if you want to take the address of things on the stack, the compiler needs to manage a separate stack for the things you take the address of. Once I got that working, I was able to put all the pieces together, and render a glorious 3D rotating wireframe cube.

In terms of future plans, I’d like to add a simple asynchronous TCP networking API to UVM. That will make it possible to write something like a simple web server running on top of UVM, or maybe something like a custom bulletin board or a chat that can run without a web browser. I’m also thinking that it might be fun to create something like a little 64KB demoscene competition. In that spirit, I added some demos implementing demoscene fire and plasma effects based on Lode Vandevenne’s excellent tutorials.

If you think that UVM sounds like a cool project and you’d like to contribute, there are some open issues on GitHub. I can use feedback on various design issues. I also really welcome bug reports, and new example programs to showcase what’s possible with UVM. All of the examples are licensed as CC0 (public domain) so that anyone can freely reuse the code and remix them. I would love to see new cool demoscene effects, games and audio/music programs, or anything fun, creative or useful you can come up with. If you find any of this interesting, feel free to reach out via GitHub issues and discussions or via twitter/X.

From → Compilers, UVM

Leave a Comment

Leave a comment