My C and Assembler 3D Real Time Renderer from 1997

bhouston1 pts0 comments

Rendering Real-Time 3D Before GPUs<br>In early 1997, while I was in high school, I built a software renderer that was capable of per-pixel Phong lighting, texture mapping with bump mapping in real-time on then modern 486 DX2 50Mhz computer. I taught myself most of the techniques from BBS tutorials, early internet writeups, demo-scene source releases, and Michael Abrash's writing.

I recently went back through the original codebase. This post is a write-up of how I solved the problem of getting high-quality real-time 3D results on hardware with no GPU, a slow FPU, and a 256-color display.

I wrote the engine in C++ with Borland C++ 4.0. Wherever the inner loops needed to be fast, I dropped into hand-written x86 assembly through Borland C++'s integrated TASM capabilities.

The full source is here GitHub as 3DMaskDemo1997.

VGA Mode 13h and a Software Double Buffer#

I used VGA Mode 13h , the straightforward 320x200 256-color mode that you could enable with a single BIOS interrupt.

This was a 16-bit DOS program, so it lived in the standard conventional-memory world of at most 640 KB. That limit shaped everything from how I stored assets to how large I could make the off-screen buffers and lookup tables.

The mode switch was as simple as:

// Switch the VGA card into 320x200 with 256 colors.<br>asm mov ax, 0x13<br>asm int 0x10

Mode 13h did not give me a second hardware page to flip between, so I built my own double buffering in software. Each frame rendered into an off-screen buffer in conventional memory. Once the frame was done, I copied only the dirty rectangle into video memory using a hand-written x86 block copy.

That gave me the flicker-free animation I wanted without moving to Mode X and its unchained page-flipping setup. I built a software substitute for hardware page flipping rather than hardware page flipping itself.

This is the dirty-rectangle copy routine:

void copyrect ( word source, word dest, recttype rect )<br>int copywidth, skipwidth, copyheight, startoffset;

// Round the rectangle to dword boundaries so the copy loop<br>// can use movsd efficiently.<br>copywidth = ((rect.r + 3) >> 2) - (rect.l >> 2);<br>skipwidth = 320 - (copywidth copyheight = rect.b - rect.t + 1;<br>startoffset = rect.t * 320 + ((rect.l >> 2)<br>// Copy only the dirty rectangle from the virtual screen to video memory.<br>asm push ds<br>asm mov ds, [source]<br>asm mov es, [dest]<br>asm mov si, [startoffset]<br>asm mov di, si<br>asm mov dx, [skipwidth]<br>asm mov bx, [copyheight]<br>lineloop:<br>asm mov cx, [copywidth]<br>asm rep movsd<br>asm add di, dx<br>asm add si, dx<br>asm dec bx<br>asm jnz lineloop<br>asm pop ds

Fixed-Point Math and Rotation#

The 486 could do floating-point math, but not quick enough for the frame rates I wanted, so I used fixed-point arithmetic instead. There was a significant cost to converting between integers and floating point on the 486 in addition to the raw floating-point arithmetic cost, so keeping the inner loops mostly in integer math was a practical win.

Instead of using the entire range of a 16-bit integer for whole numbers, I reserved some of those bits for the fractional part. Then I could use ordinary integer add, subtract, and multiply instructions, as long as I kept track of where the implied decimal point lived. In a few places I did need 32 bit intermediates for multiplications.

For the rotation code, 256 represented 1.0, so multiplying a coordinate by a trig value also multiplied it by 256. The >> 8 shifted the result back down by 256, restoring the intended scale after the fixed-point multiply.

Different parts of the renderer used different fixed-point scales:

8 fractional bits for the rotation math

6 fractional bits for rasterizer screen-space coordinates

14 fractional bits for some texture and lighting coordinate intermediates before scaling them back down

I tuned the precision per subsystem instead of using one format across the whole engine.

For example, here is the actual rotation setup from transformvertices():

// Compute the trig values once per frame and store them in<br>// 8-bit fixed-point form.<br>xcos = (int) ( cos ( (float)xrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>xsin = (int) ( sin ( (float)xrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>ycos = (int) ( cos ( (float)yrotation / MAX_ANGLE * M_PI * 2 ) * 256 );<br>ysin = (int) ( sin ( (float)yrotation / MAX_ANGLE * M_PI * 2 ) * 256 );

for ( loop = 0; loop // Load the object-space vertex.<br>x = objectvertex[loop].x;<br>y = objectvertex[loop].y;<br>z = objectvertex[loop].z;

// Rotate around X and shift away the fixed-point scale.<br>// The trig values are scaled by 256, so >> 8 divides by 256 again.<br>ytemp = y;<br>ztemp = z;<br>y = ( ytemp * xcos - ztemp * xsin ) >> 8;<br>z = ( ytemp * xsin + ztemp * xcos ) >> 8;

// Rotate around Y the same way.<br>xtemp = x;<br>ztemp = z;<br>x = ( xtemp * ycos - ztemp * ysin ) >> 8;<br>z = ( xtemp * ysin + ztemp * ycos ) >> 8;

Plastic Rendering via Phong Light Map#

Plastic mode was the simplest of the three renderers. It used a single material color and a procedurally generated...

point mode rect loop fixed ztemp

Related Articles