//****************************************************************************// //****************** Gameboy Display - October 26th, 2017 *******************// //**************************************************************************// - Now, in any graphics computer system, there's a set of rules that tells the hardware how to interpret the data it puts on the screen - In the LC3, there were 2 registers for display: the data register, and the status register - In an actual display device, there can be THOUSANDS of registers and multiple modes of displaying things - Let's say you're just trying to light a pixel, and the computer manual tells you MODE 3 is the most straightforward mode - You should go to register x4000000 REG_DISPCTL, and set this: [00000 | 1 | 000000| 0 | 1 | 1] - In the gameboy, there are multiple "display layers" to allow for parallax backgrounds; we'll stick exclusively to background #2 to keep things simple - Starting in memory location 0x6000000, there's a chunk of 38,400 chunks of memory, each one a "short" integer, for each pixel on the GameBoy screen (160x240) -Each "short" is 16 bits and holds the color for a pixel, and is formatted w/ 5 bits per color: [0 | 5blue | 5green | 5red] - "When your kindergarten teacher told you the primary colors were red, blue, and yellow, they didn't so much lie to you as confuse you" - red + blue is magenta - green + blue is cyan - red + green is yellow - So, the top-left pixel is at address x6000000; if we set this pixel to [0111111111111111], it'll be pure white! - To do this in the GameBoy, we'd write something like this: int main() { 0x4000000 //now, C will look at this, say NUMBER, and convert this to an int //BUT, we want C to treat this like an address...so let's cast it (unsigned short *)0x4000000 //Converts this to a pointer to a short //NOTE: even though a short is 16 bits, the pointer itself is 32 bits *(usigned short *)0x4000000 = 1027; //Now, dereferences it, so it assigns the memory location at 0x4000000 the value 1027; this "1027" value is the code to tell the GameBoy to run in 'Mode 3' *(usigned short *)0x6000000 = 31; //Gives the top-leftmost pixel a red color // "A piece of advice in this class: KNOW THE DIFFERENCE between a 'pointer declaration' asterisk and a 'dereferencing' asterisk!" while(1); //the GameBoy does NOT have an Operating System, so it literally NEVER stops running until we turn it off; this way, the emulator doesn't immediately shut off } - *a single red pixel lights on the screen, a small beacon of hope in the darkness*...yeah, no one's impressed - Now, if you look at this code and say, "Cool! I want to write that for the rest of my life!"...you're nuts - Let's try and clean this up a bit: #define REG_DISPCTL *(unsigned short *)x4000000 #define MODE3 3 #define BG2_ENABLE (1<<10) int main() { REG_DISPCTL = MODE3 | BG2_ENABLE; //ADDS the mode3 and Bg2 code together, so this actually says "enable mode3 AND BG2" *(unsigned short *)0x6000000 = 32767; //sets the pixel to be white, cause variety while(1); } - Now, to position the pixel on the screen, remember that each pixel takes up TWO bytes, because it's a SHORT (short = 16 bytes, 1 memory address = 8 bytes) - "So, to shift our dot to the right 1 pixel, we have to add TWO to the memory address" - This is a SUPER common error in assembly programming, where people constantly forgot to allocate extra space for certain datatypes, etc. - Now, in C, they said "Hey, we should help out the programmer; when they specify a memory address, we'll automatically offset it by the right amount for the datatype" - This sounds tricky, but it makes sense: in a movie theater, each of the seats is 36 inches wide; when I tell you to move "3 seats down", you automatically move 108 inches down - the same idea works for integers, doubles, etc. in C! If you tell C to move 3 addresses down for an integer, it'll move 3 * 4 = 12 addresses to the beginning of the 3rd int! - This is called POINTER ARITHMETIC in C - So, to have a pixel show up in the middle of the screen, we could do either of these things: * (unsigned short *) (x6000000 + 2*(19200 + 120)) = 32767; //regular, untyped address-moving, doing it ourselves * ( ((unsigned short *)(0x6000000)) + 19200 + 120) = 32767; //casts it to a short pointer FIRST, then adds our address offset; since it's a short, C will automatically multiply the offset by 2 for us! - Now, in the GameBoy, the videoBuffer memory is NOT always exactly at x6000000; it changes around as the program is running, so we shouldn't hardcode that value into our program; we should make it a variable: unsigned short *videoBuffer = (unsigned short *)0x6000000; - ...which let's us rewrite our main function as: int main() { REG_DISPCTL = MODE3 | BG2_ENABLE; *( videoBuffer + 19320 ) = 32767; //does the pointer arithmetic while(1); } - Now, when Kernighan and Richie where making this stuff in New Jersey, they'd be taking breaks at the beach, relaxing, sipping drinks on the boardwalk...and one day, one of them (probably Richie, the darn guy) said: "I just don't like this. Sure, it's logical and I get it, but it just doesn't seem nice! So here's what we'll do:" - We'll replace the pointer offset stuff with THIS: videoBuffer[19320] = 32767; //access the value at the pointer w/ an offset of 19320; dereferences it for us - "...isn't that cool? No? Well, Richie thought it was cool, so BOOM, now it's in C." - Now, if THAT works, then what about something like this? 19320[videoBuffer] = 32767; - "This is ridiculous, right? It won't even COMPILE, right?" -...WRONG! This works JUST FINE! -"...but PLEASE, NEVER hand in code like this on your homework. This is a pretty abstruse example of syntactic sugar" - "Now, what we've been doing with this bracket-offset syntax looks kind of like an array, right? Is this an array?...NO!" - To be clear: NO, THIS IS NOT AN ARRAY! This is JUST a convenient shortcut that C gives us to deal with pointers. "Forget arrays like you have in Java...C has em', but this is NOT an example of an array" - Now, figuring out 16-bit color as a single number is pretty annoying, so instead, we could have three separate variables - "red, green, blue" - and do something like this: red | green<<5 | blue <<10 - Now, let's turn that into a macro: #define COLOR(r, g, b) ((r) | (g)<<5 | (b)<<10) - So, we can now just define colors w/ RGB values from 0 to 31 - Another convenience thing: We start at the top-left pixel - to go RIGHT one pixel, we add 1 - To go DOWN one pixel, we add 240, since each row is 240 pixels wide (from 0 to 239) - So, to move to a specific coordinate at the screen, starting at (0, 0), we'd have something like: #define ROWLENGTH 240 #define OFFSET(row, col) ((row)*(ROWLENGTH) + (col)) - Now, with all that setup, we're FINALLY ready to start on our long, hard, epic quest of GameBoy-program making - "But before we do that, I have one question for all of you people: why didn't we just use the setPixel function?" -...*quiet* - "...because there IS no setPixel function! This is C, folks! We actually HAD to handle all this stuff by ourselves - and we did!" - Let's write that setPixel function, for good measure: void setPixel(int row, int col, unassigned short color) { videoBuffer[OFFSET(row, col)] = color; } - Don't forget to declare a function prototype at the top of the file! Otherwise, C will start compiling, see that the function isn't declared, say "well, I assume by default all functions return an int, so let's do that", see our "setPixel" function returns void, say "Nah, there's ALREADY a setPixel function that returns an int instead, silly!", and error - Let's make another function that draws a rectangle: void drawRect(int row, int col, int height, int width, unsigned short color){ for (int r = 0; r < height; r++) { for (int c = 0; c < width; c++) { setPixel(row + r, col + c, color); } } } - A final word: the GameBoy runs at 16Mhz, so real-world animations need to use a LOT of performance-saving hardware tricks to do anything cool and still run acceptably. I'm not expecting you to do anything like that for your game; I'm just expecting it to work (and preferably, well, be fun!)