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
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
Here is the code to copy the off screen buffer to video memory, making it visible.
First we want to initialize mode13h, and create the off screen buffer.
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.
What that line of code does:
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.
Horizontal lines
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
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.
Filled rectangles
Arbitrary lines
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.
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:
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
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.
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.
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;
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);
}
void draw_pixel(int x, int y, int colour)
{
*(off_screen + y*screen_width + x)=colour;
}
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.
int get_pixel(int x, int y)
{
return *(off_screen + y*screen_with + x);
}
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
}
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
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 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
}
}
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.
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;
}
}
}
}
}
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.