Alex Russell's Dos Game Programming in C for Beginners

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

Chapter 2

Double buffering vs. page flipping, and Syncing to vertical retrace

To get smooth animation it is common to do all the drawing to an area of memory that is not being displayed by the video adapter. Then during the vertical blanking period to copy this off screen image to video memory. There are two main ways to do this: double buffering, and page flipping.

For page flipping the video hardware must support more than one visible page of video memory, and have a way to quickly change the page being displayed. By a 'page' of video memory I mean enough memory to hold one full screen. Mode13h does not support page flipping. Only 64k is directly accessible on the standard VGA card, and almost all of this memory is used to display the screen in mode13h.

Double buffering requires that enough memory to hold a page of video be allocated in normal memory. All drawing is done to this 'off screen page' then the off screen page is copied to main video memory during the vertical blank to make it visible. This is the method that we will be using.

The copying (or flipping) is done during the vertical re-trace to prevent 'tearing' and flickering. Tearing happens when you update the screen slower than the CRT draws the current image which causes odd tearing effects to appear on the screen. During the vertical blank the CRT image is not being 'drawn' onto the physical screen because the position of the electron beam is being moved to the top of the screen from the bottom. If we copy our image to video memory during this time the image on the screen will not tear, flicker or flash. On the very fast computers available today this is less of a problem, but it is an important technique to be aware of.

The downside of these techniques is that time may be wasted waiting for the vertical blank.

Graphic Primitives

Pixels

It is actually rare to draw a single pixel in game, but it is a good exercise. All of our code will draw to an off screen buffer, then a separate function will copy the off screen buffer to video memory, making it visible.

Here is the code to copy the off screen buffer to video memory, making it visible.

unsigned char far *screen;		// pointer to the VGA video memory
unsigned char far *off_screen;	// pointer to our off screen buffer
int screen_width, screen_height; 
unsigned int screen_size;

First we want to initialize mode13h, and create the off screen buffer.

int init_video_mode(void)
{
	off_screen=farmalloc(64000u); 

	if ( off_screen )
		{
		screen=MK_FP(0xa000, 0);
		screen_width=320;
		screen_height=200;
		screen_size=64000u;
		enter_mode13h();
		_fmemset(offscreen, 0, screensize);
		return 0;
		}
	else
		{
		// no mem! Return error code!
		leave_mode13h();
		printf("Out of mem!\n");
		return 1;
		}
}

#define INPUT_STATUS_0 0x3da

// copy the off screen buffer to video memory
void update_buffer(void)
{
	// wait for vertical re-trace
	while ( inportb(INPUT_STATUS_0) & 8 )
		;
	while ( !(inportb(INPUT_STATUS_0) & 8) )
		;
	
	// copy everything to video memory
	_fmemcpy(screen, off_screen, screen_size);
}

We wait for the vertical re-trace by checking the value of the INPUT_STATUS port on the VGA card. This returns a number of flags about the VGA's current state. Bit 3 tells if it is in a vertical blank (2^3 = 8). We first wait until it is NOT blanking, to make sure we get a full vertical blank time for our copy. Then we wait for a vertical blank.

Now that we can update the whole screen lets draw a single pixel. To draw a pixel we need to calculate where in the off screen buffer to change the value of one byte to the index of the colour we want. If we want to draw on the first line it is easy, it is just x pixels from the start. To draw on a line other than the first line we have to move down to the yth line. How do we do that? Each line is 320 pixels long. For each y we must move 320 pixels. Therefore the calculation of a pixel's position is:

Offset=y*320 + x;

Or, more generically:

Offset=y*screen_width + x;

We now know everything we need to draw a single pixel.

void draw_pixel(int x, int y, int colour)
{

	*(off_screen + y*screen_width + x)=colour;
}

What that line of code does:

y*screen_with + x	this gives us the offset from the start of the buffer to
			The position of the pixel at (x,y)
*(off_screen + offset) sets the value at offset to colour.

Note that '*' means both 'de-reference pointer' and multiplication in c. The compiler can tell which meaning is to be used from the context of the code.

Then update_buffer() would have to be called to make the pixel actually appear on the CRT screen.

To get the value of a pixel we just return the value in off_screen at the offset for x, and y.

 int get_pixel(int x, int y)
{

	return *(off_screen + y*screen_with + x);
}

Horizontal lines

