Back to the schedule
Previous: Emacs as a Highschooler: How It Changed My Life
Next: Welcome To The Dungeon

State of Retro Gaming in Emacs

Vasilij "wasamasa" Schneidermann

Download compressed .webm video (9.6M)
Download compressed .webm video (7.2M, highly compressed)
View transcript

Many jokes have been made about the true nature of Emacs, such as it being a fully-fledged operating system. This talk will demonstrate its suitability for playing retro games, then explore the inner workings of a CHIP-8 emulator capable of smooth video game emulation.

Questions

Q5: Do you think would be possible to write some compiler in order to write chip-8 games on elisp?

It could be possible if you restrict yourself to some very limited Elisp subset or lispy assembler. For the latter, here's some projects to draw inspiration from:

Q4: What's the biggest perf bottleneck for your emulator? does it spend time executing your Lisp or something else in the Emacs infrastructure (eg redisplay)?

Redisplay was super slow, it's like 3-4x as slow as executing the CPU cycles.

  • Okay that's the reason why GCCEmacs does not help :)

Q3: Do you think that you make our tiny console based in the chip ATMega like Arduboy?

I'm sorry, I didn't quite understand the question, could you please clarify it? I'm not exactly a hardware person, might have to defer it to someone else.

I've looked at Arduboy and I believe the DEFCON CHIP-8 Badge is the closest to this: https://hackaday.io/project/19121-andxor-dc25-badge/log/53223-chip8-schip-game-emulation

Q2: Any tutorial to start? I want to make my game now, no, for chip8

I'm not aware of tutorials, but there's CHIP-8 resources online. You can of course study the assembly of existing games, that's how I figured out the tricks that broke my emulator :>

Q1: How did you manage to present a game engine without showing any game? :-) Show us!!

See the alt stream, it has several demos not shown due to time constraints.

Notes

Transcript

[00:00:00.880] Hello everyone and welcome to my talk, "The State of Retro Gaming and Emacs." First of all, a little bit about myself. My name is Vasilij Schneidermann. I'm 28 years old. I work as a cyber security consultant at msg systems, and test other people's web applications and review the source code for security problems. You can reach me by email. I have my own self-hosted git repositories, and I have a blog where you can occasionally find new posts by me on all kinds of things, not just Emacs things.

[00:00:32.160] The motivation about this one... I found that Emacs is the ultimate procrastination machine, and there are lots of fun demonstrations. I'll go over a few of them. For example, someone made a thing to order salad for himself online, so he doesn't have to walk over to the shop. There's plenty of IRC bots. There's some game things. There's an emulator for the Z-machine which you can use to play zork.

[00:00:55.600] And so I asked myself, at this point, can you actually emulate retro games at 60fps? I looked around a bit and found some projects, but none that were actually able to do it at 60fps. So I set out to do my own one, and looked out for a console that you can actually emulate at that speed, using Emacs with its very, very limited rendering.

[00:01:16.320] And here's the project, chip8.el. It's pretty much finished. It clocks into under 1000 source lines of code. It supports the superchip 8 extensions. It runs at full speed. All games behave okay, as far as I'm concerned, and yeah, I'm pretty happy with it. It's very much the hello world of emulation, and I might, maybe, do some other emulation projects in the future.

[00:01:40.880] Now, for the section which is the longest: bunch of fun facts about chip8.el which I've learned during this project. So what the hell is chip8 anyway? First of all, unlike many other emulation game things, it's not a console, but a VM. It was designed for easy porting of home computer games. It wasn't terribly successful, but there's still a small community of enthusiasts writing games for it, and there are even a few demos. This VM has system specs. It has a very, very simple 8-bit cpu with 16 registers, and 36 fixed-size instructions. You have a whole 4 kilobyte of RAM. You have a stack with 16 return addresses. The resolution is 64 by 32 black/white pixels.

[00:02:25.760] Rendering is done by drawing sprites. These are drawn in XOR mode, meaning that if you draw a sprite and set a bit, it just flips over from black to white or white to black. For sound, you have a monotone buzzer that can just beep at one frequency. Most unusually, there's a hexadecimal keypad as input, so the keys are basically zero to nine and a to f. So how does this whole thing work? It runs at an unspecified speed. You'll probably have to do some fine-tuning to find the speed you're happy with.

[00:02:56.080] Sound and delay timers exist. They count down at 60fps down to 0. This is done so that you can play a sound at some specific time. The game itself is loaded with a fixed offset into RAM. The program counter is set to exactly that offset, and from there it enters the game loop where it decodes an instruction, executes it for the side effects, and just loops and does this ad infinitum.

[00:03:19.599] So the game loop was the first thing where we ran into problems. The usual game approach is to do stuff, figure out how long to wait, wait for exactly that much, and repeat. This doesn't work well in Emacs at all, because, well, user input, basically. Emacs is designed to just do whatever it needs to do whenever you enter user input instead of doing things at one specific time. If you try to do interruptable sleep, well, you get unpredictable behavior. For example, it can be the timer doesn't run at all at the next time because you've accidentally cancelled it. If you do uninterruptable sleep, it freezes instead , which isn't what we want either.

