Recreating Voodoo Graphics and a Late-1990s Gaming PC on an FPGA
Pangram verdict · v3.3
We believe this text is mainly AI, with some human-written content.
AI likelihood · overall
AIArticle text · 1,527 words · 2 segments analyzed
I've spent the last month adding features and improving performance in z486_MiSTer, mostly working through games from the first half of the 1990s. Looking a few years ahead brought me to another change I wanted to explore: the arrival of 3D graphics cards. The first one that left a strong impression on me was the Voodoo. The game was Need for Speed II SE. Smooth textures, fog, and the speed of the whole thing made it feel like a new generation of PC gaming. Could I recreate that on an FPGA now that the z486 CPU exists? The result of this detour is zSST, a SystemVerilog implementation of the 3dfx Voodoo Graphics, or SST-1. Combined with my z486 CPU and the surrounding PC hardware, it forms z486 XL: a DOS PC with Voodoo graphics running in the programmable logic of a Xilinx KV260 board. Tomb Raider now runs with its original 3dfx renderer. zSST implements most of the central Voodoo features: prepared triangles, texture filtering and mipmapping, depth and alpha tests, fog, blending, dithering, framebuffer access, and buffer swaps. It supports both the fixed-point and floating-point setup interfaces. Hardware game testing is still concentrated on Tomb Raider; broader compatibility and later Voodoo generations are work for another day. The CPU and renderer run at 100 MHz on the KV260. That board has enough logic, DSP blocks, on-chip memory, and DDR bandwidth for the combined design. The DE10-Nano does not have room for this graphics addition. The KV260 uses its onboard DDR; there is no external SDRAM module to add. Starting from the programming model Fortunately, there is plenty of material to work from. 3dfx released the Glide source in 1999, before NVIDIA acquired its core graphics assets in December 2000. The surviving Glide source and SST-1 specification explain how software prepares triangles, configures the pixel pipeline, and manages textures and framebuffers. The specification is a behavioral target, rather than a circuit diagram. It tells what should happen when software writes a register, but leaves many implementation choices open. 86Box provides useful references for complicated rendering behavior. The earlier MAME Voodoo work is another part of this preservation history. SpinalVoodoo supplied particularly useful Glide traces and reference screenshots for testing. From triangles to 3D, one pixel per clock Voodoo Graphics turns triangles into pixels, leaving much of the 3D work to the host CPU. Its command interface is surprisingly compact: five main command registers drive the accelerator. Register Action triangleCMD Start rendering a prepared triangle. ftriangleCMD Start a triangle through the floating-point setup interface. nopCMD Flush the pipeline; optionally reset the statistics counters. fastfillCMD Clear a clipped rectangle of color and/or depth data. swapbufferCMD Switch the displayed buffer, immediately or synchronized to vertical retrace. Both triangle commands launch the same rendering pipeline. Other registers hold coordinates, gradients, and render state, while memory-mapped regions provide texture uploads and direct framebuffer access. The main drawing primitive is simply a prepared triangle. For game developers, Glide presents a friendlier interface: void grDrawTriangle(const GrVertex *a, const GrVertex *b, const GrVertex *c); Before this call, the host CPU transforms the 3D geometry, computes vertex lighting, clips it, and projects it onto the screen. Glide then prepares the screen-space triangle and its parameter gradients—the increments used to interpolate values across its surface—and writes the triangle command to start rendering. Unlike later GPUs such as the GeForce 256, SST-1 has no hardware transform-and-lighting engine. That still leaves plenty of work for the accelerator. The rasterizer finds which pixel centers lie inside the triangle and interpolates their color, depth, and texture coordinates. The texture unit fetches and filters texels; the framebuffer unit combines colors, applies visibility tests and fog, blends with the existing image, and writes the result. A pixel takes several stages to finish, while new pixels can keep entering. The original card divides this work between two ASICs: the FBI, or Frame Buffer Interface, and TREX, the texture mapping unit, usually called the TMU. At a 50 MHz graphics clock, the advertised peak is one textured, depth-tested output pixel per clock: 50 million pixels per second. One pixel per clock does not mean that a pixel finishes in one clock. It means that different stages can work on different pixels simultaneously: while one pixel is being textured, an earlier one can be blended and another written out. Once the pipeline is full, it can ideally accept and finish a pixel every clock, provided memory keeps up. That is the appeal of a fixed-function pipeline. A software renderer executes many instructions for each pixel; dedicated hardware overlaps that work across a steady stream of pixels. Voodoo brought richly textured 3D games to life at a fluid 30 FPS or more—a big part of what made it so popular. Building the pixel pipeline Compared with an x86 CPU, the arithmetic path is pleasantly regular. Let's follow a pixel from its interpolated parameters through texturing and color operations to the framebuffer, starting with how the numbers are represented. Fixed point behind a floating-point interface Floating-point arithmetic is central to modern GPU programming. SST-1 sits at an interesting transition: software can submit floating-point values, but the rendering machinery largely operates in fixed point—integers with an implicit scale factor. Setup value Fixed-point register format Screen X and Y 12.4 Red, green, blue, alpha 12.12 Depth Z 20.12 Texture S/W and T/W 14.18 Reciprocal W 2.30 Here 12.4 means twelve bits before the binary point, including the sign, and four fractional bits. A screen coordinate of 10.5 is therefore stored as the integer 168: multiply by 16 to encode it, divide by 16 to recover the value. Those fractional bits let the rasterizer handle vertices between pixel centers. The fvertex, fstart, and floating-point gradient registers accept IEEE single-precision values. SST-1 converts them into its internal fixed-point representation, and zSST follows that contract. Once the triangle is prepared, advancing along a scanline mostly means adding a precomputed increment to each interpolated parameter. Much of the pixel-by-pixel work becomes simple integer addition. Four texels for one pixel For perspective-correct texturing, the TMU interpolates S/W, T/W, and 1/W, then divides the first two by the third to recover texture coordinates. This keeps a floor or wall texture in perspective as the surface recedes. The TMU also selects a mip level: a smaller version of the texture for pixels that cover a larger area of its surface. This reduces aliasing and shimmering in the distance. Bilinear filtering then combines four neighboring texels—the pixels of the texture—around the sample position. First blend the top pair horizontally, then the bottom pair, and finally blend vertically between those two results. The fractional position determines the weights, producing a smooth transition between texel colors instead of an abrupt jump from one to the next. Four texture reads produce one filtered sample. In zSST, a four-stage front end pipelines the perspective and level-of-detail calculations. Address generation and cache lookup supply the texels, and two registered decode stages turn their stored formats into colors for filtering and texture combining. Palette-based and NCC-encoded textures need different decoding rules, but ultimately feed the same pixel stream. Color, tests, fog, and blending Once texture and framebuffer data are available, zSST's FBI pixel path uses six registered stages: Stage Main work F0 Select sources, check chroma key, prepare Z/W depth values. F1 Apply the color and alpha combine functions. F2a Test alpha/depth and look up the fog factor. F2b Apply fog. F3 Reconstruct destination color and perform alpha blending. F4 Convert to framebuffer precision, dither, and apply write masks. These stage boundaries are chosen to meet the FPGA's clock target. The SST-1 specification describes the operations but does not reveal the original ASIC's exact pipeline registers. Splitting fog lookup from fog application, for example, keeps a long arithmetic path out of a single clock while retaining the ability to accept one pixel per clock. The result is written to the back buffer. A retrace-synchronized buffer swap then displays the finished image without switching buffers halfway through scanout. The original Voodoo was a 3D-only add-on, passing the ordinary VGA card's output through when inactive. z486 XL makes the analogous selection between the PC's VGA output and zSST's display output inside the FPGA system. The hard part: feeding it from memory The zSST pixel pipeline proved relatively straightforward to implement, at least compared with z486's CPU pipelines. Keeping it fed turned out to be much harder. A bilinear sample needs four texels from separate addresses. A depth-tested, blended pixel also needs the existing depth and color, followed by writes of the new values. Performing those accesses one at a time quickly destroys throughput. I ended up spending more time designing, tuning, and debugging the memory system than the arithmetic pipeline. How the original card supplied the pixels The board and its division of labor, with matching chip positions in both views.
Connections are schematic; click either image to enlarge. Photo: Konstantin Lanzet; crop: Pittigrilli, Wikimedia Commons. Photo license: GFDL 1.2 or later. Schematic: nand2mario. The division of labor is visible on this Diamond Monster 3D. The upper 3dfx chip is the TMU, the lower one the FBI, each with four EDO RAM chips to its right.