Normal bitmaps can be boring, so many games spice them up by rotating and scaling them for various purposes. Rotated characters can follow the curve of a 2D terrain in a game like Dragon, Fly!. Mario himself can scale up to huge size in New Super Mario Bros.. So today we continue the HTML5 vs. JavaScript series by testing the performance of rotating and scaling bitmaps to better compare the performance across Windows, Mac, iOS, and Android. Will Flash maintain its lead? Read on to find out.

Today’s test builds off the last test by adding buttons and functionality for bitmap rotation and scaling. Unfortunately for Flash and AS3, this means that BitmapData.copyPixels can no longer be used. How will the conversion to BitmapData.draw affect performance? Try out the tests to see for yourself and then check out my results below.

Launch the HTML5 version
Launch the Flash version

Here is the source code for the Flash version:

package
{
	import flash.geom.Matrix;
	import flash.events.MouseEvent;
	import flash.text.TextFormat;
	import flash.geom.Rectangle;
	import flash.utils.getTimer;
	import flash.text.TextFieldAutoSize;
	import flash.display.StageScaleMode;
	import flash.display.StageAlign;
	import flash.text.TextField;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
 
	public class BitmapScaleRotFlash extends Sprite
	{
		[Embed(source="opaque.jpg")]
		private static const OPAQUE:Class;
 
		[Embed(source="alpha.png")]
		private static const ALPHA:Class;
 
		private var opaqueBMD:BitmapData = (new OPAQUE() as Bitmap).bitmapData;
		private var alphaBMD:BitmapData = (new ALPHA() as Bitmap).bitmapData;
		private var screenBMD:BitmapData;
		private var screenRect:Rectangle;
		private var drawMat:Matrix;
		private var time:TextField;
 
		public function BitmapScaleRotFlash()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
 
			screenRect = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight);
			screenBMD = new BitmapData(640, 480);
			addChild(new Bitmap(screenBMD));
 
			drawMat = new Matrix();
 
			makeButtons(
				"Alpha - Rotation", "Alpha - Scaling", "Alpha - Rotation & Scaling", null,
				"Opaque - Rotation", "Opaque - Scaling", "Opaque - Rotation & Scaling"
			);
 
			time = new TextField();
			time.text = "Time: 9999";
			time.autoSize = TextFieldAutoSize.LEFT;
			time.x = stage.stageWidth - time.width;
			time.y = stage.stageHeight - time.height;
			addChild(time);
		}
 
		private function makeButtons(...labels): Number
		{
			const PAD:Number = 5;
			var curX:Number = PAD;
			var curY:Number = stage.stageHeight - PAD;
			for each (var label:String in labels)
			{
				if (label == null)
				{
					curX = PAD;
					curY -= button.height + PAD;
					continue;
				}
 
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans");
				tf.autoSize = TextFieldAutoSize.LEFT;
				tf.text = label;
				tf.name = "lbl";
 
				var button:Sprite = new Sprite();
				button.buttonMode = true;
				button.graphics.beginFill(0xF5F5F5);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.graphics.endFill();
				button.graphics.lineStyle(1);
				button.graphics.drawRect(0, 0, tf.width+PAD, tf.height+PAD);
				button.addChild(tf);
				button.addEventListener(MouseEvent.CLICK, onButton);
				if (curX + button.width > stage.stageWidth - PAD)
				{
					curX = PAD;
					curY -= button.height + PAD;
				}
				button.x = curX;
				button.y = curY - button.height;
				addChild(button);
 
				curX += button.width + PAD;
			}
 
			return curY - button.height;
		}
 
		private function onButton(ev:MouseEvent): void
		{
			var mode:String = ev.target.getChildByName("lbl").text;
			switch (mode)
			{
				case "Opaque - Rotation":
					test(opaqueBMD, true, false);
					break;
				case "Opaque - Scaling":
					test(opaqueBMD, false, true);
					break;
				case "Opaque - Rotation & Scaling":
					test(opaqueBMD, true, true);
					break;
				case "Alpha - Rotation":
					test(alphaBMD, true, false);
					break;
				case "Alpha - Scaling":
					test(alphaBMD, false, true);
					break;
				case "Alpha - Rotation & Scaling":
					test(alphaBMD, true, true);
					break;
			}
		}
 
		private function test(bmd:BitmapData, rotation:Boolean, scaling:Boolean): void
		{
			var REPS:int = 10000;
			var maxX:int = screenRect.width - bmd.width;
			var maxY:int = screenRect.height - bmd.height;
			var beforeTime:int = getTimer();
			screenBMD.fillRect(screenRect, 0xffffffff);
			screenBMD.lock();
			for (var i:int = 0; i < REPS; ++i)
			{
				drawMat.a = drawMat.d = 1;
				drawMat.b = drawMat.c = 0;
 
				if (scaling)
				{
					drawMat.a = 1+Math.random();
					drawMat.d = 1+Math.random();
				}
 
				if (rotation)
				{
					drawMat.rotate(Math.random()*Math.PI*2);
				}
 
				drawMat.tx = Math.random()*maxX;
				drawMat.ty = Math.random()*maxY;
 
				screenBMD.draw(bmd, drawMat);
			}
			screenBMD.unlock();
			var afterTime:int = getTimer();
			time.text = "Time: " + (afterTime-beforeTime); 
		}
	}
}