Horizontal lines are only slightly more complicated than pixels. The offset to the start of the line is calculated then the line is drawn by setting adjacent pixels on the same line to the colour.

void horz_line(int x, int y, int len, int colour)
{
	unsigned char far *p;

	p=off_screen + y*screen_width +x;  // make p point to the start of the line
	_fmemset(p, colour, len);	    // fill in the line
}

p is set to point to the start of the horizontal line then the memset() function is used to fill in the line with colour. This code does not check to see if the line goes off the right edge of the screen. If the line did extend past the right edge it would continue on the next line starting at the left edge. Drawing a very long line at (319, 199) can crash the computer as the line will be 'drawn' into memory past the end of the of screen memory.

Vertical lines

For vertical lines we have to move down one line each pixel. This is the same as moving the width of the screen into the off screen buffer.


The pixel at (3, 0) is at offset:
Off set = y*320 + x
Off set = 0*320 + 3
Off set = 3

The pixel at (3,1) is at offset: Off set = y*320 + x Off set = 1*320 + 3 Off set = 323

As you can see as we move down one row the offset will increase by the width of the screen.

void vert_line(int x, int y, int len, int colour)
{
	unsigned char far *p;
	
	p=off_screen + y*screen_width +x;  // make p point to the start of the line
	while ( len--)			// repeat for entire line length
		{
		*p=colour;		// set one pixel
		p+=screen_width;	// move down one row
		}
}

Filled rectangles

Filled rectangles are drawn by drawing horizontal lines to fill the rectangle. The off set of the first line is calculated, a horizontal line drawn, then we move down one row and repeat.

void rect_fill(int x, int y, int width, int height, int colour)
{
unsigned char far *p;
	
	p=off_screen + y*screen_width +x;  // make p point to the start of the line
	while ( height--)			// repeat for entire line height
		{
		_fmemset(p, colour, width);	// set one line
		p+=screen_width;		// move down one row
		}

}

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.

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

void line(int x0, int y0, int x1, int y1, int colour)
{
	int  inc1, inc2, i;
	int cnt, y_adj, dy, dx, x_adj;
	unsigned char far *p;

	if ( x0 == x1 )
		{
		// vertical line
		if ( y0 > y1 )
			{
			i=y0;
			y0=y1;
			y1=i;
			}

		p=off_screen + y0*screen_width + x0;
		i=y1 - y0 + 1;
		while ( i-- )
			{
			*p=colour;
			p+=screen_width;
			}
		}
	else
		{
		if ( y0 == y1 )
			{
			// horizontal line
			if ( x0 > x1 )
				{
				i=x0;
				x0=x1;
				x1=i;
				}
			p=off_screen + y0*screen_width + x0;
			i=x1 - x0 + 1;
			_fmemset(p, colour, i);
			}
		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=-screen_width;
					}
				else
					y_adj=screen_width;	 // going down

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

				// set p to start pixel
				p=off_screen + y0*screen_width + x0;
				dx++;
				while ( dx-- )  // for the length of the line
					{
					*p++=colour;  
					// 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=-1;  // moving left
					}
				else
					x_adj=1;   // moving right

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

				// set p to first pixel position
				p=off_screen + y0*screen_width + x0;
				dy++;
				while ( dy-- )  // for height of line
					{
					*p=colour;   // set one pixel
					p+=screen_width;  // move down one pixel

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

			}
		}
}

Chapter 2 Exercises

1. Do you need to know the height of a video screen to calculate the position of a pixel?

2. Write a horizontal line routine that does not use memset().

3. Write a routine to draw pixels from each corner of the screen to the opposite corner, using the draw_pixel() function.

4. What does this code do: fmemset(off_screen, 0, screensize);

5. Write a function to draw un-filled rectangles using the draw_pixel() function.

6. Write a function to draw un-filled rectangles using the vert_line() and horz_line() functions.

7. Write a small program to compare the speed of the functions in Q5 and Q6. Use clock() to time them, and run both at least 5000 times. Do not wait for vertical blank when timing. Which is faster? Why?

8. Write a function that draws lines from the centre of the screen to the outside edge in a complete circle. Use the line() function.

9. Write a function that fills the screen with a checkerboard pattern.

10. Write a function that draws lines at 45 degrees, using this prototype:
void line_45(int x0, int y0, int length, int left_or_right, int colour);
Use a pointer. Only calculate the offset once at the start of the function.

Click here for chap2.zip (11 KB)

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

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