GP2X Gp2x Demo Development


Dzz posted on Apr 14 2006 at 11:47 PM said:
Code:
ul = (unsigned long) (SCREENBASE + g_nScene1ULY*320);
g_pusRegs[0x290E>>1] = (unsigned short) (ul & 0xFFFF);
g_pusRegs[0x2912>>1] = (unsigned short) (ul & 0xFFFF);
g_pusRegs[0x2910>>1] = (unsigned short) (ul >> 16);
g_pusRegs[0x2914>>1] = (unsigned short) (ul >> 16);
As you can see, there are actually two different sets of registers for the visible screen location. They correspond to the odd and even scan lines on the screen. If you want to code a special effect based on an interlace type of effect it could be fun to do it using different values for these registers (I'm thinking a cheap wipe/fade effect between scenes maybe). Otherwise, they should just get the same value as in the code above.
The GP2X's LCD only uses the even scanline registers for both odd and even lines. The odd ones are ignored unless you're using the tv-out (or explictly select interlaced - I'm sure I managed to when I was hacking around.)
Also you should mask the low word with 0xFFFC to remind you that the hardware ignores the lowest two bits - i.e. screens can only start on a 32bit boundary. 16bit depth = granuality of 2 pixels, 8bit depth = granuality of 4 pixels.
24bit depth would end up with a 3 pixel granuality, however getting 24bit working is another matter, it looks like GPH may have only connected 6:6:6 lines to the LCD even though both the MMSP2 and LCD can do 8:8:8.
It is possible to add a delay to the screen to make it appear 0-3 pixels shifted, but you loose the same amount of columns on the right. (register 0x2826 bits 0-1, sets how many pixels to shift (0-3), bit 3 sets direction 0=left, 1=right (or other way around)
 
Last edited by a moderator:
paeryn posted on Apr 15 2006 at 06:36 AM said:
The GP2X's LCD only uses the even scanline registers for both odd and even lines. The odd ones are ignored unless you're using the tv-out (or explictly select interlaced - I'm sure I managed to when I was hacking around.)
Also you should mask the low word with 0xFFFC to remind you that the hardware ignores the lowest two bits - i.e. screens can only start on a 32bit boundary. 16bit depth = granuality of 2 pixels, 8bit depth = granuality of 4 pixels.
24bit depth would end up with a 3 pixel granuality, however getting 24bit working is another matter, it looks like GPH may have only connected 6:6:6 lines to the LCD even though both the MMSP2 and LCD can do 8:8:8.
It is possible to add a delay to the screen to make it appear 0-3 pixels shifted, but you loose the same amount of columns on the right. (register 0x2826 bits 0-1, sets how many pixels to shift (0-3), bit 3 sets direction 0=left, 1=right (or other way around)
Thanks for the very useful info! If you have more insights gleaned from your great work on the hardware SDL, please share them!

I could swear that while I was playing around with the odd/even line registers I got it to move the odd and even lines up and down, but I cannot reproduce that now so I must have dreamed it or something.

So for anybody following this closely, my idea about using those registers for a scanline-based fade effect is bogus!
 
Last edited by a moderator:
Dzz posted on Apr 15 2006 at 02:12 PM said:
I could swear that while I was playing around with the odd/even line registers I got it to move the odd and even lines up and down, but I cannot reproduce that now so I must have dreamed it or something.

So for anybody following this closely, my idea about using those registers for a scanline-based fade effect is bogus!
Just played around with it. If you want to have the LCD interlaced then set bit 5 of 0x2800 (reg[0x2800>>1] |= 0x20)
Clear it to go back to a progressive display.
Note:
The LCD works in a funny way. When using interlaced modes for tv-out you normally have to set the stride to 2x, start the even line to (odd start + width of one line) and halve the screen height. However when interlacing this LCD, don't make those adjustments, the LCD doesn't really interlace. What you end up with is one full (320x240) frame from the even registers, followed by one full frame from the odd registers.
I think (from the talks I had with one of the engineers of the LCD) is that the LCD sort of blends the current frame with the previous.
 
Last edited by a moderator:
Dzz posted on Apr 15 2006 at 12:47 AM said:
The other odd-looking code here is the inline assembly. Remember last time we had those lines beginning with colons? This is just like that. The third colon starts the "clobber" list and so it basically means that we have clobbered memory. If these kind of lines are not inserted here, a "smart" optimizing compiler might think that I'm writing to the same memory address over and over, and try to save execution time by throwing away all the work (since for normal memory all the writes before the last one end up having no effect).

You can achieve the very same result declaring your g_pusRegs array as volatile. This is the intented use of this type qualifier.
 
Last edited by a moderator:
I got the general idea from all this documentation and started working on a demo... but for some odd reason, when the demo exits, USB stops working. Did I forget something?
 
TKF15H posted on Apr 18 2006 at 11:49 PM said:
I got the general idea from all this documentation and started working on a demo... but for some odd reason, when the demo exits, USB stops working. Did I forget something?

A little bit more detail would help. What did you use, what code? I doubt that the function calls we make here interfere with the USB stuff. Are you using the USB-Serial thingy?
 
Last edited by a moderator:
Nope, no USB-Serial thingy. I fiddled around a bit with it and now it crashes after a few frames so I'll have to fix that before I wory about USB (despite the fact that USB still gets disabled).
 
TKF15H posted on Apr 19 2006 at 05:17 PM said:
Nope, no USB-Serial thingy. I fiddled around a bit with it and now it crashes after a few frames so I'll have to fix that before I wory about USB (despite the fact that USB still gets disabled).
Does the executable from the zip file in the article display this behavior? If so, can anybody else reproduce the problem? If not, you could put your code up for download and we can try to spot the issue.
 
Last edited by a moderator:
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
 
Dzz posted on Apr 20 2006 at 12:15 AM said:
Episode 4 - in which absolutely nothing impressive is accomplished!

As always, the source and gpe is available from http://members.gamedev.net/dzz/demo4.zip

I've read your progress with interest and look forward to more.

Call me old fashioned (or at least old) but I'm putting in a request
that you give more thought to function naming convention. One of
the problems with the 'C' language is that it completely ignores
multiple module program structure and leaves such issues up to the
coder. This is no problem for the single coder but team development
or borrowed code bumps into the namespace collision issue.

I want to use your demo code in my application and I need to
rename some of the functions to avoid confusion/collision.

One convention that works well is:

<module name> <underscore> <function verb> <underscore> <qualifiers>

e.g. StartTimer() becomes Timer_Start() written in timer.c / timer.h

e.g. ClearScreen() becomes Screen_Clear() written in screen.c / screen.h

e.g. AddDebugLine() becomes Debug_Add_Line() written in debug.c / debug.h

Now all related functions have the same prefix and I know in which
program file each will be found.

Object oriented languages put the object name in place of the module
name but the pattern can be the same.

If you don't like mixed capitalisation or underscores, I suppose you leave
them out or learn to touch type :)
 
Last edited by a moderator:
crusty posted on Apr 20 2006 at 01:11 PM said:
Dzz posted on Apr 20 2006 at 12:15 AM said:
Episode 4 - in which absolutely nothing impressive is accomplished!

As always, the source and gpe is available from http://members.gamedev.net/dzz/demo4.zip

I've read your progress with interest and look forward to more.

Call me old fashioned (or at least old) but I'm putting in a request
that you give more thought to function naming convention. One of
the problems with the 'C' language is that it completely ignores
multiple module program structure and leaves such issues up to the
coder. This is no problem for the single coder but team development
or borrowed code bumps into the namespace collision issue.

I want to use your demo code in my application and I need to
rename some of the functions to avoid confusion/collision.

One convention that works well is:

<module name> <underscore> <function verb> <underscore> <qualifiers>

e.g. StartTimer() becomes Timer_Start() written in timer.c / timer.h

e.g. ClearScreen() becomes Screen_Clear() written in screen.c / screen.h

e.g. AddDebugLine() becomes Debug_Add_Line() written in debug.c / debug.h

Now all related functions have the same prefix and I know in which
program file each will be found.

Object oriented languages put the object name in place of the module
name but the pattern can be the same.

If you don't like mixed capitalisation or underscores, I suppose you leave
them out or learn to touch type :)

Dzz's code is not clean doesn't compile warning free at all. For the complete packages/articles/PDF's I try to fix that and I changed the code like this:

1) functions:
start with verb follwed by noun(s) with cap first letter:

doThing(); getLength(); fuckMe();

2) vars: are all lower case and words are separeted by undescores (I was lazy and didn't do that everywhere yet)

int black_puppies;

3) every *.c has a *.h with proper prototypes and so on...

As I consider the stuff in here as WIP no need to be clean :) On the other hand you are right, cleaning otherpeople's code is a shit work, I hate that!
 
