Saturday, May 24, 2008

Flash AS3: graphics.drawRect Memory Leak


I know this is supposed to be an art blog, but as soon as you introduce a computer, the lines between art and engineering start to get blurred. Regardless, I found this Flash AS3 Memory Leak to be such a landmine that I just had to post it somewhere for other people to hopefully find and breath a sigh of relief when they realize why their flash game slows down and grinds to a halt the longer they play it.

Okay, no beating around the bush. Do this code once a frame (or more!) and you're flash game will grind to a halt.

mySprite.graphics.beginFill( 0xFFFFFF );
mySprite.graphics.drawRect( vPos.x, vPox.y, vSize.x, vSize.y );
mySprite.graphics.endFill();

This was my "DrawBlock" function for my falling blocks game. I see these three lines posted all over the web in tutorial and in forums. --Worse, I often only see the first two lines and no endFill(). The above code eats memory.

This code will NOT eat memory:
mySprite.graphics.clear(); // <--- required.
mySprite.graphics.beginFill( 0xFFFFFF );
mySprite.graphics.drawRect( vPos.x, vPox.y, vSize.x, vSize.y );
mySprite.graphics.endFill();

Okay, now I'll begin my rant / explanation of the problem and solution.

In 8 years of making professional console games I've never seen a drawRect function eat memory. I'm talking, SNES, Playstation, PS2, Xbox, PS3, Xbox360 --none of them had any reason to use more memory to fill some offscreen pixels with color. So I found this result in Flash, not only unexpected, but jaw-dropping.

"drawRect" should do exactly what it says it does. In a console game engine, we would create a new sprite (or something of that effect) and the engine would grab a piece of memory for that sprite. This would happen once. After the sprite has some memory to draw on, there's no need to ever get more.

Imagine if you will, a simple square piece of paper --like one of those note pads you most likely have on your desk. Saying "create a new sprite" is like pulling off the top sheet of paper and putting it on the desk in front of you so you can use it.

After that, you can scribble on that piece of paper as much as you want and as often as you want. You can draw a rectangle as many times as you want in as many colors as you want but you only ever use ONE piece of paper. And if the paper is too full, you can always erase it or cover the thing with white out or something that allows you to continue to use ONE piece of paper.

In computer terms, that piece of paper is a piece of memory, and drawing on it is *ultimately* like changing the color of pixels. It doesn't matter how many times you draw and change the color of the pixels, you'll always have the same number of pixels in your sprite. The idea of a "drawRect" function taking more memory with each call is a big giant "WTF!" because there's no need for it.

Now, here's my hypothesis on why flash's drawRect function is eating up memory. (notice I said "hypothesis") I believe that flash's "drawRect" function is really an "addDrawRect" function for one simple reason: Flash obviously capitalizes on vector graphics.

I believe "drawRect" doesn't draw a rect at all, I think it just adds a rectangle shape to a list of things to draw when this sprite is told to draw to the screen (later in the frame). Basically, every time you call drawRect, imagine pulling another sheet of paper off your notepad and placing it on top of what you already have in front of you. Each one on of these pieces of paper would have one shape drawn on it. Before long you will have a stack of paper in front of you because, however thin paper is, it adds up.

And that's it. In console games, drawing a sprite is usually as simple as copying the sprite's pixels to the screen (aka screen buffer), while drawing a sprite in flash basically runs through a list of every shape in a list and then draws it to the screen based on what the vector math for that shape tells it to do.

So in console games, rather than calling a clearRect or clearSprite function every frame, we literally just drop a new color right on top of the old. If a block in my tetris board is red and should be changed to blue, then we just draw blue over the red data because clearing the data and then drawing it blue takes twice as long.

With flash, if I used my little draw block function to change the color of a block every frame, the first frame Flash would draw 1 rect (say, red), the second frame Flash would draw 2 rects directly on top of each other (red and then the second color, like blue), on the third frame Flash would draw 3 times (red, blue, and 3rd color), forth frame -4 rects, etc...

Here's the catch --drawing (aka copying or writing to memory) is the SLOWEST part of a game. It's the reason we have expensive graphics chips that bench-test based on how many polys they can draw in a frame.

So, in the end, Flash is quite the opposite of what I've seen in the past. Rather than finding tricky ways to skip the "clear" function, I should use it every chance I get.

so... the next time you use "drawRect" in Flash, think of it as "addDrawRect".

Thanks for reading,
~Danny

PS: my Falling Blocks flash game.

6 comments:

John said...

Yep its a very good pratice to add EndFill with your code. But what I don't get is with the "fillRect" command you get (shape,x,y,h,w,shapeColor,centerboolean I can't find a good explation any where what "centerboolean" is refering to so if you got some idea help me out thx.

JT

Thiagarajan said...

Hey thanks man,
I was doing a Cellular automata sim using with cells rendered using drawRect and din't use clear function before each re-render.The whole thing ground to a halt within seconds.
Adding clear fixed the whole thing.

Danny said...

awesome I glad I was able save someone a little hair pulling :)

Anonymous said...

Don't forget, the Flash player is all vector.
Any of the systems you mentioned in your post are raster. Therefore your altering the pixels directly, whereas in Flash you're creating a new vector shape, which is defined via points and math to determine the paths.
Any new square, circle or line drawn with DisplayObject.graphics doesn't merge itself into the existing graphic, but instead creates a new object that overlays above the existing.

Danny said...

Exactaly my point my good man :)

Unknown said...

Thanks for the info. I was about 5 minutes away from throwing away the project I was working on and found your info, said "that cant possibly fix my problem" and then it sure did.