Start at the Beginning
Table of Contents
The other advantage of double buffering is that it prevents what's called 'tearing'. Back in the day, everyone used CRT (cathode ray tube) monitors instead of the LCDs we use today. Inside these monitors was an electron gun that would light up pixels by running an electron beam down the screen extremely quickly. Once it reached the bottom, it'd move back up to the top before drawing the next frame. The time the electron gun takes to move back up to the top is called the vertical blank period or vblank.
If we start drawing a new frame to the screen while the monitor is in the middle of drawing, half the screen will be the previous frame, and the other half will be the next frame. If things are moving quickly, this creates a weird visual effect called "tearing". If we wait until the monitor is in vsync before drawing our frame, we can avoid this entirely - as long as the drawing takes less time than the vblank. Without double buffering, it might not if we're drawing a particularly complex scene and waiting until vsync before we start. If, instead, we draw away on our back buffer, the quick copy operation is all we have to do in vblank, and that's almost guaranteed to finish in time.
Even though LCD monitors don't quite work this way, this approach is still required. LCD pixels never have to be refreshed, but the monitor queries the VGA for the current contents of the graphics array every so often and only redraws the actual physical screen then. If we're in the middle of drawing when that happens, we'll get tearing.
On a VGA card, vblank occurs 70 times per second. Thus, waiting for vblank also gives us a way to prevent our game from running too fast - but it does limit us to 70fps. This makes our lives easier in a lot of ways, but it is limited. Eventually we'll learn ways around this.
Here's the function we'll use to wait for vblank:
(click here for vblank.c)
Game Programming From The Ground Up
Wednesday, September 8, 2010
Wednesday, August 25, 2010
Double Buffering
Start at the Beginning
Table of Contents
My preferred approach for drawing pixels (and thus images) to the screen is something called double buffering. I prefer it because it's simple, it's fast, and because we'd need to implement it eventually anyways (I'll explain why in the next section).
With double buffering, we draw our screen image into a block of memory we handle ourselves, and then we blast it into video memory as quickly as possible. We only need to deal with the vestigal 16-bit memory layout once, when we do that fast copy - but happily, there's a built in function that manages it all for us.
For the record - while I'm writing this book from first principles to try to engender a deep understanding of the concepts, and normally I'm opposed to this sort of "just let the computer do it" handwaving - here I'm interested in teaching game programming concepts, not completely worthless, x86-specific processor tinkering concepts. No one should have to know this anymore unless they're writing an OS. No one should have ever had to know this!
To implement double buffering, first we'll need to create our "back buffer". The back buffer is a chunk of memory exactly like the one in video memory where the VGA stores the screen, except that it's in our address space - meaning it's memory we can access whenever and however we want.
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
/* here, we allocate a global array to store our screen buffer. An array is basically a chunk of memory that stores a series of items of the same type. Here, it stores a series of bytes representing the pixels that form the image on-screen. There's a slot for each pixel on the screen, so to compute the total number, we compute SCREEN_WIDTH*SCREEN_HEIGHT */
unsigned char screen_buffer[SCREEN_WIDTH*SCREEN_HEIGHT];
Then we need a way to copy this back buffer into video memory. To do this we'll use a function DJGPP (our compiler) provides called dosmemput(). dosmemput() is non-standard (meaning it'll only work with DJGPP on DOS, not with other compilers or operating systems) function that can copy a large chunk of data to anywhere in memory quickly - even outside of our program's address space, into places like video memory. The call we'll use to actually copy our back buffer will look like this:
That means we're copying SCREEN_WIDTH*SCREEN_HEIGHT bytes from screen_buffer to 0xA0000 - where the VGA stores the screen. Now we can write to our back buffer as much as we want, quickly and easily. When we're done, we copy the back buffer over to put it on screen:
Previous
Table of Contents
My preferred approach for drawing pixels (and thus images) to the screen is something called double buffering. I prefer it because it's simple, it's fast, and because we'd need to implement it eventually anyways (I'll explain why in the next section).
With double buffering, we draw our screen image into a block of memory we handle ourselves, and then we blast it into video memory as quickly as possible. We only need to deal with the vestigal 16-bit memory layout once, when we do that fast copy - but happily, there's a built in function that manages it all for us.
For the record - while I'm writing this book from first principles to try to engender a deep understanding of the concepts, and normally I'm opposed to this sort of "just let the computer do it" handwaving - here I'm interested in teaching game programming concepts, not completely worthless, x86-specific processor tinkering concepts. No one should have to know this anymore unless they're writing an OS. No one should have ever had to know this!
To implement double buffering, first we'll need to create our "back buffer". The back buffer is a chunk of memory exactly like the one in video memory where the VGA stores the screen, except that it's in our address space - meaning it's memory we can access whenever and however we want.
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
/* here, we allocate a global array to store our screen buffer. An array is basically a chunk of memory that stores a series of items of the same type. Here, it stores a series of bytes representing the pixels that form the image on-screen. There's a slot for each pixel on the screen, so to compute the total number, we compute SCREEN_WIDTH*SCREEN_HEIGHT */
unsigned char screen_buffer[SCREEN_WIDTH*SCREEN_HEIGHT];
Then we need a way to copy this back buffer into video memory. To do this we'll use a function DJGPP (our compiler) provides called dosmemput(). dosmemput() is non-standard (meaning it'll only work with DJGPP on DOS, not with other compilers or operating systems) function that can copy a large chunk of data to anywhere in memory quickly - even outside of our program's address space, into places like video memory. The call we'll use to actually copy our back buffer will look like this:
dosmemput(screen_buffer, SCREEN_WIDTH*SCREEN_HEIGHT, 0xA0000);
That means we're copying SCREEN_WIDTH*SCREEN_HEIGHT bytes from screen_buffer to 0xA0000 - where the VGA stores the screen. Now we can write to our back buffer as much as we want, quickly and easily. When we're done, we copy the back buffer over to put it on screen:
/* imagine we have some constant RED defined for a red pixel.
This line would make the very top left pixel red */
back_buffer[0] = RED;
/* This line would make the pixel to the right of it red */
back_buffer[1] = RED;
/* And this would make the pixel below it red.
Each row has 320 pixels, and the rows are stored top to
bottom, one after the other */
back_buffer[320] = RED;
/* now copy the back buffer to the screen */
dosmemput(screen_buffer, SCREEN_WIDTH*SCREEN_HEIGHT, 0xA0000);
This line would make the very top left pixel red */
back_buffer[0] = RED;
/* This line would make the pixel to the right of it red */
back_buffer[1] = RED;
/* And this would make the pixel below it red.
Each row has 320 pixels, and the rows are stored top to
bottom, one after the other */
back_buffer[320] = RED;
/* now copy the back buffer to the screen */
dosmemput(screen_buffer, SCREEN_WIDTH*SCREEN_HEIGHT, 0xA0000);
Previous
Monday, August 23, 2010
Plotting Pixels (In Theory)
Start at the Beginning
Table of Contents
The VGA stores the contents of the screen in memory in segment 0xA0000. In theory this means we could just write directly into 0xA0000 to draw on the screen as easily as by assigning variables.
...What exactly segment 0xA0000 is, unfortunately, can only be answered by explaining the x86 architecture's archaic and obsolete 16-bit memory layout. By default, our compiler inserts code into our program that, at start up, lets us use a much more modern, 32-bit memory layout introduced with the 386 processor. This has many perks, but unfortunately it also makes difficult to write directly into video memory the way we could in 16-bit mode.
There are a few solutions. The first is to take advantage of a BIOS function that plots a pixel, much in the same way that we switched video modes before. Here's the code:
(click here for slowpix.c)
The only problem with this is that it's slow. Plotting pixels is something we'll be doing a lot of, so our pixel plotting routine needs to be as fast as possible. This solution just won't fly.
Another approach is to use a number of tricks our compiler provides to write directly into video memory, as hard as that is in the 32-bit world. This introduces a lot of complexity I'd like to avoid - because it's hard to understand, and because I don't want to have to explain all of it!
Instead, we'll be using a technique called double buffering.
Table of Contents
The VGA stores the contents of the screen in memory in segment 0xA0000. In theory this means we could just write directly into 0xA0000 to draw on the screen as easily as by assigning variables.
...What exactly segment 0xA0000 is, unfortunately, can only be answered by explaining the x86 architecture's archaic and obsolete 16-bit memory layout. By default, our compiler inserts code into our program that, at start up, lets us use a much more modern, 32-bit memory layout introduced with the 386 processor. This has many perks, but unfortunately it also makes difficult to write directly into video memory the way we could in 16-bit mode.
There are a few solutions. The first is to take advantage of a BIOS function that plots a pixel, much in the same way that we switched video modes before. Here's the code:
(click here for slowpix.c)
The only problem with this is that it's slow. Plotting pixels is something we'll be doing a lot of, so our pixel plotting routine needs to be as fast as possible. This solution just won't fly.
Another approach is to use a number of tricks our compiler provides to write directly into video memory, as hard as that is in the 32-bit world. This introduces a lot of complexity I'd like to avoid - because it's hard to understand, and because I don't want to have to explain all of it!
Instead, we'll be using a technique called double buffering.
Wednesday, August 18, 2010
Tuesday, August 17, 2010
Switching To Mode 13h (In Practice)
Start at the Beginning
Table of Contents
The following is a complete C program that switches into mode 13h then back into text mode. Anything between /* and */ is a comment - the compiler ignores it. We use it to describe exactly what the program is doing when the code itself is unclear. Read the following program closely, including all the comments.
(click here for setmode.c)
Type the above program out in RHIDE and save it to a file called setmode.c. You don't need to copy the comments - in fact, a better thing to do is replace my comments with your own, explaining your own understanding of the code. This will force you to think hard about it.
Now we're going to 'compile' our code. Compilation is the process of taking source code, analyzing it, and turning it into machine code the computer can load and execute. As our program gets more complex, we'll need to have a more involved compile step. For now, it should be fairly simple.
gcc setmode.c -o setmode.exe
gcc is the name of DJGPP's compiler. setmode.c is the name of the file you want to compile, of course. The -o stands for output - it tells GCC where to output the result of compilation.
If it worked, GCC won't display anything. If it outputs an error, check your code, find the mistake, and try again. GCC's error messages tend to be cryptic, but they do tend to lead you in the right direction. To see what I mean, let's fake an error message. Go to line 55:
set_video_mode(TEXT_MODE);
and change it to read
set_video_mode(TXT_MODE);
That is, remove the E in TEXT.
You'll get the following output:
setmode.c: In function 'main':
setmode.c:55: error: 'TXT_MODE' undeclared (first use in this function)
setmode.c:55: error: (Each undeclared identifier is reported only once
setmode.c:55: error: for each function it appears in.)
Now, the first thing to understand is that this is all one error message. The first line tells you the following errors are all inside main(). The next three lines are just GCC splitting the error message up. So the error really is:
setmode.c:55: error: 'TXT_MODE' undeclared (first use in this function) (Each undeclared identifier is reported only once for each function it appears in.)
So this should be clear. The error is in video.c on line 55. The problem is GCC doesn't know what TXT_MODE is. It's undeclared, because we didn't declare TXT_MODE - we declared TEXT_MODE! You need to be very precise with computers. Change TXT_MODE back to TEXT_MODE and it should compile without a hitch - as long as you don't have any errors of your own to fix.
Running the program - by typing setmode at the DOS prompt - should do pretty much nothing (it'll clear the DOS back buffer, actually). That's because all our program does is change video modes and instantaneously switch back. It doesn't wait around in the new video mode or draw anything to the screen in the new video mode. We'll get there soon.
Table of Contents
The following is a complete C program that switches into mode 13h then back into text mode. Anything between /* and */ is a comment - the compiler ignores it. We use it to describe exactly what the program is doing when the code itself is unclear. Read the following program closely, including all the comments.
(click here for setmode.c)
Type the above program out in RHIDE and save it to a file called setmode.c. You don't need to copy the comments - in fact, a better thing to do is replace my comments with your own, explaining your own understanding of the code. This will force you to think hard about it.
Now we're going to 'compile' our code. Compilation is the process of taking source code, analyzing it, and turning it into machine code the computer can load and execute. As our program gets more complex, we'll need to have a more involved compile step. For now, it should be fairly simple.
gcc setmode.c -o setmode.exe
gcc is the name of DJGPP's compiler. setmode.c is the name of the file you want to compile, of course. The -o stands for output - it tells GCC where to output the result of compilation.
If it worked, GCC won't display anything. If it outputs an error, check your code, find the mistake, and try again. GCC's error messages tend to be cryptic, but they do tend to lead you in the right direction. To see what I mean, let's fake an error message. Go to line 55:
set_video_mode(TEXT_MODE);
and change it to read
set_video_mode(TXT_MODE);
That is, remove the E in TEXT.
You'll get the following output:
setmode.c: In function 'main':
setmode.c:55: error: 'TXT_MODE' undeclared (first use in this function)
setmode.c:55: error: (Each undeclared identifier is reported only once
setmode.c:55: error: for each function it appears in.)
Now, the first thing to understand is that this is all one error message. The first line tells you the following errors are all inside main(). The next three lines are just GCC splitting the error message up. So the error really is:
setmode.c:55: error: 'TXT_MODE' undeclared (first use in this function) (Each undeclared identifier is reported only once for each function it appears in.)
So this should be clear. The error is in video.c on line 55. The problem is GCC doesn't know what TXT_MODE is. It's undeclared, because we didn't declare TXT_MODE - we declared TEXT_MODE! You need to be very precise with computers. Change TXT_MODE back to TEXT_MODE and it should compile without a hitch - as long as you don't have any errors of your own to fix.
Running the program - by typing setmode at the DOS prompt - should do pretty much nothing (it'll clear the DOS back buffer, actually). That's because all our program does is change video modes and instantaneously switch back. It doesn't wait around in the new video mode or draw anything to the screen in the new video mode. We'll get there soon.
Monday, August 16, 2010
Switching To Mode 13h (In Theory)
Start at the Beginning
Table of Contents
To switch into Mode 13h, we load register AH with 0 to specify a video mode switch; load register AL with 0x13, the video mode we want to switch into; and then call interrupt 0x10, which handles graphics functions. Simple, right?
Not really. To understand what all this means, you need to understand a bit about CPUs. Basically, the CPU can only operate on data that it has loaded into tiny, special pieces of onboard (i.e. on the CPU) memory called registers. To execute a simple line of code like
y = y + 5;
the CPU will first load the contents of y from wherever it's stored in memory into a register, add 5 to it, and then copy it back to the memory location that corresponds to y.
Two of the registers on x86 CPUs (the type of CPUs in most desktops these days, and the type of CPU we're programming for) are called AH and AL. They each hold a single byte. They're actually each one half of a larger register called AX. There's also BX, CX, DX (and their respective half-registers), SP, SI, DI, BP, CS, DS SS, ES...and so on. The world of low-level coding on the x86 architecture is deeply messy and unpleasant. Be happy your compiler takes care of shuffling data from memory to the registers and back again.
When you send an 'interrupt' to the CPU, the CPU stops whatever it's doing and executes whatever code is assigned to that interrupt. Most code these days doesn't ever need to deal with this, but it's a useful way to give programs access to system functions. The BIOS is a set of functions like this, stored in ROM on the motherboard, that are accessed via interrupt. Interrupt 0x10 is reserved for graphics functions - the precise function selected is determined by whatever number is in AH. 0 is the "switch video mode" function. It switches the computer to whatever video mode corresponds to the number in AL. Since we want to switch to mode 13h, we store 0x13 in AL.
Table of Contents
To switch into Mode 13h, we load register AH with 0 to specify a video mode switch; load register AL with 0x13, the video mode we want to switch into; and then call interrupt 0x10, which handles graphics functions. Simple, right?
Not really. To understand what all this means, you need to understand a bit about CPUs. Basically, the CPU can only operate on data that it has loaded into tiny, special pieces of onboard (i.e. on the CPU) memory called registers. To execute a simple line of code like
y = y + 5;
the CPU will first load the contents of y from wherever it's stored in memory into a register, add 5 to it, and then copy it back to the memory location that corresponds to y.
Two of the registers on x86 CPUs (the type of CPUs in most desktops these days, and the type of CPU we're programming for) are called AH and AL. They each hold a single byte. They're actually each one half of a larger register called AX. There's also BX, CX, DX (and their respective half-registers), SP, SI, DI, BP, CS, DS SS, ES...and so on. The world of low-level coding on the x86 architecture is deeply messy and unpleasant. Be happy your compiler takes care of shuffling data from memory to the registers and back again.
When you send an 'interrupt' to the CPU, the CPU stops whatever it's doing and executes whatever code is assigned to that interrupt. Most code these days doesn't ever need to deal with this, but it's a useful way to give programs access to system functions. The BIOS is a set of functions like this, stored in ROM on the motherboard, that are accessed via interrupt. Interrupt 0x10 is reserved for graphics functions - the precise function selected is determined by whatever number is in AH. 0 is the "switch video mode" function. It switches the computer to whatever video mode corresponds to the number in AL. Since we want to switch to mode 13h, we store 0x13 in AL.
Thursday, August 12, 2010
Mode 13h
Start at the Beginning
Table of Contents
Mode 13h is the name of the 'graphics mode' most classic DOS games ran in. Telling the computer to go into Mode 13h caused it to set the screen resolution to 320x200, and to use the screen format described above - 8 bits per pixel using a palette, 18 bits per palette entry.
Old computers used a standard graphics system called the VGA - short for Video Graphics Array. It had a number of graphics modes, each with their own dedicated number. Standard DOS text mode is mode 3. Mode 13h is the mode we'll be using in this volume, for the sake of simplicity and nostalgia. The 'h' after 13 means that 13 should be read as a hexadecimal number. In decimal, that's:
1x16^1 + 3x16^0
= 16 + 3
= 19
An alternate representation of hexadecimal numbers, the one used by the C programming language, is prefixing them with 0x - e.g. "we're going to tell the computer to switch into Mode 0x13".
Table of Contents
Mode 13h is the name of the 'graphics mode' most classic DOS games ran in. Telling the computer to go into Mode 13h caused it to set the screen resolution to 320x200, and to use the screen format described above - 8 bits per pixel using a palette, 18 bits per palette entry.
Old computers used a standard graphics system called the VGA - short for Video Graphics Array. It had a number of graphics modes, each with their own dedicated number. Standard DOS text mode is mode 3. Mode 13h is the mode we'll be using in this volume, for the sake of simplicity and nostalgia. The 'h' after 13 means that 13 should be read as a hexadecimal number. In decimal, that's:
1x16^1 + 3x16^0
= 16 + 3
= 19
An alternate representation of hexadecimal numbers, the one used by the C programming language, is prefixing them with 0x - e.g. "we're going to tell the computer to switch into Mode 0x13".
Subscribe to:
Posts (Atom)