And here are the images it embeds:

Opaque image
Alpha image

You can get the source code of the HTML5 version by simply opening up the HTML file. Its embedded images are Base64-encoded into the img tags.

Here are the devices tested: (note that the Windows Desktop device has changed since last time)

Name CPU GPU OS
MacBook Pro Retina 2.3 GHz Intel Core i7 Intel HD Graphics 4000 OS X 10.8.3
Windows Desktop 3.4 GHz Intel Core i7 2600 Nvidia GeForce GTX 550 Ti Windows 7 SP 1
iPad 2 1 GHz ARM Cortex-A9 PowerVR SGX543MP2 iOS 6.1.3
LG Optimus G 1.5 GHz Qualcomm Krait Qualcomm Adreno 320 Android 4.1
HTC Evo V 4G 1.2 GHz Qualcomm Snapdragon Qualcomm Adreno 220 Android 4.0

And here are the results:

Performance Graph (all)

Performance Graph (Windows, Mac)

Performance Graph (mobile)

From this data we can draw some interesting conclusions:

  • Flash is in last place on Windows by a wide margin of about 3x. On Mac it’s also about 3x slower than Safari and Chrome but Firefox lags behind the browser competition nearly the point where it’s as slow as Flash.
  • The slowdown in Flash compared to the plain bitmap draws last week mirrors the slowdown scene in Composing BitmapData Scenes.
  • Safari is the fastest mobile browser by far. On the iPad 2 it even bests much faster devices like the LG Optimus G and is nearly as fast as the slower times on Windows and Mac machines.
  • There isn’t much difference between opaque and alpha images regardless of environment. The same goes for rotation and scaling: they’re about the same cost.

Flash was looking great in the results of the last article but this time around we’ve seen a dramatic reversal: it’s in last place on both Windows and Mac. On Android and iOS it doesn’t even get a chance to get started in browsers, only as AIR apps (not tested). Between the two tests these are probably the more realistic results for most games, but this is still far from a full performance comparison. Stay tuned for more in this series to see if Flash can make a comeback!

As a disclaimer with mobile, there are many devices out there with wildly different performance characteristics. Even iOS has five base models of iPad, six of the iPhone, and five of the iPod Touch. There are hundreds to thousands of Android devices. It’s therefore really hard to get a complete picture of mobile performance since very few people have access to so many devices. I certainly don’t. However, I’m guessing many people reading this article have a couple of minutes to point their mobile browsers at the above tests. Want to contribute your own test results? Post a comment with your device name, opaque and alpha times, and if you have them or can look them up, CPU and GPU specs.