Last edited by a moderator:
synkro posted on Apr 20 2006 at 05:55 AM said:
Dzz's code is not clean doesn't compile warning free at all. For the complete packages/articles/PDF's I try to fix that and I changed the code like this:

As to naming conventions, yawn. The two suggestions that you both put forward as necessary are also completely incompatible with each other.

At any rate, the article series has reached a point where it's a pretty complete framework. Any code from this point would just be demonstrating a particular effect or some other code specific to a particular demo, so perhaps this is good stopping point. Just fill in DrawScreen (or drawScreen or screen_draw or whatever floats your boat) with something creative.
 
Last edited by a moderator:
In part one and two I had missing prototypes, empty c files... can't remember the rest. Name the stuff the way you like it, everybody can freely modify it to theirs tastes. Me wonders that you already consider the "framework" "complete": no malloc, no sound, no sine to just name a few... Nonetheless keep up the good work...
 
synkro posted on Apr 20 2006 at 07:02 AM said:
In part one and two I had missing prototypes, empty c files... can't remember the rest. Name the stuff the way you like it, everybody can freely modify it to theirs tastes. Me wonders that you already consider the "framework" "complete": no malloc, no sound, no sine to just name a few... Nonetheless keep up the good work...
I suppose it's possible to force the compiler to warn about missing unnecessary prototypes if you set some obscure option. I'm pretty sure none of the C files were empty, although I might have included one by mistake somehow I suppose.

