Episode 2
Summary: Diving into the basics of the gp2x hardware and some further adventures with system calls makes "demo.gpe" into a real graphics application (although not a very impressive one). Total demo.gpe size at end of episode 2: 1636 bytes.
Last time, I got a simple program running that printed a debugging line to the serial port and restarted the gp2x menu before continuing. Time to build on that base to make the shell of a real demo. What needs doing?
* Drawing to the screen
* Flipping between alternate screens on a vertical sync (double buffering)
* Watching for a button press to exit the demo
First up, the gp2x screen. A normal application or demo uses a library such as SDL to make accessing the gp2x screen very easy with nice abstract APIs. I'm not using any of that for this demo, so I have to go closer to the hardware. The LCD screen is managed by the MagicEyes MMSP2 chipset. This hardware gets criticized a lot but it's actually a very nifty device with a ton of features. Getting access to these features involves setting register values. These registers exist at certain memory addresses, which is a very convenient way of accessing them. To set their values, just store the value in the appropriate memory location.
The difficulty is that from my linux process, I don't have a way of specifying a particular physical memory address -- all of my memory references are done in the context of my process and I'm not allowed to reach beyond the bounds that the operating system has placed around my process. This is a good thing, because that way I cannot accidentally write data all over the operating system or any other programs that happen to be running. Unfortunately, it also means that I cannot access the hardware registers.
Luckily there are system calls that allow me to do this. First, I have to open a special linux "device" file called "/dev/mem". This nifty thing gives me access to the gp2x memory as if it was a file! Now I'll add the "open" and "close" system calls to my growing list of system calls and add a little bit of structure to the project by breaking out the system calls into their own source file, and beginning an "Initialize" and "Cleanup" function to take care of startup and shutdown stuff. Here's the new system call:
Code:
int OpenFile(char *pszFile, int nMode)
{
int nFile;
asm volatile
(
"mov r0, %1\n" // file to open
"mov r1, %2\n" // mode
"swi #0x900005\n" // open
"mov %0, r0\n" // collect the file descriptor for return
: "=r"(nFile) // %0 = output value from the function
: "r"(pszFile), "r"(nMode) // inputs: %1, %2
: "r0", "r1" // registers we clobber
);
return nFile;
}
This is a good time to explain the lines that start with colons at the end of the inline assembly section. The first line gives the "output" parameters, which means the values outside of the assembly section that will be receiving values during the assembly code. We need one value like that here because I want to return the value from the 'open' system call, which will be stored in register 0 when the system call returns. The next line is the 'input' parameters which are values that give inputs to the assembly code from the C code. The items from these two lines are referred to inside the assembly code as %0, %1, %2, etc, in exactly the order they are described. The final colon line specifies which registers get clobbered; this tells the compiler to save them and restore them. In this case I believe I don't actually have to specify these since the function is allowed to clobber r0, r1, r2, and r3, and the function doesn't do anything except call the inline assembly, but I figure better safe than sorry.
Ok, once I have opened /dev/mem, I need to get more convenient access to the memory addresses for the MMSP2 registers. We do this with a system call called 'mmap' which asks for an address corresponding to a particular physical address range. In this case I want the physical addresses that start at address 0xC0000000 (the base of the registers).
So all I need to do is make another interface to a system call, just like the others. Except mmap is different -- since it has six parameters, and the "standard" method of making system calls can only handle four parameters, I have to call it differently. After more hair-pulling and crying over failed attempts, this is my result:
Code:
void *MMap(void *pAddr, int nLen, int nProtection, int nFlags, int nFD, int nOff)
{
void *pvRet;
asm volatile
(
"mov r1, %1\n"
"mov r2, %2\n"
"mov r3, %3\n"
"mov r4, %4\n"
"mov r5, %5\n"
"mov r6, %6\n"
"stmdb sp!, {r1, r2, r3, r4, r5, r6}\n"
"mov r0, sp\n"
"swi #0x90005A\n" // mmap
"add sp, sp, #24\n"
"mov %0, r0\n"
: "=r"(pvRet)
: "r"(pAddr), "r"(nLen), "r"(nProtection), "r"(nFlags), "r"(nFD), "r"(nOff)
: "r0", "r1", "r2", "r3", "r4", "r5", "r6"
);
return pvRet;
}
What I do here is push all of the parameters on the stack and pass a pointer to them into the system call in register 0. Then clean the stack up again afterward. There is probably a prettier way to do this, but it works so I say "good enough"!
Refer to the complete source code if there are other questions about getting access to these registers.
There are a WHOLE BUNCH of registers! For now, I'm just interested in the bare set needed to get the screen doing what I want. Refer to the cryptic MMSP2 document for more information on the registers beyond what I describe here. Enterprising demo writers have a lot of interesting avenues to look into as far as using the MMSP2 video hardware goes. There are different "regions" with different positions, alpha blending between them, and all kinds of funky poorly-described stuff just waiting to be discovered.
I'll just use a couple registers right now though -- the one at address 0x28DA specifies the bits per pixel and which of the "regions" is activated. The magic value for the "usual" 16bpp and only "region 1" active is 0x04AB. The other register needed here is at 0x290C which contains the width in bytes of a screen line. If I'm going to be playing with screen depth I'll need to set that, so I might as well initialize it now (to 640, because 320 pixels times two bytes per pixel is 640).
The next issue is: where is the screen memory? The "normal" way to find this under linux seems to be to open the two frame buffer devices which act like the "/dev/mem" device I used before. Then you use mmap to get access to that memory. If you look at rlyeh's minlib or the SDL implementation you'll see this sort of thing being done. The files are /dev/fb0 and /dev/fb1. It's a reasonably portable way to do screen stuff under linux and it's a good idea.
But I want something more flexible, so I won't use that. Instead, I'm going to allocate a whole bunch of scratch memory in the "upper" memory area which I may use for various things in the demo. I'll stick my frame buffers in that memory.
So I make the following call:
Code:
g_pNoncachedMemory = mmap(0, 0xF00000, PROT_READ|PROT_WRITE, MAP_SHARED, g_nMemoryFD, 0x3000000);
This maps 15 megabytes of memory starting at physical address 0x3000000 (48 mb). I then reserve the second megabyte of that for frame buffers. This is a demo, I can torture the hardware any way I want!
Ok, that's it for initialization.
The main demo loop looks like this:
Code:
while(!IsStartButtonPushed())
{
DrawScreen();
WaitForVSync();
FlipScreen();
}
Each of those needs a bit of description. Trial and error and a look at other people's code and the MMSP2 document tell me that bit 8 of register 0x1184 contains the state of the "start" button, so looking at it is quite easy. When the bit is cleared, that means that the button is pushed, and I should exit the demo.
DrawScreen() is where the magic happens. Whichever of the two frame buffers is not currently being displayed gets the 1337 FX drawn into it. For now I just wrote a simple slow rectangle function to make sure things are working properly, and I draw a couple rectangles.
WaitForVSync() is a function that waits for the "vsync" to start. The LCD display is apparently redrawn much like a television set, one pixel at a time. There is a brief period in between redraws and I need to wait for that time to occur before I replace the old frame with the new frame -- otherwise it will get replaced in the middle and the screen will look jagged.
A particular bit in register 0x1182 shows whether the VSync is currently set, so it's easy to wait for it.
Finally, FlipScreen. The MMSP2 maintains a pointer to the memory representing the screen. To give it the new screen I just drew, I just have to set the pointer to the proper thing.
Whew. That's it. The code and current version of "demo.gpe" can be fetched from
http://members.gamedev.net/dzz/demo2.zip for those who are curious to follow along at home. There's a lot of stuff to digest in this episode, and the result is an actual demo... but kind of a stupid one. The total size of the program is now 1636 bytes. Still not too big.
Next time, I'll start trying to do something a little bit more fun now that the preliminaries are out of the way. It'll take me a while to work on that though, so don't expect another episode before the end of the upcoming weekend at the earliest. This is fun stuff!