ActionScript3 Number Buffers
For the young ones and the older nostalgics out there who want to know what it was like to program Java in the mid-to-late '90s, I can recommend a round of ActionScript 3. The VM it runs on, the ActionScript Virtual Machine[a], is slow at allocating objects and suffers from GC pauses, the collections API is slow and in general, it's just like pre-HotSpot[b] Java.
With that off my chest and out of the way, let's drag Flash a little bit closer to the millenium and hope it doesn't die from some unknown Y2K[c] bug.
1. The Problem
Flash is good at one thing - throwing polygons onto the screen. However, since Flash is bad at optimizing function calls, however, you should try to pack as many polygons into each throw as possible. The way to do this is to pass vertex buffers to the drawing API.
A vertex buffer is simply a Vector
of Number
s, and the problem is: How do we create this buffer fast enough?
2. Vector.<Number>
Let's start with the obvious method. We create the vector and push the vertices:
var v:Vector.<Number> ();
// 1st vertex
v.push (0, 0, 0);
// 2nd vertex
v.push (1, 0, 0);
// 3rd vertex
v.push (0, 1, 0);
drawTriangle (v);
This works, but it is slow. The Vector[d]
starts off with too little internal storage to hold all the coordinates, which means that the push
sometimes will result in an internal re-allocation and copy of the old buffer. We can do better if we pre-allocate the whole buffer in the constructor call:
var v:Vector.<Number> (9);
// 1st vertex
v[0] = 0; v[1] = 0; v[2] = 0;
// 2nd vertex
v[3] = 0; v[4] = 0; v[5] = 0;
// 3rd vertex
v[6] = 0; v[7] = 0; v[8] = 0;
drawTriangle (v);
This is a lot faster. To run, at least; you'll be paying in the time it takes to write all those indices. The obvious next step is to encapsulate this in a utility class.
3. NumberBuffer
We start by taking a look at Java's java.nio.Buffer[e]
API, and whip up a NumberBuffer
.
/**
* A fast vector of Numbers.
*
* @author Leo Sutic leo@sutic.nu
*/
public class NumberBuffer
{
public var buffer:Vector.<Number>;
public var length:int = 0;
public var read:int = 0;
public function NumberBuffer(size:int) {
buffer = new Vector.<Number> (size);
}
/**
* Reads the next Number from the buffer.
*
* @return the number at the read pointer
*/
public function next () : Number {
var n:Number = buffer[read];
++read;
return n;
}
/**
* Writes a single number to the buffer.
*/
public function write (n:Number) : void {
buffer[length] = n;
++length;
}
/**
* Copies the contents of another
* NumberBuffer into this buffer.
*
* @param source the buffer to copy from
* @param off the start offset in the source
* buffer to copy
* @param len the number of elements to copy
*/
public function copy (source:NumberBuffer,
off:int, len:int) : void {
var srcVec:Vector.<Number> = source.buffer;
var dstVec:Vector.<Number> = buffer;
var end:int = off + len;
for (var i:int = off; i < end; ++i) {
dstVec[length] = srcVec[i];
++length;
}
}
/**
* Resets the buffer without affecting the
* underlying Vector.
*/
public function reset () : void {
length = 0;
read = 0;
}
/**
* Sets the read pointer to 0.
*/
public function beginRead () : void {
read = 0;
}
}
4. NumberBufferStack
Finally, to get rid of the object allocation cost as we allocate and use buffers on each frame, we will borrow a trick from games programming - the per-frame stack allocator[f][1]. The idea here is that we start each frame with no buffers in use, and at the end of each frame we have also no buffers in use. The time each buffer is used is bounded by the beginning of frame rendering and the end. Therefore we can maintain a list of buffers and just hand them out in order throughout the frame rendering. At the start of the render, we hand out buffer[0]
, then buffer[1]
up to buffer[n]
. Since we render a lot of pixels with each buffer, n
will be small enough to be manageable. At the end of each frame, we simply reset the "next buffer to hand out"-pointer to zero; and we're ready for the next frame. The result is code that looks like this:
function beginFrame () : void {
...
}
function renderFrame () : void {
var b:NumberBuffer = pool.get ();
// Use b, don't dispose it
...
// Use another buffer
var b2:NumberBuffer = pool.get ();
// Use b and b2, don't dispose
...
}
function endFrame () : void {
// Dispose *all* used
// buffers in one go.
pool.disposeAll ();
}
Here's the implementation:
/**
* A stack allocator for number buffers.
*
* @author Leo Sutic leo@sutic.nu
*/
public class NumberBufferStack
{
/**
* The pool of NumberBuffers.
*/
private var pool:Vector.<NumberBuffer>
= new Vector.<NumberBuffer> ();
/**
* Number of NumberBuffers in the
* pool vector.
*/
private var inPool:int = 0;
/**
* Number of NumberBuffers from the
* pool vector that have been given
* out.
*/
private var poolUsed:int = 0;
/**
* Initial buffer size when creating
* new NumberBuffers.
*/
private var initBufferSize:int;
/**
* Creates a new stack.
*
* @param initBufferSize initial buffer
* size for the NumberBuffers that are
* created as needed.
*/
public function NumberBufferStack (
initBufferSize:int) {
this.initBufferSize = initBufferSize;
}
/**
* Obtains a new number buffer from this
* stack.
* @return an empty NumberBuffer
*/
public function getNumberBuffer ()
: NumberBuffer {
var n:NumberBuffer = null;
if (inPool == poolUsed) {
n = new NumberBuffer (initBufferSize);
pool[inPool] = n;
++inPool;
++poolUsed;
return n;
}
n = pool[poolUsed];
n.reset ();
++poolUsed;
return n;
}
/**
* Deallocates all buffers that have been
* given out.
*/
public function disposeAll () : void {
poolUsed = 0;
}
}
4.1. For Large Values of n
For large values of n
, a mark
/ reset
mechanism can be implemented. The mark
function will push the current poolUsed
value onto a stack, and reset
pops the value back. This has the effect of disposing all buffers since the last call to mark
.
Footnotes
[1] | Scope Stack Allocation by Andreas Fredriksson of the Frostbite[g] team at DICE[h]. The presentation is interesting; if you're into games or embedded programming, you should take five minutes and read it. |
Links
http://www-archive.mozilla.org/projects/tamarin/ | |
http://www.oracle.com/technetwork/java/javase/tech/index-jsp-136373.html | |
http://en.wikipedia.org/wiki/Year_2000_problem | |
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/Vector.html | |
http://docs.oracle.com/javase/6/docs/api/java/nio/Buffer.html | |
http://www.slideshare.net/DICEStudio/scope-stack-allocation | |
http://dice.se/frostbite | |
http://dice.se/ |