I wasn't thinking that malloc would necessarily be needed by a demo. If it is, here's how to do it: declare a huge (big enough) unsigned char array as a local variable in main(). Pass that to an initialization function for your memory allocator. Write a memory allocator, which is just a bunch of data structure code not particularly specific to demo development. Or find one of the billion implementations of such a thing on the web and port it.

I guess I could demonstrate how to use /dev/dsp in an environment where we don't have pthreads, which would be useful. Maybe I'll do that.

I'm not sure what you mean about sine, a minute with google turns up plenty of trig function code, I don't know that I need to write more.
 
Last edited by a moderator:
Actually, now that I think about it, maybe the best way to continue from here is to address particular issues that people want to hear about.

The problem I have reached here is that if I start to implement some particular demo effect, all that code is going to end up being a big pile of stuff that looks like:

x_val = 3 * time + TrigFunctionCollection_Sin_FixedPoint16Dot16(loop_counter);
color_val = MathFunctionCollection_Modulo_UnsignedShort(x_val, 256);

or whatever - interesting I suppose but not of any general use.

But if somebody wants to know something that is specific to gp2x demo development that would probably be more helpful in general (providing that it's not just a request to write a pile of code that can probably be found with google).

So the thread is open to suggestions, so far there's how to write sound. I just thought of another one, which is how to get code running on the 940. But it will probably be better to do those in isolation instead of as part of a larger demo project. For one thing, it will be less work for people to reorganize the code to suit their tastes if there is less of it.
 
I have been following this thread since the start, and it is very interesting to me. At this time, I am not planning on using/compiling any of it, but I still find it intriguing to follow the progression and hope to learn things along the way.

I am particularily interested in anything to do with sound. I've never felt comfortable with audio programming, especially at a low level like this.

At this point, if I decided to do a demo, I wouldn't even know how to approach the sound issue.

I am also curious about how you would go about generating the sound tracks and what tools you'd use to compose, convert, and play them on the PC/GP2X.

So, anyway, I just wanted to register my vote. To me, your series is a technology framework. In my opinion, it should be left as a framework because it should be left to the demo coder's artistry to come up with specific visual/audio effects. For a tutorial, boring placeholders are what you'd want. Also, I would fear that if you added in any interesting effects to your demo, then your competition would be flooded with minor tweaks upon that same theme.
 
Flavor posted on Apr 20 2006 at 08:17 AM said:
I have been following this thread since the start, and it is very interesting to me. At this time, I am not planning on using/compiling any of it, but I still find it intriguing to follow the progression and hope to learn things along the way.

I am particularily interested in anything to do with sound. I've never felt comfortable with audio programming, especially at a low level like this.

At this point, if I decided to do a demo, I wouldn't even know how to approach the sound issue.

I am also curious about how you would go about generating the sound tracks and what tools you'd use to compose, convert, and play them on the PC/GP2X.

So, anyway, I just wanted to register my vote. To me, your series is a technology framework. In my opinion, it should be left as a framework because it should be left to the demo coder's artistry to come up with specific visual/audio effects. For a tutorial, boring placeholders are what you'd want. Also, I would fear that if you added in any interesting effects to your demo, then your competition would be flooded with minor tweaks upon that same theme.
Ok, I'll tackle sound next for sure. Most people wanting to enter the demo competition will not be trying for a 64k demo so they can just use the SDL (or mikmod) sound functions -- no fuss. The pesky restrictions of the approach being taken here make that impossible of course.

My impression is that the giants of 64k demo writing on other platforms have two different approaches to the problem. First, the demo groups have developed over time very capable "soft synths" which generate complex musical tones on the fly based on algorithms. I think that's so cool that it makes me feel faint, but it's a major development task, probably on the same order of magnitude as a simple 3D graphics engine.

The other approach is to use small "mod" files (or other variations on the "mod" concept). One would use a "tracker" (many are available) to create the files. Then there has to be a bunch of code to interpret the file, of course. I've done some research on this and the most likely approach is to port a thing called "minifmod" which plays FastTracker (.xm) modules and claims to be less than 10k when compiled.

I spent a fair amount of time one evening studying the minifmod source code and it's going to be nontrivial to port. The reliance on external li braries is not too extensive, but it does rely heavily on floating point math and most importantly one of the largest central core functions is coded in intel assembly. Someday I have been planning to suck it up and port that, but as you can imagine it will not be the funnest project. It also doesn't make much of an article for this series -- How to Port minifmod to the gp2x.
 
Last edited by a moderator:
Dzz posted on Apr 20 2006 at 03:33 PM said:
But if somebody wants to know something that is specific to gp2x demo development that would probably be more helpful in general (providing that it's not just a request to write a pile of code that can probably be found with google).

I vote for a simple, minimal way to get some code executed by the 940.

Also, low level AC97 manipulations would be very interresting, although less gp2x specific.

Another thing that would interrest me : how to use the hardware dithering.

But enought with requests : I feel guilty to ask for all these things while I did not even try to cross-compile my own stuff onto the gp2x yet :)

Tnak you very much for this great article serie, anyway.
 
Last edited by a moderator:
Dzz: Have you looked at mlib by Mr Spiv? It's a a module player written in arm assembler for the GP32. It may be easier to port than X86 code, and being written in assembler, it'll probably be a little shorter, too.

Also, I see Spiv has released his b2fxec source too which could make a useful compression engine. Comes with decompression source in arm assembler.

=====

rixed:
There are various ways to get code running on the 940, one is to "create" a compiler that is dedicated to creating executables that'll work on the 940 (basically, using a standard ARM compiler and hacking the linkerscript, and then building your own crt0.s). This method allows usage of the run time library and lots more.

Another is to generate position independant code using the same compiler as you use for everything else, but this means that for the best ease of use, your 940 code should consist of a single function, and no library calls.

Both methods would require your main code to copy the 940 code to a convenient location in ram (above the 32MB limit) and tell the 940 to execute it. Both would require a way to communicate between the processors (possibly using the upper memory again).

Low level AC97 manipulations is a little more difficult, as Linux has all the cards here (all the interrupts/etc point inside the linux kernel for example). You can certainly mess with a few settings, but they can be overwritten at any time, and so it's probably easier and more reliable to use the appropriate IOCTLs on /dev/dsp or /dev/mixer.
 
Squidge posted on Apr 20 2006 at 04:13 PM said:
Dzz: Have you looked at mlib by Mr Spiv? It's a a module player written in arm assembler for the GP32. It may be easier to port than X86 code, and being written in assembler, it'll probably be a little shorter, too.

Also, I see Spiv has released his b2fxec source too which could make a useful compression engine. Comes with decompression source in arm assembler.
Thanks for the tips! I'll check those out.

Another is to generate position independant code using the same compiler as you use for everything else, but this means that for the best ease of use, your 940 code should consist of a single function, and no library calls.
This is the method that I have used myself, although I have never had a problem using more than one function (just make sure the correct function is the first one. I'll give a complete example in a future article in this series. Anybody eager to try it right away can work from rlyeh's minlib code.
 
Last edited by a moderator:
Back
Top