Simple Stage3D Benchmark
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:
- Launch Simple Stage3D Benchmark (VGA resolution = 640×480)
- Launch Simple Stage3D Benchmark (SVGA resolution = 800×600)
- Launch Simple Stage3D Benchmark (XGA resolution = 1024×768)
- Launch Simple Stage3D Benchmark (720p resolution = 1280×720)
- Launch Simple Stage3D Benchmark (1080p resolution = 1920×1080)
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!
#1 by Matan Uberstein on October 18th, 2011 ·
Thank you very much for sharing this. :) All works very well, while testing this I came across a very interesting thing. I have a 22″ screen so I can’t display the full 1080p version of the bench without browser scrollbars. I realized that when I scroll the FPS drops greatly, I wasn’t expecting this at all, as the rendering is handled via the GPU. I went on to test various browsers I have installed. Here are my findings (Note all while actively scrolling): Firefox – 30fps | Chrome – 7fps | Opera – 8fps. Conclusion, when the time comes and I want to use the GPU with Flash, I’ll make the scrollbars within Flash and cut out the browser all together, clearly the browser’s redraw is killing performance.
Out all of this I expected FF to perform the worst out the lot, definitely not the case. Let me know if you experience the same.
Thanks again!
#2 by jackson on October 18th, 2011 ·
Good idea about doing the scrolling in Flash, not the browser.
I tried out the scrolling on the same machine as in the article (Mac OS X 10.7, Core i5, GeForce GT 330M) and didn’t see any slowdown on Chrome or Firefox. Opera, however, was slow (30-40FPS) with the default settings even. This is sad since I can normally crank up to 50×50 in Chrome…
Thanks for the ideas. :)
#3 by Matan Uberstein on October 18th, 2011 ·
This gets even more interesting, seems like Mac’s redraw system works better. I should have added my system specs as well, the above was tested on Win7 x64 Ultimate, AMD Phenom II x6, XFX GeForce 8800GT. I’ll try and track down a high spec’d Ubuntu machine to see what it does. :)
Also, I tried running these tests on my Xoom tab and Desire HD phone, it just doesn’t show anything :( I can see the flash object, but it’s just blank. FYI, I have FP11 installed on both. :)
#4 by skyboy on October 21st, 2011 ·
I was pretty surprised to find that my CPU handles 1 triangle at exactly the same speed as 840 triangles in 3 circles: 35 FPS average, but 4 circles decreases that to 30 FPS. (640×480) So clearly the biggest drain is the clear method; If a program can avoid doing that, they can increase performance from 200% to 20%, depending. This particular app could benefit from caching the Vectors and using a lookup table for sin/cos. ;)
#5 by jackson on October 22nd, 2011 ·
Software renderers are almost always bound on fill rate (number of pixels drawn). Unfortunately, you have to clear the back buffer before drawing to it. It’s a shame, because this really is a big slowdown when you know you’re going to overwrite every pixel anyway. :(
#6 by Tamber on December 29th, 2011 ·
A simple and intellgniet point, well made. Thanks!