Voodoo Registers – Part 2

In the first part of this series, I explained how to install the sstfb framebuffer device driver on Linux, query the base address of the 3Dfx Voodoo chipset and map it into a user-mode process. Then I set about writing directly to the Voodoo registers I’d mapped and got as far as issuing a single accelerated color buffer clear. In this post, we set about drawing our first triangle.

To implement our fast clear procedure, we simply write the color we want into the color1 register, set the bounds of the rectangle into the clipLeftRight and clipLowYHighY registers and then write a value (any value) into the fastfillCMD register. In general, this is how commands in the Voodoo chipset work — set state in state registers and then write a command value to a command register. Indeed, this is also how triangles are drawn.

Status Check

When you write to one of the Voodoo registers, that write goes into a FIFO and is processed in order by the Voodoo chipset. That FIFO is 64 entries deep and if it fills up, further writes are at best discarded, and at worst, confuse the chip and cause it to hang. The number of free entries in the FIFO is available, along with a number of other pieces of useful information in the status register, which is the very first entry in the register file. Its declaration looks like this:

union VD_STATUS_REG                      // Status register @ 0x000
{
    struct
    {
        uint32_t    pci_fifo_free  : 6;  // 5:0   PCI FIFO freespace (0x3f=FIFO empty).  Default is 0x3f.
        uint32_t    v_retrace      : 1;  // 6     Vertical retrace (0=Vertical retrace active, 1=Vertical retrace inactive).  Default is 1.
        uint32_t    fbi_busy       : 1;  // 7     FBI graphics engine busy (0=engine idle, 1=engine busy).  Default is 0.
        uint32_t    trex_busy      : 1;  // 8     TREX busy (0=engine idle, 1=engine busy).  Default is 0.
        uint32_t    sst_busy       : 1;  // 9     SST-1 busy (0=idle, 1=busy).  Default is 0.
        uint32_t    display_buffer : 2;  // 11:10 Displayed buffer (0=buffer 0, 1=buffer 1, 2=auxiliary buffer, 3=reserved).  Default is 0.
        uint32_t    mem_fifo_free  : 16; // 27:12 Memory FIFO freespace (0xffff=FIFO empty).  Default is 0xffff.
        uint32_t    swap_pending   : 3;  // 30:28 Swap Buffers Pending.  Default is 0x0.
        uint32_t    pci_interrupt  : 1;  // 31    PCI Interrupt Generated.  Default is 0x0. (not currently implemented).
    };
    uint32_t        val;
};

The first entry, pci_fifo_free is the one we’re interested in. When the PCI FIFO becomes full (i.e., the number of free entries becomes zero), we need to wait for it to drain a little before we can write more data into the chipset. We can implement a wait like this:

void voodoo::wait_fifo_free(uint32_t space)
{
    while (regs->status.pci_fifo_free < space)
    {
        usleep(10);
    }
}

Here, we use the usleep function, which puts the calling thread to sleep for the number of nanoseconds specified in the argument. We can use the wait_fifo_free function to wait until at least space entries are free in the PCI FIFO. If more entries become free while we’re waiting, that’s fine — it just means we’re not going to have to wait next time. We’ll use wait_fifo_free in our function to draw a triangle.

Triangles and Parameters in the Voodoo Registers

The Voodoo chipset is capable of drawing perspective correct, Gouraud shaded, textured, depth tested triangles. The parameters for this are all stored in the register file and a command register is provided to trigger drawing. In fact, the chipset can accept parameters in fixed-point or floating-point (each stored in its own part of the register file) and separate command registers are provided to indicate which it should use. We’re going to use the floating point registers for our work. To define the three vertices of our triangle, we’ll use six registers, fvertexAx, fvertexAy, fvertexBx, fvertexBy, fvertexCx, and fvertexCy, which live at offsets 0x088 through 0x09C in the register file.

Now, the Voodoo is somewhat picky about how triangles are rendered. It is really a basic hardware rasterizer and doesn’t include much polygon setup logic in the traditional sense. First, it requires that the three vertices be sorted in order of ascending Y coordinate. Second, it needs to know whether the triangle is left handed or right handed. To see what this means, check out the diagram below:

Left and Right Handed Triangles as seen by the Voodoo Registers

Left and Right Handed Triangles

Vertices A, B and C have been sorted in order of ascending Y (remember, the Y origin is at the top of the display here). The triangle on the left is left handed and the triangle on the right is right handed. We call the longest vertical side (from A to C) the major axis, and the handedness of the triangle is determined by which side of line AC vertex B lies on. Conveniently, if we calculate the signed area of the triangle, the sign of the area tells us which way the triangle points. We calculate the area like this:

float dxAB = Ax - Bx;
float dxBC = Bx - Cx;
float dyAB = Ay - By;
float dyBC = By - Cy;

float area = (dxAB * dyBC - dxBC * dyAB);

In fact, once we’ve set up the (sorted-in-Y) coordinates of the three vertices, the documentation says we need to write this area to the ftriangleCMD register (at 0x100) to trigger rendering, but only the sign of the area is needed by hardware to produce the triangle. Before go ahead and draw our triangle, however, there’s one last thing we need to tell the hardware — what color the triangle should be. Again, we’ll do this by writing to registers in the Voodoo’s register file. The four registers of interest are fstartR, fstartG, fstartB and fstartA, which are located at offsets 0x0A0 through 0x0B0 1, respectively.

Draw Something!

Our initial triangle drawing function performs the following steps:

  • Sort the vertices into order of ascending Y coordinate
  • Wait for there to be enough space in the FIFO for the required register writes (11 in this case — six position, four color and one command register)
  • Write the three vertex coordinates into the fvertexAx through fvertexCy registers
  • Write the color of the triangle into the fstartR through fstartA registers
  • Calculate the area of the triangle and write it into the ftriangleCMD register.

Also, one technical detail is that we surround the write to the command register with memory fence operations. On x86, we implement this using the sfence instruction, which strongly orders otherwise weakly ordered stores. This prevents the command register write from being write combined with any other writes into the Voodoo registers and is a practice recommended by the programming documentation. The code looks like this:

void voodoo::draw_triangle(const float A[2],
                           const float B[2],
                           const float C[2])
{
    const float * va;
    const float * vb;
    const float * vc;

    /*

        SORT VERTICES A, B AND C INTO ORDER OF ASCENDING Y
        AND MAKE va, vb AND va POINT TO THEM IN THAT ORDER
        ... CODE OMITTED FOR BREVITY.

    */

    float dxAB = va[0] - vb[0];
    float dxBC = vb[0] - vc[0];
    float dyAB = va[1] - vb[1];
    float dyBC = vb[1] - vc[1];

    float area = (dxAB * dyBC - dxBC * dyAB);

    if (area == 0.0f)
        return;

    wait_fifo_free(11);

    regs->fvertexAx = va[0];
    regs->fvertexAy = va[1];
    regs->fvertexBx = vb[0];
    regs->fvertexBy = vb[1];
    regs->fvertexCx = vc[0];
    regs->fvertexCy = vc[1];

    regs->fstartR = 240.0f;
    regs->fstartG = 160.0f;
    regs->fstartB = 40.0f;
    regs->fstartA = 255.0f;

    sfence();

    (volatile float&)regs->ftriangleCMD = area;

    sfence();
}

And the result? Our first triangle:

First triangle rendered by writing directly to the Voodoo Registers

First Voodoo Triangle

This seems like quite a bit of work to do for a single triangle. Although it is, we have to remember that the CPU is performing quite a bit of the setup work that would be done in hardware by a modern GPU. This is pretty much the way that the original Glide worked (with grDrawTriangle). Eventually, we’ll be able to optimize this access, re-use computation between triangles and punt a lot of the work into another thread. Next time, we’ll look at the gradient registers, which are fundamental to smooth shading and texture mapping and at some of the color path configurations supported by the Vooodoo.

Notes:

  1. The astute reader may notice that there’s room for a fifth register in amongst the color registers. This is the fstartZ register, which will be used for depth and which I’ll cover in a future post.
Hardware, Retro

2 responses to Voodoo Registers – Part 2


  1. Steen Rasmussen

    Good stuff. I hope to find time to setup a Voodoo1 machine this weekend so I’ll be able to play a long. Do you have any recommendations to what documentation would be worthwhile reading? I see a lot of stuff here but not sure where to start.

  2. Thanks! That site is where I found most of the documentation I’ve been using. The register spec is http://www.falconfly.de/downloads/voodoo1-reference-rev161.zip. There’s lots of missing information. Some got filled in with the Voodoo 2 spec (http://www.falconfly.de/downloads/voodoo2-reference-rev116.zip). I’ve also been reading through the Glide source code that’s up at SourceForge.