m9b · part 6 of 6

Re-retro: five games on 36 MHz

The five games were written for DOS PCs of the early 1990s. Their modern source ports assume a modern machine. Running them on m9b meant undoing some of that modernization: putting them back on a diet of memory, cycles and sound hardware that’s in some ways tighter than the PCs they first ran on.

Doom
Descent
Tyrian
System Shock
Game Port of Speed on m9b What mattered most
Doom (1993, shareware) doomgeneric ≈ 22 fps (timedemo) the column and span drawers; the data cache
Descent (1995, shareware) D1X Rebirth 0.40 ≈ 10 fps, from 5 the texture mapper’s divides; the cockpit
Tyrian 2.1 (1995) OpenTyrian 35 fps, full speed, from 17 one bounds check per pixel
System Shock (1994) Shockolate ≈ 7.4 fps, from 5.8 memory: 17 MB cutscenes; misaligned reads; divides
Ultima VII (1992), both games Exult 1.12.1 7–13 fps memory: a 6.2 MB heap in under 6 MB
Ultima VII, original engine Ultima7_Decomp 8–10 fps (the engine’s limit is 10) word-aligned heap blocks; sprites and tiles a word at a time

The machine they land on

m9b is in-order, has no floating-point unit, and runs at 36 MHz with 8 MB of RAM behind caches whose misses cost about 25 cycles each (part 3). It has no sound chip and no MIDI synthesizer. It does have a 32-bit hardware multiplier and divider, a reasonably fast SD card, and a video engine that does page flipping and scaling for free.

The software is simply ported, not rewritten. Each game is its upstream source, trimmed to what the build needs, with every change marked, and a platform layer of its own in place of SDL.

One platform for five games

All five share a small runtime, first written for Doom:

Music: rendered once, streamed forever

All five games composed their music for a synthesizer: an AdLib or Sound Blaster’s Yamaha OPL FM chip, or a Roland MT-32. None fits. An open-source OPL2 core needs 1,337 cells and 4 block RAMs on this chip, which has about a thousand cells and one block RAM to spare. A software synthesizer would cost 10 to 15% of the CPU before the game draws anything, and the ports’ own emulators compute in floating point.

So the music is rendered once, on a PC, and streamed from the SD card during play. For four of the games, that’s libADLMIDI’s emulation of the OPL3 chip playing each game’s own instruments and its sound driver’s volume rules: Doom’s GENMIDI bank, Descent’s HMI banks, System Shock’s AIL timbres; Tyrian through its own player and OPL emulator. Ultima VII uses Exult’s recordings of a real Roland MT-32, the synthesizer it was composed for. Every track comes out as 24 kHz mono WAV at a common loudness, streamed through a ring buffer in PSRAM that the main loop tops up and the audio interrupt drains.

The results aren’t small: 81 MB for Doom, 69 for Descent, 128 for Tyrian, 95 for System Shock, 208 for Ultima VII. SD cards are big so that’s not much of a problem.

System Shock needed one more step. Its music is adaptive: each theme is up to 54 short sequences, and a music AI starts them on up to 8 threads, a main piece and layers over it, choosing what comes next from what’s happening. Each sequence is rendered on its own (227 files), and the port implements the music driver’s interface on top of them: the game’s own AI still picks the pieces, the port streams up to eight of them at once and mixes them.

Doom: the drawers

Doom spends its time in two loops: R_DrawColumn, which draws walls and sprites a vertical column at a time, and R_DrawSpan, which draws floors and ceilings a horizontal row at a time. Together they’re 40% of the frame.

Change (CPU at 27 MHz at the time) 2,000 frames fps
Baseline 181.0 s 11.0
Column and span drawers: locals, 4 pixels per iteration 157.2 s 12.7
Page flip in the vertical-blank interrupt 144.5 s 13.8
Range checks off 141.3 s 14.2
Drawing straight into the display buffers 138.0 s 14.5

From there, the hardware took over: the CPU going synchronous at 36 MHz, the 2-way data cache, faster SD reads, cbo.zero in memset and memcpy, the radix-4 divider. The timedemo now runs at about 22 fps. Before the second cache way, moving a buffer by a few hundred bytes changed Doom’s speed by 2%: a column of the screen touches 200 cache lines, and whatever else was hot fought it for them.

Descent: one divide in sixteen

Descent is a true 3D game with perspective-correct texture mapping, and D1X’s C version of the texture mapper divides by depth for every pixel: 70 cycles or more each. At first, texture mapping was two thirds of the time and the game ran at 5 fps.

About 10 fps in the first level, with the full cockpit.

Tyrian: one check per pixel

Tyrian’s profile showed two thirds of the time in one function, drawing the three scrolling background layers a byte at a time, each pixel checked against the screen’s edges. Rows that are wholly on screen now skip the checks, and the game went from 17 to 35 fps: its full speed, set by its own timer, with time to spare. Its music needed rendering too: OpenTyrian’s OPL emulator computes envelopes in floating point for every sample.

System Shock: back to 1994’s memory

Shockolate is Night Dive’s 2018 release of the Macintosh source, made portable. The Mac version, and the port, assumed far more memory than 1994’s DOS version: starting level 1 took 5.3 MB of heap, and a cutscene 17 MB. m9b has 5.1 MB of heap for it.

Level 1 now takes 2.8 MB, a cutscene about 0.2 MB. Speed came from two places: the texture unpacker read words at odd addresses, 97% of the misaligned accesses the trap handler was emulating, and the fixed-point divides went through a 64-bit software routine instead of the hardware divider. With those fixed and the radix-4 divider in the CPU, a saved game went from 5.8 to 7.4 fps. Double clicks needed their own fix: at 7 fps, two clicks fell between polls of the mouse, so the buttons are now sampled in the audio interrupt, with their times.

Ultima VII: a heap diet

The original Ultima VII is 16-bit real-mode code on Origin’s own memory manager, with no source. Exult reimplements its engine in C++17, and plays the original data. Starting The Black Gate took 6.2 MB of heap; m9b has 5.8 MB to give it, and the C library’s allocator lost another 540 KB to holes, until Doug Lea’s malloc replaced it.

Trinsic, a few minutes in, now peaks at 4.1 MB, with room for the MT-32 music and sound effects. It plays at 7 to 13 fps. Exult does more work per object than the 1992 engine did on a 386, and that’s the next thing to look at.

The 1992 engine itself runs too. Ultima7_Decomp is a decompilation of both DOS games into portable C and C++, and m9b-u7 gives it a platform layer for the board. The game gets the DOS memory it expected, in a 4 MB arena, and needed few changes: its far heap keeps blocks word-aligned, so the CPU doesn’t trap, and sprite spans and tiles are drawn a word at a time. Walking around Trinsic, it runs at 8 to 10 fps, against its own limit of 10 steps a second. Its music and sound effects are the same MT-32 recordings the Exult port uses.

How the ports were tested

None of the five games is fast by today’s standards. But they run in full, with their music, in a chip whose hardware most likely was not designed for them.