Episode 4 - in which absolutely nothing impressive is accomplished!
Last time I experimented with the gp2x hardware a bit to play with 8 bit video mode and scrolling. That's nice for some special effect but now it's time to lay the foundation for some real work.
The first issue to think about is memory speed. On the gp2x, the lower 32MB of memory are managed by linux. That memory is marked as "cached", which means that reads and writes go to the 16kb cache on the arm 920 chip, which is usually a lot faster. The upper 32mb of memory is uncached. That uncached memory is the only memory I can get physical addresses for, so I need to have my display buffer in that memory. Linux uses some particular address ranges for the frame buffers in that upper memory range; in episode 1 I allocated some different "upper" memory for this purpose. This is a problem though. If I'm going to be messing around with the screen memory it will be pretty slow to read it from the uncached memory.
So I'd like a temporary memory buffer in cached memory that I can just zap to the screen memory at the end of each frame. There's certainly some overhead involved in that, but it's probably worth it.
To do this, i just pass in some memory to a new little function, SetMemScreen(), then add another function MemScreenToScreen() that is responsible for copying the data. Finally, another really simple function to clear the "MemScreen". Pretty simple. Here's the code to one function just to make sure you get the idea:
Code:
void MemScreenToScreen()
{
unsigned short *pusSrc, *pusDest;
int n;
pusSrc = GetMemScreen();
pusDest = GetScreen();
for(n = 0; n < 320*240; n++)
*pusDest++ = *pusSrc++;
}
That function is not the most efficient code; I'll discuss that later. Here's another not very efficient function. It clears a screen-sized memory area:
Code:
void ClearScreen(unsigned short *pus)
{
int n = 320 * 240;
while(n)
{
*pus++ = 0;
n--;
}
}
Now using all this I can write a new main loop to process a frame:
Code:
while(!IsStartButtonPushed())
{
pus = GetMemScreen();
ClearScreen(pus);
DrawScreen(pus);
MemScreenToScreen();
WaitForVSync();
FlipScreen();
}
where "DrawScreen" writes magical graphics wizardry to the 16-bit screen buffer.
Next topic! Sometimes if I make a mistake it's hard to track down the reason. So it's critical to have some sort of method for getting debugging prints. In episode 1 I demonstrated a quick way to print data out the serial port, but not everybody has a serial cable, so I thought it would be a good idea to write a little file that has some debugging functions in it. In the code for demo4, look for the files "debug.c" and "debug.h". Those files contain a couple of interesting little functions:
Code:
// given a pointer to the screen and an x,y location draw the string pStr
// this bypasses the "DebugLines" mechanism defined below. It would be
// good for putputting a "frames per second" counter every frame for
// example
int DrawDebugString(unsigned short *pus, int nXPos, int nYPos, char *pStr);
// given a pointer to the screen and an x,y location draw a message
// and an integer to the screen
int DrawDebugNumber(unsigned short *pus, int nXPos, int nYPos, char *pszMessage, int nValue);
// add a debug line to the screen. this maintains a list of 20 lines
// that scrolls so there's lots of room for debug messages
void AddDebugLine(char *pStr);
// add a message+integer to the list of debug lines
void AddDebugNumber(char *pszMessage, int nValue);
// clear all of the debug lines
void ClearDebugLines();
// draw all the debug lines to the screen
void DrawDebugLines(unsigned short *pus);
Ok, now I have a nice little debugging facility! It adds a lot to the size of the program (6k or so), but that's okay because I won't be including it in the final demo, it's just for debugging.
Another thing that will help a lot is a timer to measure an amount of elapsed time, so I can see how much time various things are taking to draw or whatever. So I added some more functions (in clib.c):
Code:
void StartTimer(unsigned long *pul)
{
*pul = g_pulRegs[0x0A00>>2];
}
// returns the time in tenths of a millisecond since StartTimer was called
int GetTimer(unsigned long *pul)
{
unsigned long ul;
ul = g_pulRegs[0x0A00>>2];
if(ul > *pul)
return (int) ((ul - *pul + 368) / 737);
// wrapped around
return (int) ((((ul/4)+(1<<30)) - (*pul/4)) / 184);
}
That's fairly ugly code. Basically, the gp2x has a register that contains a timer which increments somewhat more than 7 million times per second. I'm just remembering its value in StartTimer then fetching it again in GetTimer and converting the result to tenths of milliseconds. If the counter wraps around, that is handled as well.
Here's an interesting thing: notice that there are divisions in this code, and the compiler doesn't complain! Recall from before, since I have abandoned all libraries I can't divide because division is supplied by a library function. However, it turns out that if you divide by a constant, the compiler is able to generate some very efficient code to do that without actually doing a division (basically to divide by 184 the compiler computes a representation for 1/184 and does a multiply instead).
My file clib.c has a simple implementation of division and mod inside of it; if somebody was clever they could use that to implement the functions that the compiler is looking for when it does a divide (__divsi3 for signed integer division) and then use the "/" symbol in code. I prefer to just call a divide function explicitly to remind myself what is actually going on though. I'll replace the divide with a better one in assembly at some point.
Ok, now my main loop looks like this:
Code:
while(!IsStartButtonPushed())
{
StartTimer(&ulTimer);
pus = GetMemScreen();
ClearScreen(pus);
DrawScreen(pus);
DrawDebugNumber(pus, 250, 4, "Time: ", nLoopTime);
DrawDebugLines(pus);
MemScreenToScreen();
nLoopTime = GetTimer(&ulTimer);
WaitForVSync();
FlipScreen();
}
The purpose of this is to measure how long it takes to clear the screen and then copy it again. The result that gets printed is 106 -- meaning 10.6 milliseconds. Ouch! That's a big price to pay before I actually do anything!
So now I'll try to reduce that. Let's start with the function that copies the screen. Here's a fairly optimized version in assembly:
Code:
CopyScreen:
stmfd sp!, {r4-r10} @ remember registers 4-10
mov r2, #4800 @ we will run the loop 4800 times to copy the screen
.CopyScreenLoop:
ldmia r1!, {r3-r10} @ pull in 32 bytes from the source
stmia r0!, {r3-r10} @ write the 32 bytes to the destination
subs r2, r2, #1 @ decrement the loop counter
bne .CopyScreenLoop @ if we're not done, do it again
ldmfd sp!, {r4-r10} @ restore the registers
mov pc, lr @ return
Coming into the function, register 0 contains the destination pointer, and register 1 contains the source pointer. The key is the ldmia and stmia instructions; these load or save a whole gob of memory into a bank of registers in one shot (in this case 8 registers at four bytes each is 32 bytes). So if I run this 4800 times, that's a total of 153600 bytes, which is the size of the screen.
Woo! The timer now says "49" for each loop, just 4.9 milliseconds. Now, how about the function to clear the screen?
Code:
ClearScreen:
stmfd sp!, {r4-r10} @ remember registers 4-10
mov r2, #4800 @ we will run the loop 4800 times to copy the screen
mov r3, #0 @ load up the registers with zeros
mov r4, #0
mov r5, #0
mov r6, #0
mov r7, #0
mov r8, #0
mov r9, #0
mov r10, #0
.ClearScreenLoop:
stmia r0!, {r3-r10} @ write the 32 bytes of zeros to the destination
subs r2, r2, #1 @ decrement the loop counter
bne .ClearScreenLoop @ if we're not done, do it again
ldmfd sp!, {r4-r10} @ restore the registers
mov pc, lr @ return
Basically the same idea. Now I run it and the time is 2.9 milliseconds per loop. Not bad!
Someone out there is thinking: Hey, what about the blitter? Can't the blitter handle these operations? Unfortunately, the answer is no. The blitter needs physical addresses to work on, and my "MemScreen" is is in virtual memory (normal cacheable linux-supplied memory).
Well, there it is. No fancy graphics or music still, but some interesting work toward the demo. The executable has bloated out to 9396 bytes, but most of that is the debugging code (which includes a font) so it's not too bad. Without the debug code included the size is 3384 bytes.
As always, the source and gpe is available from
http://members.gamedev.net/dzz/demo4.zip