Return to Suhalia Noctis

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.

13h

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:

One polygon

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.

Multiple polygons

Here's a little bit more of a scene, with a few more things going on:

Scanline renderer

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.

Captured palettes

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:

Noctis visor modes