Flash 11’s new Stage3D API gives us hardware-accelerated 3D graphics, which is a major jump forward for Flash-based games and simulations. Along with this comes some added responsibility: we must now care about our users’ graphics cards. Today’s article features a simple benchmarking application that you can run to get a basic idea of how Stage3D is performing on a certain computer. Read on for the benchmarking app!

The app’s design is simple, as its name implies. It simply draws a grid of pulsating, swirling circles in all white. It has buttons for you to control:

  • The number of rows and columns
  • The rendering mode: hardware or software
  • The number of sides of each circle, which is made of triangles

Here you can try it out at a variety of resolutions:

And here’s the source code:

package
{
	import com.adobe.utils.*;
 
	import flash.display.*;
	import flash.display3D.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.text.*;
	import flash.utils.*;
 
	public class SimpleStage3DBenchmark extends Sprite 
	{
		private var context3D:Context3D;
		private var vertexBuffer:VertexBuffer3D;
		private var indexBuffer:IndexBuffer3D; 
		private var program:Program3D;
 
		private var modelMatrix:Matrix3D = new Matrix3D();
		private var numSides:uint = 30;
		private var numTris:uint;
		private var numRows:uint = 2;
		private var numCols:uint = 2;
 
		private var fps:TextField = new TextField();
		private var lastFPSUpdateTime:uint;
		private var lastFrameTime:uint;
		private var frameCount:uint;
		private var driver:TextField = new TextField();
		private var settings:TextField = new TextField();
		private var help:TextField = new TextField();
 
		public function SimpleStage3DBenchmark()
		{
			stage.align = StageAlign.TOP_LEFT;
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.frameRate = 60;
			setupContext(Context3DRenderMode.AUTO);
		}
 
		private function setupContext(renderMode:String): void
		{
			driver.text = "Setting up context with render mode: " + renderMode;
			stage.stage3Ds[0].addEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			stage.stage3Ds[0].requestContext3D(renderMode);
		}
 
		protected function onContextCreated(ev:Event): void
		{
			var firstTime:Boolean = context3D == null;
 
			stage.stage3Ds[0].removeEventListener(Event.CONTEXT3D_CREATE, onContextCreated);
			context3D = stage.stage3Ds[0].context3D;			
			context3D.configureBackBuffer(stage.stageWidth, stage.stageHeight, 0, true);
 
			var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
			vertexShaderAssembler.assemble(
				Context3DProgramType.VERTEX, 
				"m44 op, va0, vc0\n" +
				"mov v0, va1\n" 
			);
			var fragmentShaderAssembler:AGALMiniAssembler= new AGALMiniAssembler(); 
			fragmentShaderAssembler.assemble(
				Context3DProgramType.FRAGMENT, 
				"mov oc, v0"  
			);
 
			program = context3D.createProgram();
			program.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode);
 
			driver.text = context3D.driverInfo;
 
			if (firstTime)
			{
				makeButtons("+Sides", "-Sides", "+Rows", "-Rows", "+Cols", "-Cols", "Toggle Hardware");
				stage.addEventListener(MouseEvent.CLICK, onStageClick);
 
				fps.autoSize = TextFieldAutoSize.LEFT;
				fps.text = "Getting FPS...";
				addChild(fps);
 
				driver.autoSize = TextFieldAutoSize.LEFT;
				driver.y = fps.height;
				addChild(driver);
 
				settings.autoSize = TextFieldAutoSize.LEFT;
				settings.y = driver.y + driver.height;
				addChild(settings);
			}
 
			makeCircles();
 
			if (firstTime)
			{
				help.autoSize = TextFieldAutoSize.LEFT;
				help.text = "Click stage to hide/show UI";
				help.y = settings.y + settings.height;
				addChild(help);
 
				addEventListener(Event.ENTER_FRAME, onEnterFrame);
				frameCount = 0;
				lastFPSUpdateTime = lastFrameTime = getTimer();
			}
		}
 
		private function onStageClick(ev:MouseEvent): void
		{
			if (ev.target is Stage)
			{
				for (var i:int; i < numChildren; ++i)
				{
					var child:DisplayObject = getChildAt(i);
					if (child != fps) child.visible = !child.visible;
				}
			}
		}
 
		private function makeButtons(...labels): void
		{
			const PAD:Number = 5;
 
			var curX:Number = 0;
			for each (var label:String in labels)
			{
				var tf:TextField = new TextField();
				tf.mouseEnabled = false;
				tf.selectable = false;
				tf.defaultTextFormat = new TextFormat("_sans", 16);
				tf.autoSize = TextFieldAutoSize.LEFT;
				tf.text = label;
				tf.name = "label";
 
				var button:Sprite = new Sprite();
				button.buttonMode = true;
				button.graphics.beginFill(0xffaaaaaa);
				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);
				button.x = curX;
				button.y = stage.stageHeight - button.height;
				addChild(button);
 
				curX += button.width + PAD;
			}
		}
 
		private function onButton(ev:MouseEvent): void
		{
			switch (ev.target.getChildByName("label").text)
			{
				case "+Sides":
					numSides++;
					makeCircles();
					break;
				case "-Sides":
					if (numSides > 3) numSides--;
					makeCircles();
					break;
				case "+Rows":
					numRows++;
					makeCircles();
					break;
				case "-Rows":
					if (numRows > 1) numRows--;
					makeCircles();
					break;
				case "+Cols":
					numCols++;
					makeCircles();
					break;
				case "-Cols":
					if (numCols > 1) numCols--;
					makeCircles();
					break;
				case "Toggle Hardware":
					var oldRenderMode:String = context3D.driverInfo;
					context3D.dispose();
					driver.text = "Toggling hardware...";
					setupContext(
						oldRenderMode.toLowerCase().indexOf("software") >= 0
							? Context3DRenderMode.AUTO
							: Context3DRenderMode.SOFTWARE
					);
					break;
			}
		}
 
		private function makeCircles(): void
		{
			const numVertices:uint = numSides + 1;
			numTris = numSides - 2;
 
			var posIndex:uint;
			var triIndex:uint;
 
			var vData:Vector.<Number> = new Vector.<Number>(numVertices * 6);
			var tris:Vector.<uint> = new Vector.<uint>(numTris * 3);
 
			var curTheta:Number = 0;
			const stepTheta:Number = (2.0*Math.PI) / numSides;
			for (var i:uint = 0; i < numVertices; ++i)
			{
				var cos:Number = Math.cos(curTheta) * 0.5;
				var sin:Number = Math.sin(curTheta) * 0.5;
 
				vData[posIndex++] = cos;
				vData[posIndex++] = sin;
				vData[posIndex++] = 0;
				vData[posIndex++] = 1;
				vData[posIndex++] = 1;
				vData[posIndex++] = 1;
 
				curTheta += stepTheta;
			}
			for (i = 0; i < numTris; ++i)
			{
				tris[triIndex++] = 0;
				tris[triIndex++] = i+1;
				tris[triIndex++] = i+2;
			}
 
			if (vertexBuffer)
			{
				vertexBuffer.dispose();
				indexBuffer.dispose();
			}
 
			vertexBuffer = context3D.createVertexBuffer(numVertices, 6);
			vertexBuffer.uploadFromVector(vData, 0, numVertices); 
 
			indexBuffer = context3D.createIndexBuffer(numTris*3);
			indexBuffer.uploadFromVector(tris, 0, numTris*3);
 
			settings.text = "Sides: " + numSides
				+ ", Tris: (" + numTris + " each, " + (numTris*numRows*numCols) + " total)"
				+ ", Rows: " + numRows
				+ ", Cols: " + numCols;
		}
 
		protected function onEnterFrame(ev:Event): void
		{
			if (!context3D)
			{
				return;
			}
 
			var t:Number = getTimer() / 1000;
			var scale:Number = 0.2 + Math.sin(t) * 0.2;
 
			context3D.clear(0.5, 0.5, 0.5);
			context3D.setProgram(program);
			context3D.setVertexBufferAt (0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3); 
			context3D.setVertexBufferAt(1, vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);
 
			var colSpacing:Number = 2.0 / (numCols+1);
			var rowSpacing:Number = 2.0 / (numRows+1);
			for (var row:int; row < numRows; ++row)
			{
				for (var col:int = 0; col < numCols; ++col)
				{
					modelMatrix.identity();
					modelMatrix.appendScale(scale, scale*(688/528), 1);			
					modelMatrix.appendRotation(t * 50, Vector3D.Z_AXIS);
					modelMatrix.appendTranslation(-1+colSpacing+col*colSpacing, -1+rowSpacing+row*rowSpacing, 0);
 
					context3D.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, modelMatrix, true);
					context3D.drawTriangles(indexBuffer, 0, numTris);
				}
			}
 
			context3D.present();
 
			frameCount++;
			var now:int = getTimer();
			var dTime:int = now - lastFrameTime;
			var elapsed:int = now - lastFPSUpdateTime;
			if (elapsed > 1000)
			{
				var framerateValue:Number = 1000 / (elapsed / frameCount);
				fps.text = "FPS: " + framerateValue.toFixed(4);
				lastFPSUpdateTime = now;
				frameCount = 0;
			}
			lastFrameTime = now;
		}
	}
}

The above app can be very useful for finding out a few things:

  • The driver the user is using (e.g. DirectX, OpenGL, Software and it’s blitting mode)
  • How many triangles you can draw at maximum
  • How many draw calls you can make at maximum

These are very useful during the planning phase of a game and to benchmark the performance of Stage3D on your target machines (you should have at least one!). You can also use it as a starting point to test out various Stage3D features in a simple environment. For example, you can replace the white-only shader with an experimental shader to see how much more expensive it is than drawing a solid color. In any case, happy benchmarking!

Spot a bug? Have a suggestion? Different results on a different OS or video card? Post a comment!