WEBVTT
00:00:00.880 --> 00:00:04.520
Hello everyone and welcome to my talk, "The State of Retro Gaming and Emacs."
00:00:06.960 --> 00:00:08.639
First of all, a little bit about myself.
00:00:08.639 --> 00:00:12.000
My name is Vasilij Schneidermann. I'm 28 years old.
00:00:12.000 --> 00:00:14.719
I work as a cyber security consultant at msg systems,
00:00:14.719 --> 00:00:17.359
and test other people's web applications
00:00:17.359 --> 00:00:20.160
and review the source code for security problems.
00:00:20.160 --> 00:00:22.080
You can reach me by email.
00:00:22.080 --> 00:00:25.039
I have my own self-hosted git repositories,
00:00:25.039 --> 00:00:28.160
and I have a blog where you can occasionally find new posts by me
00:00:28.160 --> 00:00:32.160
on all kinds of things, not just Emacs things.
00:00:32.160 --> 00:00:34.559
The motivation about this one...
00:00:34.559 --> 00:00:37.600
I found that Emacs is the ultimate procrastination machine,
00:00:37.600 --> 00:00:39.600
and there are lots of fun demonstrations.
00:00:39.600 --> 00:00:41.200
I'll go over a few of them.
00:00:41.200 --> 00:00:45.840
For example, someone made a thing to order salad for himself online,
00:00:45.840 --> 00:00:48.239
so he doesn't have to walk over to the shop.
00:00:48.239 --> 00:00:51.760
There's plenty of IRC bots. There's some game things.
00:00:51.760 --> 00:00:55.600
There's an emulator for the Z-machine
which you can use to play zork.
00:00:55.600 --> 00:00:57.440
And so I asked myself, at this point,
00:00:57.440 --> 00:00:59.920
can you actually emulate retro games at 60fps?
00:00:59.920 --> 00:01:02.079
I looked around a bit and found some projects,
00:01:02.079 --> 00:01:06.159
but none that were actually able to do it at 60fps.
00:01:06.159 --> 00:01:08.000
So I set out to do my own one,
00:01:08.000 --> 00:01:09.200
and looked out for a console
00:01:09.200 --> 00:01:11.119
that you can actually emulate at that speed,
00:01:11.119 --> 00:01:14.690
using Emacs with its very, very limited rendering.
00:01:16.320 --> 00:01:19.200
And here's the project, chip8.el.
00:01:19.200 --> 00:01:20.560
It's pretty much finished.
00:01:20.560 --> 00:01:24.000
It clocks into under 1000 source lines of code.
00:01:24.000 --> 00:01:26.159
It supports the superchip 8 extensions.
00:01:26.159 --> 00:01:27.280
It runs at full speed.
00:01:27.280 --> 00:01:29.600
All games behave okay, as far as I'm concerned,
00:01:29.600 --> 00:01:31.680
and yeah, I'm pretty happy with it.
00:01:31.680 --> 00:01:34.479
It's very much the hello world of emulation,
00:01:34.479 --> 00:01:40.880
and I might, maybe, do some other emulation projects in the future.
00:01:40.880 --> 00:01:43.360
Now, for the section which is the longest:
00:01:43.360 --> 00:01:45.439
bunch of fun facts about chip8.el
00:01:45.439 --> 00:01:49.200
which I've learned during this project.
00:01:49.200 --> 00:01:51.759
So what the hell is chip8 anyway?
00:01:51.759 --> 00:01:54.960
First of all, unlike many other emulation game things,
00:01:54.960 --> 00:01:56.799
it's not a console, but a VM.
00:01:56.799 --> 00:02:00.000
It was designed for easy porting of home
computer games.
00:02:00.000 --> 00:02:02.320
It wasn't terribly successful,
00:02:02.320 --> 00:02:05.439
but there's still a small community of enthusiasts writing games for it,
00:02:05.439 --> 00:02:09.119
and there are even a few demos.
00:02:09.119 --> 00:02:11.039
This VM has system specs.
00:02:11.039 --> 00:02:14.720
It has a very, very simple 8-bit cpu with 16 registers,
00:02:14.720 --> 00:02:17.280
and 36 fixed-size instructions.
00:02:17.280 --> 00:02:19.680
You have a whole 4 kilobyte of RAM.
00:02:19.680 --> 00:02:22.080
You have a stack with 16 return addresses.
00:02:22.080 --> 00:02:25.760
The resolution is 64 by 32 black/white pixels.
00:02:25.760 --> 00:02:28.000
Rendering is done by drawing sprites.
00:02:28.000 --> 00:02:29.200
These are drawn in XOR mode,
00:02:29.200 --> 00:02:31.840
meaning that if you draw a sprite and set a bit,
00:02:31.840 --> 00:02:35.040
it just flips over from black to white or white to black.
00:02:35.040 --> 00:02:39.360
For sound, you have a monotone buzzer that can just beep at one frequency.
00:02:39.360 --> 00:02:43.120
Most unusually, there's a hexadecimal keypad as input,
00:02:43.120 --> 00:02:48.480
so the keys are basically zero to nine and a to f.
00:02:48.480 --> 00:02:50.720
So how does this whole thing work?
00:02:50.720 --> 00:02:52.400
It runs at an unspecified speed.
00:02:52.400 --> 00:02:53.040
You'll probably have to do some fine-tuning
00:02:53.040 --> 00:02:56.080
to find the speed you're happy with.
00:02:56.080 --> 00:02:58.080
Sound and delay timers exist.
00:02:58.080 --> 00:03:01.120
They count down at 60fps down to 0.
00:03:01.120 --> 00:03:05.120
This is done so that you can play a sound at some specific time.
00:03:05.120 --> 00:03:07.840
The game itself is loaded with a fixed offset into RAM.
00:03:07.840 --> 00:03:10.480
The program counter is set to exactly that offset,
00:03:10.480 --> 00:03:11.920
and from there it enters the game loop
00:03:11.920 --> 00:03:13.280
where it decodes an instruction,
00:03:13.280 --> 00:03:14.800
executes it for the side effects,
00:03:14.800 --> 00:03:18.130
and just loops and does this ad infinitum.
00:03:19.599 --> 00:03:22.720
So the game loop was the first thing where we ran into problems.
00:03:22.720 --> 00:03:25.120
The usual game approach is to do stuff,
00:03:25.120 --> 00:03:26.640
figure out how long to wait,
00:03:26.640 --> 00:03:29.280
wait for exactly that much, and repeat.
00:03:29.280 --> 00:03:31.680
This doesn't work well in Emacs at all, because, well,
00:03:31.680 --> 00:03:34.959
user input, basically.
00:03:34.959 --> 00:03:37.760
Emacs is designed to just do whatever it needs to do
00:03:37.760 --> 00:03:39.040
whenever you enter user input
00:03:39.040 --> 00:03:42.319
instead of doing things at one specific time.
00:03:42.319 --> 00:03:46.640
If you try to do interruptable sleep, well, you get unpredictable behavior.
00:03:46.640 --> 00:03:50.480
For example, it can be the timer doesn't run at all at the next time
00:03:50.480 --> 00:03:52.560
because you've accidentally cancelled it.
00:03:52.560 --> 00:03:55.120
If you do uninterruptable sleep, it freezes instead ,
00:03:55.120 --> 00:03:56.720
which isn't what we want either.
00:03:56.720 --> 00:04:00.560
So I went for timers, which forced me to do inversion of control,
00:04:00.560 --> 00:04:02.560
meaning that I have to write code in the style
00:04:02.560 --> 00:04:04.879
where it just calls timer,
00:04:04.879 --> 00:04:06.560
and this allows this input to happen
00:04:06.560 --> 00:04:11.040
and for things to progress at roughly the speed I want to.
00:04:11.040 --> 00:04:14.159
So there's the timer function which is called at 60fps
00:04:14.159 --> 00:04:17.359
and I have to be very careful to not do too much in it.
00:04:17.359 --> 00:04:21.305
And, say, this function executes CPU cycles,
00:04:21.305 --> 00:04:26.479
decrement the sound/delay registers, and redraw the screen.
00:04:26.479 --> 00:04:28.800
So to map this whole system to Emacs Lisp,
00:04:28.800 --> 00:04:31.199
I've used just integers and vectors
00:04:31.199 --> 00:04:33.120
which contain even more integers.
00:04:33.120 --> 00:04:35.040
This is used for the RAM, registers,
00:04:35.040 --> 00:04:37.040
return stack, key state, screen,
00:04:37.040 --> 00:04:38.508
and so on and so forth.
00:04:38.508 --> 00:04:41.520
Basically, what you would do if you were writing C.
00:04:41.520 --> 00:04:43.360
All of this is stored in global variables.
00:04:43.360 --> 00:04:45.600
I'm not using any lists at all.
00:04:45.600 --> 00:04:48.400
As a side effect, there's no consing going on at all.
00:04:48.400 --> 00:04:50.080
There are no extra objects created
00:04:50.080 --> 00:04:53.199
which would trigger garbage collection processes.
00:04:53.199 --> 00:04:55.600
Getting this right was rather tricky, actually,
00:04:55.600 --> 00:04:58.240
and there were some hidden garbage collection problems
00:04:58.240 --> 00:05:01.759
which I had to resolve over time.
00:05:01.759 --> 00:05:03.759
So, decoding instructions.
00:05:03.759 --> 00:05:06.800
For this, you have to know that all instructions are two bytes long,
00:05:06.800 --> 00:05:08.880
and the arguments are encoded inside them.
00:05:08.880 --> 00:05:11.440
For example, the jump to address instruction
00:05:11.440 --> 00:05:15.120
is encoded as one and three hex digits.
00:05:15.120 --> 00:05:18.400
The type is extracted masking with #xF000
00:05:18.400 --> 00:05:20.400
and then shifting it by 12 bits.
00:05:20.400 --> 00:05:23.520
Mask means you perform the binary AND.
00:05:23.520 --> 00:05:28.400
You can do the same with the argument by masking with #0xFFF and no shift.
00:05:28.400 --> 00:05:30.560
If you do this long enough, you'll find common patterns.
00:05:30.560 --> 00:05:32.639
For example, addresses are always encoded like this
00:05:32.639 --> 00:05:34.880
using the last three nibbles.
00:05:34.880 --> 00:05:36.160
In the code, you'll find a big cond
00:05:36.160 --> 00:05:40.070
which dispatches on the type and executes it for the side effects.
00:05:41.440 --> 00:05:45.919
For testing, I've initially just executed the ROM until I've hit C-g,
00:05:45.919 --> 00:05:49.039
and then use the debug command to render the screen to a buffer.
00:05:49.039 --> 00:05:53.199
Later on, I found tiny ROMs that just display a static test screen,
00:05:53.199 --> 00:05:57.280
for example, logo, and looked whether it looked right.
00:05:57.280 --> 00:05:58.800
I added instructions as needed
00:05:58.800 --> 00:06:00.720
and went through more and more and more ROMs.
00:06:00.720 --> 00:06:04.000
And later I wrote a unit test suite as a safety net.
00:06:04.000 --> 00:06:07.840
This unit test suite, it just sets up an empty emulator state,
00:06:07.840 --> 00:06:09.199
executes some instructions,
00:06:09.199 --> 00:06:14.880
and then looks whether the expected side effects have happened.
00:06:14.880 --> 00:06:18.319
For debugging, I usually use edebug, but this was super ineffective,
00:06:18.319 --> 00:06:21.600
because, well, you don't really want to step through big cons
00:06:21.600 --> 00:06:23.680
doing side effects for every single cycle,
00:06:23.680 --> 00:06:26.880
when it can take like 100 cycles for things to happen.
00:06:26.880 --> 00:06:29.680
Therefore I've set up logging.
00:06:29.680 --> 00:06:32.639
Whenever I logged something and couldn't figure out the error,
00:06:32.639 --> 00:06:37.039
I compared my log output with the instrumented version of another emulator,
00:06:37.039 --> 00:06:40.479
and if the logs diverge, then I have figured out where the bug lies
00:06:40.479 --> 00:06:42.720
and could look deeper into it.
00:06:42.720 --> 00:06:44.960
Future project idea might be a chip 8 debugger,
00:06:44.960 --> 00:06:49.440
but I doubt I'll ever go into it.
00:06:49.440 --> 00:06:51.759
For analysis, I initially wrote a disassembler,
00:06:51.759 --> 00:06:54.400
which is a very simple thing but super tedious,
00:06:54.400 --> 00:06:56.639
especially if you wanted to add advanced functionality,
00:06:56.639 --> 00:06:58.720
for example, analysis or thinking of what part is data,
00:06:58.720 --> 00:07:00.000
what part is code.
00:07:00.000 --> 00:07:03.360
I had this great idea for using the radare 2 framework
00:07:03.360 --> 00:07:06.479
and adding analysis and disassembly plug-in for it.
00:07:06.479 --> 00:07:08.400
So I looked into this. Found, okay,
00:07:08.400 --> 00:07:10.319
you can write plugins in C
00:07:10.319 --> 00:07:12.639
but also in Python, so I wrote one in Python,
00:07:12.639 --> 00:07:14.720
and then discovered there's actually an existing one in core,
00:07:14.720 --> 00:07:18.400
which you have to enable explicitly by passing an extra argument.
00:07:18.400 --> 00:07:21.680
I've tried it and found it's not exactly as good as my own one,
00:07:21.680 --> 00:07:24.160
so I improved this one and submitted pull requests
00:07:24.160 --> 00:07:26.610
until it was at the same level.
00:07:28.080 --> 00:07:30.720
Rendering was the trickiest part of this whole thing,
00:07:30.720 --> 00:07:34.319
because, well, I decided against using a library.
00:07:34.319 --> 00:07:37.120
Not like there would have been any usable library for this.
00:07:37.120 --> 00:07:40.880
My usual approach of creating SVG files was too expensive.
00:07:40.880 --> 00:07:45.120
It just created too much garbage and took too long time.
00:07:45.120 --> 00:07:47.360
I then tried creating mutating strings.
00:07:47.360 --> 00:07:52.479
This was either too expensive, just like SVGs, or too complicated.
00:07:52.479 --> 00:07:57.280
I tried changing SVG tiles, which created gaps between the lines.
00:07:57.280 --> 00:08:00.720
Then I tried to create an xpm file which was backed by a bool vector
00:08:00.720 --> 00:08:02.400
and mutating this bool vector,
00:08:02.400 --> 00:08:04.000
but the image caching effect
00:08:04.000 --> 00:08:06.479
made it just every nth frame to appear,
00:08:06.479 --> 00:08:08.879
which wasn't good either.
00:08:08.879 --> 00:08:11.440
Then I had the idea to just use plain text
00:08:11.440 --> 00:08:13.120
and paint the individual characters
00:08:13.120 --> 00:08:14.800
with a different background color.
00:08:14.800 --> 00:08:17.120
This had perfect, perfect performance.
00:08:17.120 --> 00:08:19.280
There were many optimization attempts until I got there,
00:08:19.280 --> 00:08:21.199
and it was very, very stressful.
00:08:21.199 --> 00:08:26.160
I wasn't sure whether I would ever get to accept the performance at all.
00:08:26.160 --> 00:08:28.560
For sound you only need to do a single beep,
00:08:28.560 --> 00:08:31.280
so technically, it shouldn't be difficult to emulate it.
00:08:31.280 --> 00:08:33.039
However, doing this is hard because
00:08:33.039 --> 00:08:37.200
Emacs officially only supports synchronous playback of sounds.
00:08:37.200 --> 00:08:41.360
But there's also Emacs process, which you can launch in asynchronous way.
00:08:41.360 --> 00:08:44.720
So I looked into it and found that mplayer has a slave mode
00:08:44.720 --> 00:08:48.640
and mpv supports listing on the fifo for commands.
00:08:48.640 --> 00:08:53.760
So I've created a pipe, started a paused MPV in loop mode,
00:08:53.760 --> 00:08:56.560
and always send in pause and unpause command to the FIFO,
00:08:56.560 --> 00:08:58.000
and that way I could control
00:08:58.000 --> 00:09:02.640
when to start beeping and stop beeping.
00:09:02.640 --> 00:09:04.160
So yeah, that's it so far.
00:09:04.160 --> 00:09:07.200
It was a very educational experience.
00:09:07.200 --> 00:09:10.320
I have tried out a bunch of games which were,
00:09:10.320 --> 00:09:14.320
well, I almost say the worst ports of classic games I've ever tried.
00:09:14.320 --> 00:09:15.680
It wasn't terribly fun to play them,
00:09:15.680 --> 00:09:18.555
but was fun to improve the emulator
00:09:18.555 --> 00:09:21.760
until, well, things worked good enough.
00:09:21.760 --> 00:09:25.120
I've learned a lot about how computers work at this level,
00:09:25.120 --> 00:09:28.880
so, maybe, maybe I'll in the future make another emulator,
00:09:28.880 --> 00:09:34.000
but I'm not sure whether anything more advanced, like an Intel 8080 emulator,
00:09:34.000 --> 00:09:36.560
will actually run in Emacs fast enough,
00:09:36.560 --> 00:09:37.839
but it's still an interesting idea,
00:09:37.839 --> 00:09:40.800
because then you could actually have an OS inside Emacs
00:09:40.800 --> 00:09:43.120
and fulfill that one specific meme.
00:09:43.120 --> 00:09:45.440
But if I try to do most serious stuff,
00:09:45.440 --> 00:09:47.040
I'll probably use Chicken Scheme,
00:09:47.040 --> 00:09:49.920
which is my preferred language for serious projects,
00:09:49.920 --> 00:09:53.279
and write a NES game emulator.
00:09:53.279 --> 00:09:57.839
And that's it. Thank you.