Voodoo Registers – Part 3

In the previous post on hacking on the Voodoo Registers, I got as far as rendering our first triangle. It was a single, flat-shaded triangle and was pretty uninteresting. In this post we’ll look at how the Voodoo Graphics chipset handles linear interpolation, which is necessary for smooth shading, and ultimately texture mapping. We’ll also see the first signs of divergence between the operation of the Voodoo hardware and how the Glide API exposed it.

When we rendered our flat shaded triangle, we programmed its color into four registers, fstartR, fstartG, fstartB, and fstartA. This actually represents the color of vertex A, which is the topmost vertex when the vertices are sorted in order of ascending Y coordinate. The reason these registers have start in their names is because the hardware actually modifies their values internally using supplied deltas, or gradients. Setting these registers to zero (or leaving them at zero) results in the same color being used for every pixel in the triangle. However, we can set them to non-zero values and have the hardware iterate the values across the triangle, resulting in smooth shading.

Voodoo Registers for Gradients

There is a set of gradient registers for each of the X and Y screen axes. As the hardware steps the current pixel coordinates across and down the screen, it adds these gradients to the starting values programmed earlier. These registers are called fdRdX through fdAdX for the X gradients and fdRdY through fdAdY for the Y gradients. There are plenty of ways to calculate the gradients for the triangle, but one particularly elegant method is seen in the original Glide source code. Essentially, for each component of the gradient I (where I represents each of the RGBA components here, but later more components), we have:

dI_AB = I_b - I_a;
dI_BC = I_c - I_b;
dIdX = dI_AB * dY_BC - dI_BC * dY_AB;
dIdY = dI_BC * dX_AB - dI_AB * dX_BC;

This code is effectively working with barycentric coordinates and so we normalize everything by the area of the triangle, which we would have calculated earlier in order to program the ftriangleCMD register. We repeat this for each of the interpolants and program the resulting values into the gradient registers. Now, as the hardware steps horizontally, it will add (or subtract) the horizontal gradients to the current values, and perform likewise for the vertical gradients.

Rather than pass the colors of each vertex to the triangle setup function, we’re instead going to modify it to take pointers to three vertices, which each hold all of the parameters (coordinates, colors and later more) for each vertex. The vertex declaration looks like this:

struct VD_VERTEX
    float       x;
    float       y;
    float       r;
    float       g;
    float       b;
    float       a;

The prototype of the draw_triangle function is changed to this:

void draw_triangle(const VD_VERTEX * va,
                   const VD_VERTEX * vb,
                   const VD_VERTEX * vc);

After performing the setup for gradients, we now draw our triangle, and the result is this:

Smooth Shaded Triangle Rendered by Directly Prodding the Voodoo Registers

Smooth Shaded Triangle

Success! One smooth shaded triangle. Well, actually not that smooth. You’ll notice some banding artifacts on the rendered display. This is because the framebuffer mode we’re using here is RGB565, which has only 5 bits of precision for the red and blue channels, and 6 for the green channel. Although not related to interpolation, we can turn on dithering, which trades spatial resolution for perceived color depth.

Smoother than Smooth — Dither

Dithering is controlled by a couple of bits in the fbzMode register (@ 0x110). In particular, bit 8 turns dithering on and off, and bit 11
controls the algorithm. Simply enabling dithering can improve the perceived smoothness of our triangle substantially:

Dithered Smooth Shaded Triangle Rendered by Directly Prodding the Voodoo Registers

Dithered Smooth Shaded Triangle

Now, at the top of this post I said that we’d see the first signs of divergence between the Glide API and how the hardware actually works. Our new draw_triangle function looks a lot like Glide’s grDrawTriangle function, and at this point, operates in a very similar manner. Indeed, that function would sort vertices in order of Y, calculate gradients and program all the registers. Care was taken (some of these functions were hand-coded in assembly or optimized for certain compilers’ schedulers) to calculate all of this as efficiently as possible. However, these gradient registers are sticky (like most of the registers in the Voodoo).

By sticky, I mean that they hold their values, and those values can be reused across primitives. It’s arguable whether exposing this at the API level is of any utility. In fact, the Glide API did supply grDrawPlanarPolygon, along with a few similar functions, that would take advantage of this. However, it isn’t possible, as a user of Glide, to use this in any other way.

In the next part of the series, I’ll get into drawing more than one triangle. We’ll also look at double buffering and the consequences of filling the FIFO.

Hardware, Retro , , ,

Comments are closed.