The rendering engine
13h
After making my peace with the C++, the Borland, and MS-DOS, I set
out to find where the game produced an output. Surely something
prints to a screen somewhere? This seemed like a reasonable place to
begin. Check out this code block that was waiting for me in
NOCTIS-0.CPP:
void _320_200_256 () // inizializza grafica a 320x200x256 colori.
{
asm {
push ax
mov ax, 0x13
int 0x10
pop ax
}
}
Careful readers will note that the above snippet is not even in C++, but rather is some inline assembly. They will also spot the comment, which is in Italian. In the next codebase that I read and migrate, I think I will try to make sure I am fluent in at least one of the three languages involved. Nevertheless, we carry on.
This assembly invokes the BIOS video service to set mode
13h. We can find a good description of
13h--one of many standard display modes requestable by
a program--in IBM's 1987 smash hit Personal
System/2 and Personal Computer BIOS Interface Technical
Reference. Mode 13h is a 320-by-200 display
with 256 colors, filed under type APA--All Points Addressable--which
is IBM's nice way of letting us know that all pixels are
modifiable.
With the Video Graphics Array, or VGA, in 13h, a
particular region of memory acquires a fun property: modifying it
changes what is displayed on the screen. Noctis keeps a far pointer
to that region at the top of the file, next to a second pointer for
a staging area, where the game can work on a frame before it is
displayed:
Uchar maybefar * adaptor = (Uchar maybefar *) 0xA0000000; // this is the screen
Uchar maybefar * adapted = (Uchar maybefar *) 0xB0000000; // this is the staging
The game loop draws each frame into adapted, then
copies all 64,000 bytes into adaptor. The loop is paced
by the BIOS timer, which ticks 18.2 times a second, so the screen's
memory changes at most once every 55 milliseconds. Asynchronously,
the VGA sweeps that memory seventy times a second and paints
whatever it finds onto the monitor. Neat!
But what is in those 64,000 bytes? Logged out, they look something like:
47 47 47 47 47 47 47 47 47 47 47 47 47 47 47 47 ...
Each byte is a number from 0 to 255, which is not quite the RGB
setup that a modern reader might expect. Each number is actually an
index into a color palette, so if the first framebuffer byte
contains 12, for example, the top-left pixel receives
whatever color is currently stored in palette entry 12. Here's a
visualization of how the framebuffer and palette interact.
The framebuffer
To say that prizing apart the bespoke rendering engine inside of Noctis was difficult for me would be an understatement. I am grateful to several working San Francisco establishments, notably Ruby Wines for many sulfite-free pours and the Mechanic's Institute Chess Club for their lovely study spaces, for their unknowing support during this effort. The big question for me was: how does Noctis populate the 64,000 indexes that determine what colors to render?
It does so at a very, very low level. We'll start our digging at
the heart of the bespoke-renderer-par-excellence,
TDPOLYGS.H. First, there is the screen, which you will
recall is 320 x 200:
#define XCENTER 160
#define YCENTER 100
float dpp = 200;
Uword riga[200];
screen_x = XCENTER + x * dpp / z;
screen_y = YCENTER - y * dpp / z;
The riga array stores the beginning of each 320-byte
scanline. Once a point has been projected, y picks a
row and x picks a byte within it. The core method,
poly3d() initializes a polygon as vertices in space.
Next, it draws the edges, writing the value 255. A
separate routine then scans each row for pairs of those markers and
fills the bytes between them with the polygon's color. The
implication here is that the game does a little Michelangelo cartoon
on each frame before flooding it with color. Here are all three
stages:
Things get harder when there is more than one polygon on the screen. In a system this low level, there is no depth buffer to manage which polygon should be drawn on top of the other. The reality is both simpler and way more complicated: when two polygons overlap, whichever is drawn later wins. This means that calling code is responsible for quite an intense order of operations, and introducing a bug in this codepath can easily turn the world completely inside out. In this example, the blue rectangle is drawn last. Notice how, even when it is farther from the camera, it is drawn on top of the yellow rectangle.
Here's a little bit more of a scene, with a few more things going on:
The color palette
Much like the framebuffer is constructed in RAM and copied into the adapter's memory, the palette is constructed in memory and copied to the color registers inside the VGA's digital-to-analog converter. Each entry has a red, green, and blue component, with channel values running from 0 to 63. The complete palette is 256 triples, or 768 bytes. They look a lot like the framebuffer when we log them out:
00 00 00 01 01 01 02 02 02 03 03 03 ...
The color manipulating code is probably too fiddly for code snippets to be of any interest. But the program uses a variety of utilities to interpolate from one color to another and to try to keep its bank of 768 bytes organized. Here are a few snapshots of the palette at various stages of gameplay.
An interesting side effect of the palette-based color management
is the ease with which the game does math on top of the palette.
mask_pixels() is perhaps the most impressive: it
replaces the upper two bits of every pixel while preserving the
lower six. Those upper bits choose one of the four color banks; the
lower bits keep the pixel at the same brightness within that bank.
The picture therefore keeps all of its shapes and shading while
changing color families underneath them. Here is the same rotating
polygon (the same framebuffer) with swappable palettes and some
optional transformations at the palette layer: