Alex Russell's Game Programming Tutorial using DX

Introduction . Chapter 1 . Chapter 2 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6 . Chapter 7

Chapter 2

Double Buffering

Double buffering (also called page flipping) is a techniqued used by DX to make animation appear smooth. All drawing and updates are done to video memory that is NOT being displayed. Once the new image is ready to be displayed the hardware is told to display the video memory where the new image is stored, and the previously displayed video memory is made available to be updated. The two "pages" or "buffers" swap roles with each frame of animation displayed. The makes animation very smooth as the changes appear to be instant. The swapping is done by the DXSmith function flip().

Graphic primitives

Graphics primitives are things like dots, lines, rectangles, and sprites. Before we explore how to draw these things we first have to look at how video memory is organised.

Video memory is laid out in a simple rectangular grid where 0,0 is at the top left, x increases to the right, and y increases down. Depending on the video mode, each pixel will take 1, 2, 3, or 4 bytes. DirectX often makes the memory allocated for video memory wider than what is displayed on screen. The width of the allocated memory in bytes is called the pitch. You need to know the pitch, and the width of each pixel to draw graphic primitives. Of couse, physical memory is just one long, continous piece of memory which means once we move to the right by pitch bytes we will be at the start of the next line on screen.

Depending on the video mode the Red, Green, and Blue bits of the bytes making up a pixel can have different formats. The following code ignores this wrinkle.

To calculate the position of a pixel the formula is:
y*pitch + x*(pixel width in bytes)

All the following code are fragments to illustrate the principals. Working functions that can be added to the CgsdxIO class are included in the zip file in Chapter 4. A great deal of code that initializes DirectX is also skipped in these samples.

Single Pixel

To draw a single pixel we must calculate the offset to where the pixel is located in video memory then set the memory for one pixel. Given this information the code to draw single pixel will look something like this:

    DWORD colour;
    int offset, pixelWidth;
    DDSURFACEDESC2 surface;
    int pixelWidth;

    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;
    offset=x*pixelWidth + y*surface.lPitch;
    memcpy(((BYTE *)surface.lpSurface)+offset, &colour, pixelWidth);
    m_backSurface->Unlock(NULL);

m_backSurface is a pointer to a DX "surface". A surface is the DX term for video memory plus information about that video memory.
surface.ddpfPixelFormat.dwRGBBitCount tells us how many bits per pixel the current "surface" uses.
surface.lPitch is the pitch.
surface.lpSurface is the pointer to the actual video memory.

The first line "locks" the surface so that we can write to it.
The next line calculates the pixel width. This simple code assumes the number of bits will be a multiple of 8.
offset is the number of bytes from the start of video memory to the start of the pixel we want to set.
Once we have the offset we use memcpy() to set the pixel.
And finally, we unlock the surface.

Horizontal Line

Code to draw a horizontal line is almost identical, except we set a number of adjacent pixels in a loop:

    DWORD colour;
    long offset;
    DDSURFACEDESC2 surface;
    int pixelWidth;

    surface.dwSize=sizeof(surface);
    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;
    offset=x*pixelWidth + y*surface.lPitch;
    while ( len-- )
        {
        memcpy(((BYTE *)surface.lpSurface)+offset, &colour, pixelWidth);
        offset+=pixelWidth;
        }
    m_backSurface->Unlock(NULL);

The while loop draws pixels starting at offset for len pixels. We move to the right one pixel each time through the loop.

Vertical Line

Next we look at vertical lines. We calculate the starting offset, then we move down one for each pixel drawn. To move down one full row we add the pitch to the offset.

    DWORD colour;
    long offset;
    DDSURFACEDESC2 surface;
    int pixelWidth;

    surface.dwSize=sizeof(surface);
    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;
    offset=x*pixelWidth + y*surface.lPitch;
    while ( len-- )
        {
        memcpy(((BYTE *)surface.lpSurface)+offset, &colour, pixelWidth);
        offset+=surface.lPitch;
        }
    m_backSurface->Unlock(NULL);

Filled Rectangle

Filled rectangles are next. A filled rectangle is just a series of horizontal lines. 'width' is the width of the filled-rectangle (not the width of the screen).


    DWORD colour;
    long offset;
    DDSURFACEDESC2 surface;
    int pixelWidth, w;

    surface.dwSize=sizeof(surface);
    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;
    w=width;
    while ( height-- )
        {
        offset=x*pixelWidth + y*surface.lPitch;
        while ( width-- )
            {
            memcpy(((BYTE *)surface.lpSurface)+offset, &colour, pixelWidth);
            offset+=pixelWidth;
            }
        y++;
        width=w;
        }
    m_backSurface->Unlock(NULL);

The inner loop draws one horizontal line of pixels, and the outer loop moves down one row after each line.

A slightly optimized version of RectFill
offset=x*pixelWidth + y*surface.lPitch; is replaced with
offset+=adjust;

The second method is somewhat faster. It also demonstrates again how memory video memory is laid out. We know offset is going to move by 'width' each line and we know that to move down a full line we ned to move over by 'pitch'. So the amount to move after drawing one line of the rectanfle is pitch-width (adjusted for pixel width).

    DWORD colour;
    long offset;
    DDSURFACEDESC2 surface;
    int pixelWidth, w;

    surface.dwSize=sizeof(surface);
    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);
    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;
    offset=x*pixelWidth + y*surface.lPitch;
    adjust=surface.lPitch - width*pixelWidth;
    w=width;
    while ( height-- )
        {
        while ( width-- )
            {
            memcpy(((BYTE *)surface.lpSurface)+offset, &colour, pixelWidth);
            offset+=pixelWidth;
            }
        offset+=adjust;
        width=w;
        }
    m_backSurface->Unlock(NULL);

DX has built-in functions that take advantage of hardware to draw filled rectangles. Vertical and horizontal lines are just thin rectangles and can usually be drawn quickest using the DX rectfill function. Win32 GDI functions can also be used to draw lines, circle, etc... to DX surfaces.

Arbitrary lines

The obvious way to draw an arbitrary line is to code the equation of a line. This works, but is quite slow in practice. In 1965 J. E. Bresenham presented a much faster way to draw lines using discrete pixels. Instead of calculating the position of each pixel from the equation of a line, the line is drawn by moving in one direction at a constant rate, and moving in the other in proportion to the slope of the line. For example, a line that starts at (0,0) and goes to (200, 10) you draw a pixel at (0,0), move x to the left then check if y should be increased using pre-calculated variables. A bit of algebra is required to calculate these variables, but it turns out it is a very simple and quick operation.

The c code presents a straight forward implementation of Bresenham's line drawing algorithm. There are faster ways to code it (in assembler this algorithm can be very optimized), and there are now faster algorithms, e.g. "line slicing", but as hardware does more and more for us, it becomes less important. This code is presented to show how lines are drawn, but unless a large number of lines are being drawn the win32 GDI functions can be used to draw lines.

Note: for unsigned integers x<<1 is the same as x*2, and x>>1 is the same as x/2.

int Linev(int x0, int y0, int x1, int y1, DWORD colour)
{
    int  inc1, inc2, i;
    int cnt, y_adj, dy, dx, x_adj;
    unsigned char far *p;
    HRESULT hr;
    int err=0;
    DDSURFACEDESC2 surface;
    int pixelWidth;

    // get a pointer to video memory
    surface.dwSize=sizeof(surface);
    hr=m_backSurface->Lock(NULL, &surface, DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR, NULL);

    pixelWidth=surface.ddpfPixelFormat.dwRGBBitCount/8;

        // check for vertical line
        // the general code does NOT handle vertical lines (infinite slope)
	if ( x0 == x1 )
		{
		// vertical line
		if ( y0 > y1 )
			{
			i=y0;
			y0=y1;
			y1=i;
			}

		p=(BYTE *)surface.lpSurface + y0*surface.lPitch + x0*pixelWidth;
		i=y1 - y0 + 1;
		while ( i-- )
			{
			memcpy(p, &colour, pixelWidth);
			p+=surface.lPitch;
			}
		}
	else
		{
                // check for horizontal line (this is just faster)
		if ( y0 == y1 )
			{
			// horizontal line
			if ( x0 > x1 )
				{
				i=x0;
				x0=x1;
				x1=i;
				}
			p=(BYTE *)surface.lpSurface + y0*surface.lPitch + x0;
			i=x1 - x0 + 1;
			while ( i-- )
				{
				memcpy(p, &colour, pixelWidth;
				p+=pixelWidth;
				}
			}
		else
			{
			// general line --------------------------------------
			dy=y1 - y0; 
			dx=x1 - x0; 
			// is it a shallow, or steep line?
			if ( abs(dy) < abs(dx) )
				{
				// lo slope, shallow line
				// we always want to draw from left to right
				if ( x0 > x1 )
					{
					// swap x's, and y's
					i=x0;
					x0=x1;
					x1=i;
					i=y0;
					y0=y1;
					y1=i;
					}

				dy=y1 - y0;  // dy is used to calculate the increments
				dx=x1 - x0;  // dx is line length
				if ( dy < 0 )
					{
					// going up the screen
					dy=-dy;
					y_adj=-surface.lPitch;
					}
				else
					y_adj=surface.lPitch;	 // going down

				// calulate the increments
				inc1=dy<<1;
				inc2=(dy - dx)<<1;
				cnt=(dy<<1) - dx;

				// set p to start pixel
				p=(BYTE *)surface.lpSurface + y0*surface.lPitch + x0*pixelWidth;
				dx++;
				while ( dx-- )  // for the length of the line
					{
					memcpy(p, &colour, pixelWidth); // *p++=colour; 
					p+=pixelWidth;
					// set one pixel, move right one pixel

					if ( cnt >= 0 ) // is it time to adjust y?
						{
						cnt+=inc2;
						p+=y_adj;
						}
					else
						cnt+=inc1;
					}
				}
			else
				{
				// hi slope - like lo slope turned on its side
				// always draw top to bottom
				if ( y0 > y1 )
					{
					// swap x's, and y's
					i=x0;
					x0=x1;
					x1=i;
					i=y0;
					y0=y1;
					y1=i;
					}

				dy=y1 - y0;  // dy is line length
				dx=x1 - x0;  // dx is used to calculate incr's

				if ( dx < 0)
					{
					dx=-dx;
					x_adj=-pixelWidth;  // moving left
					}
				else
					x_adj=pixelWidth;   // moving right

				inc1=dx<<1;
				inc2=(dx - dy)<<1;
				cnt=(dx<<1) - dy;

				// set p to first pixel position
				p=(BYTE *)surface.lpSurface + y0*surface.lPitch + x0*pixelWidth;
				dy++;
				while ( dy-- )  // for height of line
					{
					memcpy(p, &colour, pixelWidth); // *p=colour;   // set one pixel
					p+=surface.lPitch;  // move down one pixel

					if ( cnt >= 0 )  // is it time to move x?
						{
						cnt+=inc2;
						p+=x_adj;
						}
					else
						cnt+=inc1;
					}
				}

			}
		}

    m_backSurface->Unlock(NULL);

    return err;

}

There are similar algorithms for circles and elipses which can be found in any good computer graphics text book. Most games these days deal mainly with 3D graphics, but it is good to understand how video memory is laid out, and how to draw these graphic primitives. Even 2D games mainly deal with drawing "sprites" which will be covered in chapter 3.

Introduction . Chapter 1 . Chapter 2 . Chapter 3 . Chapter 4 . Chapter 5 . Chapter 6 . Chapter 7

Copyright 2004 (c), Alex Russell, All rights reserved