Dienstag, 7. Dezember 2021

CHIP-8 emulator with Kotlin multiplatform

My main spare time project got a bit boring after some years and I wanted to do something small, rewarding, refreshing to test drive some cool new things. For example I wanted to  see what the current state of Kotlin multiplatform develpment is, after it was alreday really enjoyable for JVM/JS development back in 2019 for me. I am also very interested in GraalVM, my mediocre experience from 2018/2019 needs to be overriden with something more happy. So I decided to implement a CHIP-8 emulator, like ... nearly everyone who's doing software development already did before I had the idea. So I did it when it wasn't cool anymore, is that okay?

CHIP-8

I won't waste your time explaining all the details - CHIP-8 is already so well known and implemented a dozen times for every platform you can think of, you can google that easily by yourself. I use my remaining words to say that I am very thankful for this blog, this post and this wikipedia page! Those three pages contain all the information you need to implement an emulator yourself. Additionally, there are some repositories where you can get ROMs, like a nice ROM to test your emulator, or some funny game ROMs.

Kotlin Unsigned Types

Kotlin the language is heavily influenced by the JVM, as it is its main target. There's some friction because of that when implementing something as low level as an emulator. For example it's very nice to just have language support for unsigned types, which makes a lot of sense for indices. The support however, ends where anything related to the JVM appears, which is unfortunately the case for array indexing: The API expects a signed int as an index. Since there is no implicit conversion in the language, a program counter that essentially could be an unsigned int, needs to be explicitly converted with statements like 

val firstByte = memory[programCounter.toInt()]
val secondByte = memory[(programCounter.toInt() + 1)]

which is not too nice. It's not super important, as CHIP-8 only uses 4k memory, so we don't need all the bits of the int, but nonetheless. Furthermore, there are no bitshift operators on bytes, neither signed nor unsigned, so conversion to integer is always necessary after fetching instruction bytes from the memory, like so:

val a = (firstByte.toUInt() shr 4) and 0b0000000000001111u

Kotlin when statements

When statements with subject allow for very nice matching code for the opcodes. Take a look at this, as I think it is really readable. Maybe in the future passing this into all OpCode constructors can be removed with contextual constructors. I had to smirk a bit, because my main source of info for the emulator recommended to just inline all the calls and don't do a lot of architecture, but yeah, I couldn't resist and think it was for the good.

Multiplatform

Spoiler alert: I wasn't able to complete the emulator for Kotlin native targets like Windows or Linux.

So I started implementing everything in the common source set that can be compiled to all supported platforms, I planned for JavaScript, Windows, Linux and JVM. The first minor thing that was missing was a Bitset. Was able to resolve that with expect/actual pairs that pointed to the JVM implementation and implement a simple one for native by myself. The next thing that was missing in common sources was input handlig. I wasn't able to just access the native APIs, I didn't even manage to get autocompletion working. I then tried to just add two multiplatform libraries for input handling and resigned after some weird linkage errors I wasn't able to resolve. I wasn't able to be successful after 4 hours of work and I don't have that much time, so I conclude that the state of kotlin multiplatform for native targets hasn't changed that much, compared to my last try in 2019.

GraalVM

Now this one was really interesting for me. Simply bundling an existing web application with a (normally big) bunch of dependencies wasn't easy or even possible in 2019, as the ecosystem lacked tooling for kotlin, reflection, graalvm and the native image tool. This time, I had this nice gradle plugin, which is how I would wish tooling to be. Sadly, this time I had to use Windows, and for Windows, one needs to do some additional hops, namely use either the Windows SDK or Visual Studio Build Tools, which both need to be installed manually by clicking thorugh a bunch of websites and wizards. Of course the described way didn't work for me ootb, as the 2021 version of Visual Studio somehow uses different folder structures, so I needed to override the windowsVsVarsPath property in order to get it to run. After that, the compilation process just worked, finished my application in under 2 minutes, including some downloads, which is just NICE, I have to say. Size of the executable (.exe file!) is around 7MB, which is nice, considering I included ROMs, Swing and all that stuff. You can download it from the 0.0.1 release here.

Rendering

Even though there's no specification about the refresh rate at which CHIP-8 runs, it's common to set sth like 500Hz. This would mean 500 updates per second, or an update every 2ms. A game step needs to be finished within that budget though. Since simulation step and rendering step are coupled by specification for CHIP-8, both steps together may not exceed the budget. While not a problem for the game logic on modern machines, for rendering (if not to a console) it's a different story. I have/had different implementations tested, for example based on this console rendering library (which admittedly isn't meant to be used for games) or a very dumb implementation with Swing, rendering pixel by pixel on a graphics instance. Didn't meet the requirements, but I will write down the Swing journey as a seperate post, I think :)