A good choice would be DOOM. id Software's 1994 mega-hit has been ported to everything. It is designed around a core with no layering violations. It is usually easy to find and read the implementation of its six I/O sub-systems.
An other choice would be Eric Chahi's 1991 critically acclaimed[1]" title "Another World", better known in North America as "Out Of This World" which also happens to be ubiquitous. I would argue it is in fact more interesting to study than DOOM because of its polygon based graphics which are suitable to wild optimizations. In some cases, clever tricks allowed Another World to run on hardware built up to five years prior to the game release.
This series is a journey through the video-games hardware of the early 90s. From the Amiga 500, Atari ST, IBM PC, Super Nintendo, up to the Sega Genesis. For each machine, I attempted to discover how Another World was implemented. I found an environment made rich by its diversity where the now ubiquitous CPU/GPU did not exist yet. In the process, I discovered the untold stories of seemingly impossible problems heroically solved by lone programmers.
In the best case I was able to get in touch with the original developer. In the worse cases, I found myself staring at disassembly. It was a fun trip. Here are my notes.
There is very little code in Another World. The original Amiga version was reportedly 6,000 lines of assembly[2]. The PC DOS executable is only 20 KiB. Surprising for such a vast game which shipped on a single 1.44 MiB floppy. That is because most of the business logic is implemented via bytecode. The Another World executable is in fact a virtual machine host which reads and executes uint8_t opcodes.
Another World VM defines 256 variables, 64 threads, 29 opcodes, and four framebuffers[3]. That's it. If you build a VM host capable of handling these, you can run the game. If you are able to make the VM fast enough to run at 20 frames per seconds, you can actually play the game.
The virtual machine's graphic system uses a coordinate system of 320x200 with 16 palette-based colors. The color limitation may be surprising given that the development platform, the Amiga 500, supported up to 32 colors. This choice was a sweet spot allowing the graphics to be compatible with the other big platform of the era, the Atari ST which supports only 16 colors.
The limitation turned out to be a blessing in disguise. It resulted in an unique style which has aged well.
Of the four framebuffers[4], two are used for double buffering while two are used to save background (BKGD1 and BKGD2) composition. This is an optimization to avoid redrawing all the static background polygons in favor of a simple copy operation.
In the next video, see how a new scene is drawn first in the BKGD1 buffer. Each new frame, the BKGD1 is fully copied to the double buffer not being scanned by the display. There, moving elements such as Lester are drawn. Notice how once the car is "parked" it is also drawn in the BKGD1 buffer to minimize the number of polygons to render in subsequent frames.
Notice in the video how composition uses a plain painter algorithm which is simple but result in significant overdraw. This is not a problem since it is largely amortized by the BKGD copy.
The second BKGD2 is used to cache the last background. It is useful during cinematics when the background goes back and forth between scenes like in the intro when the view shows alternatively Lester desk and the inside of the particular accelerator. During gameplay it allows a significant speed-up when the player goes back to the previous screen.
The background buffer are not set in stone one rendered. They are often modified with new elements. It happens for the Ferrari 288 in the previous video but also for other subtle details.
In the early screens after emerging from the pool where the game begins, the game draws footprints and skid marks under Lester's feet as he walks on the sand. If the player walks one screen right and then back to the left again, the modified framebuffer is restored to preserve the footprints. It gives the environment a subtle additional sense of permanence.
It is so subtle as to go unnoticed in normal gameplay but I thought this was another example of Eric Chahi's great attention to detail in building this game.
- Martin Atkins
The next array illustrates the 29 opcodes. We can find here thread (THRD) management, framebuffer (FB) management, and all the register management operations. Most opcodes are "easy" to implement except for "COPY FB", FILL, and "DRAW_POLY*" which are difficult for performances reasons.
0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0E | 0x0F | |
0x00 | CMOV | MOV | ADD | CADD | CALL | RET | PAUSE THRD |
COND JMP |
SET VECT |
JNZ | CJMP | SET PAL |
RESET THRD |
SLCT FB |
FILL FB |
COPY FB |
0x10 | BLIT FB |
KILL THRD |
DRAW TEXT |
SUB | AND | OR | SHL | SHR | PLAY SOUND |
LOAD RESC |
PLAYMUSIC | |||||
0x20 | ||||||||||||||||
0x30 | ||||||||||||||||
0x40 | DRAW_POLY_SPRITE | |||||||||||||||
0x50 | ||||||||||||||||
0x60 | ||||||||||||||||
0x70 | ||||||||||||||||
0x80 | DRAW_POLY_BACKGROUND | |||||||||||||||
0x90 | ||||||||||||||||
0xA0 | ||||||||||||||||
0xB0 | ||||||||||||||||
0xC0 | ||||||||||||||||
0xD0 | ||||||||||||||||
0xE0 | ||||||||||||||||
0xF0 |
As mentioned in the previous section, 26 out of the 29 opcodes are easy to implement. The real challenge in porting this game was in manipulating pixels within the machine BUS and CPU bandwidth limitations. What will be done in this series is study how each port manipulated the framebuffers and how they solved the DRAW, FILL and COPY problems.
Ready? Let's dive into Another World on Amiga 500.
^ | [1] | Tilt d'or award year 1991 |
^ | [2] | Burgertime 8/9/2015: Out of This World |
^ | [3] | Another World Source Code Review |
^ | [4] | Thanks to Dimitri Sokolyuk for figuring out the second backbuffer. You can see a capture of all buffers during gameplay here. |