GP2X Gp2x Demo Development


Dzz posted on Apr 11 2006 at 12:01 AM said:
As the sponsor of the gp2x demo competition, I am ineligible to win a prize, but just for fun I will develop my own demo over the next few months anyway. I'll document my progress in a series of articles, in case the things I discover will be of use to other developers.

...
In the next installment I'll look into getting access to the gp2x screen so I can show off my mad grafix skillz.

This is a great idea. I hope you get alot of good response to this. It would be cool if the GP2X had a demoscene. It was great on the Amiga.

Good luck with this, and thanks for your great game which uses some awesome effects.
 
Last edited by a moderator:
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!
 
There's a /dev file that tracks the state of the buttons... since you've already got code to open files, that might be easier than looking at registers, dunno.
 
Squidge posted on Apr 12 2006 at 12:14 AM said:
@Squidge: As we aim for 64K demo here, we will need malloc to get mem for precalculates LUTs (I am thinking of a sine table) and even iterative generation of gfx as 64K is very little...

Someone obviously didn't research what the system call 'brk' does :)

I have no fuckin' clue what a system call is at all :) The only fun coding I had in the past days were testing the USB-Seriel-Thingy and the code in this thread... I'll have a look at the weekend... I promise...
 
Last edited by a moderator:
In episode 2, I wrote:

> We do this with a system call called 'mmap' which asks for an address corresponding to a particular physical address range.

That is not actually true in general. mmap maps a range of bytes from an opened file into a virtual address so it can be accessed as memory. In the particular case where it is used in this code, the file is /dev/mem and so the statement I made above is true for its usage in the demo. I just thought I should clarify that so I don't misinform anybody about what the 'mmap' system call does in general.
 
First of all, I want to say
1) I haven't read all of your post, yet.
2) I'm no expert, so I could be wrong in some areas. Please, someone, correct me if I have mislead/misinformed/etc.

Your code
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;
}

The ARM ASM code that's generated by your function call will put the parameters in registers already. In fact, pszFile should be in r0 and nMode should be in r1. In addition to that, the return value will be put in r0.

Since you are clobbering r0 and r1, that means that the compiler may have to work around these registers with some unnecessary movs before and after your ASM gets started. Though, I am not sure how GCC will optimize this. I was surprised how poorly it handled some of my code in the past.