[00:03:56.720] So I went for timers, which forced me to do inversion of control, meaning that I have to write code in the style where it just calls timer, and this allows this input to happen and for things to progress at roughly the speed I want to. So there's the timer function which is called at 60fps and I have to be very careful to not do too much in it. And, say, this function executes CPU cycles, decrement the sound/delay registers, and redraw the screen.

[00:04:26.479] So to map this whole system to Emacs Lisp, I've used just integers and vectors which contain even more integers. This is used for the RAM, registers, return stack, key state, screen, and so on and so forth. Basically, what you would do if you were writing C. All of this is stored in global variables. I'm not using any lists at all. As a side effect, there's no consing going on at all. There are no extra objects created which would trigger garbage collection processes. Getting this right was rather tricky, actually, and there were some hidden garbage collection problems which I had to resolve over time.

[00:05:01.759] So, decoding instructions. For this, you have to know that all instructions are two bytes long, and the arguments are encoded inside them. For example, the jump to address instruction is encoded as one and three hex digits. The type is extracted masking with #xF000 and then shifting it by 12 bits. Mask means you perform the binary AND. You can do the same with the argument by masking with #0xFFF and no shift. If you do this long enough, you'll find common patterns. For example, addresses are always encoded like this using the last three nibbles. In the code, you'll find a big cond which dispatches on the type and executes it for the side effects.

[00:05:41.440] For testing, I've initially just executed the ROM until I've hit C-g, and then use the debug command to render the screen to a buffer. Later on, I found tiny ROMs that just display a static test screen, for example, logo, and looked whether it looked right. I added instructions as needed and went through more and more and more ROMs. And later I wrote a unit test suite as a safety net. This unit test suite, it just sets up an empty emulator state, executes some instructions, and then looks whether the expected side effects have happened.

[00:06:14.880] For debugging, I usually use edebug, but this was super ineffective, because, well, you don't really want to step through big cons doing side effects for every single cycle, when it can take like 100 cycles for things to happen. Therefore I've set up logging. Whenever I logged something and couldn't figure out the error, I compared my log output with the instrumented version of another emulator, and if the logs diverge, then I have figured out where the bug lies and could look deeper into it. Future project idea might be a chip 8 debugger, but I doubt I'll ever go into it.

[00:06:49.440] For analysis, I initially wrote a disassembler, which is a very simple thing but super tedious, especially if you wanted to add advanced functionality, for example, analysis or thinking of what part is data, what part is code. I had this great idea for using the radare 2 framework and adding analysis and disassembly plug-in for it. So I looked into this. Found, okay, you can write plugins in C but also in Python, so I wrote one in Python, and then discovered there's actually an existing one in core, which you have to enable explicitly by passing an extra argument. I've tried it and found it's not exactly as good as my own one, so I improved this one and submitted pull requests until it was at the same level.

[00:07:28.080] Rendering was the trickiest part of this whole thing, because, well, I decided against using a library. Not like there would have been any usable library for this. My usual approach of creating SVG files was too expensive. It just created too much garbage and took too long time. I then tried creating mutating strings. This was either too expensive, just like SVGs, or too complicated. I tried changing SVG tiles, which created gaps between the lines. Then I tried to create an xpm file which was backed by a bool vector and mutating this bool vector, but the image caching effect made it just every nth frame to appear, which wasn't good either. Then I had the idea to just use plain text and paint the individual characters with a different background color. This had perfect, perfect performance. There were many optimization attempts until I got there, and it was very, very stressful. I wasn't sure whether I would ever get to accept the performance at all.

[00:08:26.160] For sound, you only need to do a single beep, so technically, it shouldn't be difficult to emulate it. However, doing this is hard because Emacs officially only supports synchronous playback of sounds. But there's also Emacs process, which you can launch in asynchronous way. So I looked into it and found that mplayer has a slave mode and mpv supports listing on the fifo for commands. So I've created a pipe, started a paused MPV in loop mode, and always send in pause and unpause command to the FIFO, and that way I could control when to start beeping and stop beeping.

[00:09:02.640] So yeah, that's it so far. It was a very educational experience. I have tried out a bunch of games which were, well, I almost say the worst ports of classic games I've ever tried. It wasn't terribly fun to play them, but was fun to improve the emulator until, well, things worked good enough. I've learned a lot about how computers work at this level, so, maybe, maybe I'll in the future make another emulator, but I'm not sure whether anything more advanced, like an Intel 8080 emulator, will actually run in Emacs fast enough, but it's still an interesting idea, because then you could actually have an OS inside Emacs and fulfill that one specific meme. But if I try to do most serious stuff, I'll probably use Chicken Scheme, which is my preferred language for serious projects, and write a NES game emulator. And that's it. Thank you.

Sunday, Nov 29 2020, ~ 1:16 PM - 1:26 PM EST
Sunday, Nov 29 2020, ~10:16 AM - 10:26 AM PST
Sunday, Nov 29 2020, ~ 6:16 PM - 6:26 PM UTC
Sunday, Nov 29 2020, ~ 7:16 PM - 7:26 PM CET
Monday, Nov 30 2020, ~ 2:16 AM - 2:26 AM +08

Back to the schedule
Previous: Emacs as a Highschooler: How It Changed My Life
Next: Welcome To The Dungeon