Most of my programming experience has been with C++, and recently with C++11/14. Last summer, I worked on a project in both C89 and C++14 for 3 months, and I saw a lot of problems that those languages have when working in a large, complicated code base.
My programming languages class last quarter introduced me to OCaml and functional programming, and I enjoyed how clean and easy to understand the code bases were. Since then, I started to find the warts of C++ harder to cope with.
My biggest problem with C++ isn’t that so-called modern C++ is bad (it’s not! I really love working with “modern” C++), but that C++ defaults to unsafe code. The problem with the unsafe-as-default languages is that the people with less experience accomplish their tasks as simply as they can, often the “default” way, which is unsafe. However, since they have less experience, they don’t know that their code is unsafe, leading to a buggy mess. If modern C++ could ditch the backwards compatibility in a way that is easy to integrate with existing code bases, probably at a translation-unit level, I think it would be the perfect language for me. But since I haven’t seen any murmurs of that happening in the C++ community, I decided to try Rust, which is pretty similar to what I outlined above, with the addition of some functional programming elements.
Introduction to Chip8
Most of my Chip8 knowledge and historical information comes the from the Wikipedia page about it.
Chip8 started out as an interpreted programming language developed in the 1970s to make game development for microcomputers easier.
Starting in the 1980s, some graphing calculators included a Chip8 interpreter which helped Chip8 remain somewhat well known.
The Chip8 virtual machine has a simple architecture, involving memory, registers, a very simple stack, two timers, and basic input/output. It supports only 35 opcodes, which makes writing a Chip8 interpreter a decent weekend project. For reference, the Z80, which is another popular architecture to emulate, has 256 opcodes.
The Chip8 virtual machine supports 64x32 pixel displays and uses sprites embedded in the source code to render the display.
Additionally, sprites for
A-F are included with the interpreter.
Chip8 has 4096 bytes of memory, however, program ROM starts at
0x200. The first 512 bytes are reserved for the interpreter itself.
Later sections of memory were originally reserved for the interpreter, but most modern interpreters just use other memory.
Modern interpreters store the font data in the first 512 bytes, leaving the rest of the space available for Chip8 program.
There are 16 8-bit registers, with the last register (
VF) storing miscellaneous flags (carry, draw, etc).
There is a separate, special register, called the index, that stores 16-bit memory address. It is used by opcodes to specify the start of memory regions for reading.
Chip8 only supports simple subroutines, not entire function calls, so the stack is just a stack of program counter values. When the interpreter jumps to a subroutine, it stores its last program counter value on the stack so when it returns, it can pop the program counter value off of the stack and resume running from where it was before the jump. The lack of a full function calls means that there are no stack frames and no stack frame local storage, reducing the complexity of the interpreter but also making Chip8 programs harder to maintain.
According to Wikipedia, the original Chip8 interpreter supported 12 levels of nesting, but most modern interpreters support at least 16.
There are two timers, a delay timer and a sound timer, both running at 60Hz. The delay timer is used for timing in-game events and the sound timer is used to enable sounds at certain times.
Originally, the computers that shipped with Chip8 also shipped with a hexadecimal keypad.
Most emulators these days use
1-4q-ra-fz-v as the keypad, as that roughly translated to the same physical layout.
Chip8 only supports the creation of a simple “beep” noise.
Part of my goal with my project was to create a Rust crate (“library” in Rust-speak) that could act as a Chip8 virtual machine, and then use that crate to create a usable emulator that could play games. Rust supports creating a library and executable in one crate, so I did that.
The library takes in a vector of bytes representing a Chip8 program, loads that into the virtual machine’s memory and then emulates a single CPU cycle each time
emulate_cycle is called.
The emulator takes the current opcode and executes it using it’s memory, stack, and registers.
The Chip8 struct has a few public members that can be set or read by the consumer of the library to interface with the interpreter.
My emulator isn’t quite done yet, but it’s playable. I still need to limit the timers to 60Hz and I want to try to smooth out framerate jumps.
Eventually, as I improve my Rust skills, I hope to better optimize my code and support
cargo’s benchmark tooling.
I also want to look into adding configuration options for keymappings and changing colors.
Finally, I would like to see if I can add a terminal-based ASCII art UI or some other terminal-based UI so that the Chip8 games could run in the terminal. If I can figure that out, I want to also port a Rust NES Emulator’s UI to the terminal.
Rust vs. C++
Overall, I’ve enjoyed writing my first Rust program. Rust is still pretty young but I can see see it doing well in the future.
This program wasn’t complicated enough to really benefit from Rust’s memory and thread safety improvements over C++, but getting a glimpse at Rust’s module system is getting me really excited for C++’s module system (especially how macros are imported/exported).
Compared to C++, it was incredibly easy to get a project setup that depends on external dependencies.
cargo streamlines the process of depending on other projects very easy.
It makes me wish there was a standardized C++ build/packaging system.
Large tech companies have the pleasure of using their own proprietary systems for all projects, but hobbyist developers do not (although many competing systems have been released).
Even if the hobbyist settled on one system, they might want to depend on a project based on another system.
While not impossible to depend on that code, it isn’t nearly as easy as adding
my_depend = "1.0.0" to their project file.
One area that Rust needs to see major improvement in is compilation time. As it currently stands, my small Rust program takes about 20 seconds to do a clean build on my 2015 Macbook Pro.
Overall, I’m very excited to see Rust become a more feature-complete competitor to C and C++ that takes the lessons learned from the past decades and integrates them into a language not held back by legacy support.