I suggest something more like this (if you're concerned about execution speed).
Code:
 int OpenFile(char *pszFile, int nMode)
{
  int nFile;

  asm volatile
  (
	//"mov r0, %1\n"	  // file to open	//pszFile is already in r0
	//"mov r1, %2\n"	  // mode   //nMode is already in r1
	"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;
}

You can edit your GCC line and add "-save-temps" and then examine the .S file that it spits out. That way you can get a nice idea of how efficient your code is.

Also, I can not test this, but something like this might be a working optimization, too.

Code:
 int OpenFile(char *pszFile, int nMode)
{
  register int nFile asm ("r0");

  asm volatile
  (
	"swi #0x900005\n"   // open
  );
  return nFile;
}

Essentially, you can boil your OpenFile down to just
Code:
inline int OpenFile(char *pszFile, int nMode)
{
  asm volatile
  (
	"swi #0x900005\n"   // open
  );
}

Though, none of my suggestions are real nice for a tutorial, but hopfully it will help people understand the ASM a bit more.
 
Wow, those suggestions are excellent! Thanks!

In retrospect you're absolutely right that the parameters will already be in the correct registers to pass them down to the system calls as long as I have them in the same order. Probably the best thing for me to do is put those in an assembly file to start with as I think the resulting functions will be trivially small (and I bet I can save at least 100 bytes from the GPE file by doing that, maybe even more). The only drawback is that I don't know how to do it, but that's no excuse -- figuring out stuff is what this is all about. So I'll look into it and post an update.
 
Dzz posted on Apr 12 2006 at 01:56 PM said:
Wow, those suggestions are excellent! Thanks!

In retrospect you're absolutely right that the parameters will already be in the correct registers to pass them down to the system calls as long as I have them in the same order. Probably the best thing for me to do is put those in an assembly file to start with as I think the resulting functions will be trivially small (and I bet I can save at least 100 bytes from the GPE file by doing that, maybe even more). The only drawback is that I don't know how to do it, but that's no excuse -- figuring out stuff is what this is all about. So I'll look into it and post an update.

You might notice that my last version was an "inline" function. I know you're trying to save both cycles and file size. If the function is small enough (like that last one is a single line of ASM) then it's probablly better to just inline it, anyway. Some guys suggest making it a "static inline" becuase then the compiler won't make both a function version and an inline version. In my practice, though, I haven't seen that happening in the resulting .S file (with -save-temps).

Anyway, if the function is just one line of ASM, it's probablly both faster and smaller to just inline it. Once it starts getting any larger, though, it's probablly going to be faster as an inline, but it will make the code larger if you call it in multiple places. Of course, if you're only calling it once anyway, then the best bet would probablly be to make your original ASM statement into a #define.

Once again, though, I'm no expert. There are guys way better than I am at this. I just happen to be learning some similar stuff at about the same time as you.
 
Last edited by a moderator:
I don't know how GCC handles this, but I've seen some nasty optimizations with other compilers with regards to inline functions where the parameters to the functions are not used. The problem normally is that the compiler inlines the function, and then notices that the parameters passed to the function in R0 - R3 are not actually used by the function, and thus removes them completely. You end up with a program that doesn't work (or works very strangely) as the parameters are now garbage.

Considering the fact we are using system calls, which are slow anyway, an additional branch to a subroutine isn't going to make it much slower, and, tbh, you should generally not be using system calls after initialisation in a demo anyway.

As for putting the SWI's into an ASM file rather than a C file, something like this could possibly be used:

Code:
	.align 4
	.globl OpenFile

OpenFile:
	swi #0x900005
	mov pc, lr

Save it as a ".s" file, and compile with gcc as normal. All you need then is a prototype for GCC so it passed the arguments:

extern int OpenFile(char *pszFile, int nMode);
 
Hi,

Flavor posted on Apr 12 2006 at 05:07 PM said:
The ARM ASM code that's generated by your function call will put the parameters in registers already. In fact, pszFile should be in r0 and nMode should be in r1. In addition to that, the return value will be put in r0.
I'm not sure that this is guaranteed to happen. I think this happens more by convention than anything else, as technically the "r" constraint is a "general-purpose register", not the "next free register starting at r0". From looking at the GCC docs, the only way to be sure of this is to declare as type 'register' the variables used in the constraints lists. Perhaps it might be worth writing assembler-heavy portions as proper assembler, rather than inline asm, for readability if nothing else!

It probably wouldn't be wise for me to even try rewording the example above just now, but a read of the GCC docs and some experiments might be able to prove it one way or the other.

I must confess I've only recently started using ARM assembler, although I did research it a few years ago, when I was looking into developing for the GBA, and I'm definitely a novice when it comes to using inline assembler in GCC.

I love the idea of this demo competition, and it has a decent deadline too. It might be just the thing to keep my momentum going at the moment. I hope everybody else feels the same.

Also, if nothing else, we may finally get a build process which will result in sane binary sizes! I tried to get something done on this a few weeks back and gave up in frustration. It may be that asm is the way to go!
 
Last edited by a moderator:
scorpio posted on Apr 12 2006 at 08:21 PM said:
Flavor posted on Apr 12 2006 at 05:07 PM said:
The ARM ASM code that's generated by your function call will put the parameters in registers already. In fact, pszFile should be in r0 and nMode should be in r1. In addition to that, the return value will be put in r0.
I'm not sure that this is guaranteed to happen. I think this happens more by convention than anything else, as technically the "r" constraint is a "general-purpose register", not the "next free register starting at r0".

GCC uses the Arm Procedure Calling Standard, which states that functions that takes 4 or less arguments use registers R0 - R3 to pass the arguments in if possible:

The first four registers r0-r3 (a1-a4) are used to pass argument values into a subroutine and to return a result value from a function. They may also be used to hold intermediate values within a routine (but, in general, only between subroutine calls).
 
Last edited by a moderator:
scorpio posted on Apr 12 2006 at 02:21 PM said:
Flavor posted on Apr 12 2006 at 05:07 PM said:
The ARM ASM code that's generated by your function call will put the parameters in registers already. In fact, pszFile should be in r0 and nMode should be in r1. In addition to that, the return value will be put in r0.
I'm not sure that this is guaranteed to happen. I think this happens more by convention than anything else, as technically the "r" constraint is a "general-purpose register", not the "next free register starting at r0".

I think you're right that "r" doesn't necessarily mean r0. But, in this case DZZ was using "pszFile" as the first "r". "pszFile" was already going to be r0 because it was the first param to the function.

My fear was that with DZZ's original code, the compiler would do something like

Code:
mov r3,r0	 //save the function parameter to a new register because DZZ said that r0 would get clobbered
mov r0,r3   //put %0 into r3

I know it sounds odd, but I saw the compiler doing such things in the past. I don't know what causes it to know sometimes and not know other times. It could have to do with how your optimizations are set.

Anyway, the main point is that if you know it's in r0 already, there's no need to put it there again.
 
Last edited by a moderator:
Squidge posted on Apr 12 2006 at 01:19 PM said:
As for putting the SWI's into an ASM file rather than a C file, something like this could possibly be used:

Code:
	.align 4
	.globl OpenFile

OpenFile:
	swi #0x900005
	mov pc, lr
Excellent, I was hoping it would be that simple. I'll give it a try later. Thanks for the help guys! My main strength is not that I'm smart, it's that I'm not afraid to be stupid.

In case one of you knows -- for the mmap call there are six parameters. Am I correct that the first four will be in r0 - r3 and parameters 5 and 6 will be on the stack? If so I should be able to simplify that function considerably as well.
 
Last edited by a moderator:
Dzz posted on Apr 12 2006 at 09:57 PM said:
In case one of you knows -- for the mmap call there are six parameters. Am I correct that the first four will be in r0 - r3 and parameters 5 and 6 will be on the stack? If so I should be able to simplify that function considerably as well.

I think so, yes. Check the document I linked to above.
 
Last edited by a moderator:
Thanks for all your help, this new system call code works great and is very simple (for example):

Code:
OpenFile:
	swi #0x900005
	mov pc, lr

MMap:
	stmdb sp!, {r0, r1, r2, r3}
	mov r0, sp
	swi #0x90005A
	add sp, sp, #16
	mov pc, lr

The demo.gpe has shrunk to 1544 bytes now. Lastest code/gpe at http://members.gamedev.net/dzz/demo2a.zip
 
Episode 3
Summary: In this episode, I combine one of the oldest of old-school demo effects with a little gp2x-specific hack to create the beginnings of a visually somewhat interesting title sequence for the demo that requires very little code and leaves almost all of the CPU available for precomputation of stuff to be used for later effects. Total size of "demo.gpe" after episode 3: 2964 bytes.

Last time, I got the basic shell of the demo up and running, including some simple screen I/O and a button to exit. Now it's time to start thinking about the content of the demo. Many demos are divided into sections. The first section is a sort of title sequence in which the author (demo group) presents the title of the demo. Then at the end there's a sequence where the credits roll. For old-style demos that usually involves a "scroller" which is a fancy text effect in which some message is delivered, usually greeting pals in the demoscene. Newer style demos have credits that look more like TV show or movie credits.

In between these, there are the main visual effects, often with transition sequences between them.

I think for this demo I will aim for two "main" effect sequences, some sort of wipes or fades between them, a small effect as part of the title sequence, and another effect plus text "scroller" at the end. Plus music, of course.

One interesting thing about doing a demo on the gp2x is that it really does have a huge amount of processing power. Some of the hardware tricks available on old machines aren't really needed on the gp2x, which can easily redraw the entire screen every frame. However, for the title sequence, it would be nice if I could keep the CPU use to a minimum, and here's why:

Let's suppose that I had ten seconds available where I could do anything I wanted with the CPU and didn't have to worry about producing fancy effects. In theory, that would be about 4 billion machine cycles to do precomputation of stuff that might come in handy for later effects! That sounds very handy! So, to get that I just need an effect for the title sequence that is very easy on the CPU, which lets me make a nod to some of the old-style techniques.

Color cycling is an old technique in which the screen data is left alone, but a color lookup table (or "palette") is changed in real-time. This is a very cheap way of making large screen areas change their appearance without actually changing much memory. To use color cycling, I'll switch the gp2x into 8-bit color mode and then rotate a color palette to produce effects on some pre-drawn screen memory. The following code accomplishes this:
Code:
g_pusRegs[0x28DA>>1] = 0x002AB; // 8bpp, only region 1 activated
g_pusRegs[0x290C>>1] = 320; // in 8bpp mode, this means width = 320
Then the only thing that remains is to figure out how to set the color palette. Displaying no shame whatsoever, I figured this out by looking at the SDL source code. Assuming you have your Red, Green, and Blue palette colors stored in some arrays, here's code to load the gp2x palette:
Code:
g_pusRegs[0x2958>>1] = 0;
asm volatile ("":::"memory");
for(n = 0; n < 256; n++)
{
	g_pusRegs[0x295A>>1] = (g_pusGreens[n] << 8) + g_pusBlues[n];
	asm volatile ("":::"memory");
	g_pusRegs[0x295A>>1] = g_pusReds[n];
	asm volatile ("":::"memory");
}
A little explanation of this is needed. Register 0x2958 is the "address" representing the first color being set. Since I'm setting all 256 colors, it is set to zero so the first colors I write go into palette entry 0.

Then the colors themselves are set by repeatedly writing to the exact same register! The chip must shift the values into an internal table every time it notices a write. Green and Blue get written first, then red by itself. It's just the way it's done.

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).

The second part of the introductory "effect" will be based on changing the pointer for the screen memory location in an unusal way. Recall in episode 2 I just allocated a big bunch of memory and decided to use some of that for display instead of using the linux /dev/fb0 device. Now, instead of the usual procedure of drawing into one screen area and "flipping" the display address to a completely different memory area, I can "scroll" the screen by just incrementing the pointer by a little bit. This could be a nice technique for a vertical scrolling game, and it suits my needs for this segment of the demo because if I can avoid redrawing the screen I can use those cycles for some precomputation instead. Here's the code that is roughly what I'm using in the demo:
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 rest of the effect is just simple array manipulation and not really worth going through. It just draws a bunch of "diamonds" with different colors, fills a color array, and moves the screen memory pointer around. The effect as it stands is somewhat interesting to look at (more than the old rectangles at least), but it isn't really finished. Once I figure out a title for the demo I'll work on making the demo title be part of this effect somehow.

In the meantime, keeping the effect running uses about 0.1% of the cpu cycles on the first processor, leaving 99.9% of them available for other work.

demo3.png


As always, the source and gpe can be found at http://members.gamedev.net/dzz/demo3.zip.

I'm going to leave the introduction sequence like this for now, at least until I figure out a title for the demo. Next I'll either tackle one of the main visual effects or change gears and start working on the music. Either one is going to be a big job!
 
Already the #3 ? :blink: So cool ! Thanks a lot, really interesting, useful and fun !
 
Orion_ posted on Apr 15 2006 at 05:46 AM said:
can we use this source code for the demo competition ?
Sure, I was hoping that people would find it helpful to use some of this code. The code is released to the public domain, and next time I'll start putting notices to that effect in the code itself.

If your entry has lame and rather ugly color cycling diamonds, it might not get a very high score :D
 
Last edited by a moderator:
Back
Top