Working Around a Bug in Float Buffer

The reason for this isn't obvious at all. Our SpriteBatcher puts a float array into a direct ByteBuffer each frame when we call Vertices.setVertices(). The method boils down to calling FloatBuffer.put(float[]), and that's the culprit of our performance hit here. While desktop Java implements that FloatBuffer method via a real bulk memory move, the Harmony version calls FloatBuffer.put(float) for each element in the array. And that's extremely unfortunate, as that method is a JNI method, which has a lot of overhead (much like the OpenGL ES methods, which are also JNI methods).

There are a couple of solutions. IntBuffer.put(int[]) does not suffer from this problem, for example. We could replace the FloatBuffer in our Vertices class with an IntBuffer and modify Vertices.setVertices() so that it first transfers the floats from the float array to a temporary int array and then copies the contents of that int array to the IntBuffer. This solution was proposed by Ryan McNally, a fellow game developer, who also reported the bug on the Android bug tracker. It produces a five-times performance increase on the Hero, and a little less on other Android devices.

I modified the Vertices class to include this fix. For this I changed the vertices member to be an IntBuffer. I also added a new member called tmpBuffer, which is an int[] array. The tmpBuffer array is initialized in the constructor of Vertices as follows:

this.tmpBuffer = new int[maxVertices * vertexSize / 4];

We also get an IntBuffer view from the ByteBuffer in the constructor instead of a FloatBuffer:

vertices = buffer.asIntBuffer();

And the Vertices.setVertices() method looks like this now:

public void setVertices(float[] vertices, int offset, int length) { this.vertices.clear(); int len = offset + length; for(int i=offset, j=0; i < len; i++, j++)

tmpBuffer[j] = Float.floatToRawIntBits(vertices[i]); this.vertices.put(tmpBuffer, 0, length); this.vertices.flip();

So, all we do is first transfer the contents of the vertices parameter to the tmpBuffer. The static method Float.floatToRawIntBits() reinterprets the bit pattern of a float as an int. We then just need to copy the contents of the int array to the IntBuffer, formerly known as a FloatBuffer. Does it improve performance? Running the SpriteBatcherTest produces the following output now on the Hero, Droid, and Nexus One:

12-28 00:24:54.770: DEBUG/FPSCounter(2538): fps: 61

12-28 00:24:54.770: DEBUG/FPSCounter(2538): fps: 61

12-28 00:24:55.790: DEBUG/FPSCounter(2538): fps: 62

12-28 00:24:55.790: DEBUG/FPSCounter(2538): fps: 62

12-28 00:35:48.242: DEBUG/FPSCounter(1681): fps: 61

12-28 00:35:49.258: DEBUG/FPSCounter(1681): fps: 62

12-28 00:35:50.258: DEBUG/FPSCounter(1681): fps: 60

12-28 00:35:51.266: DEBUG/FPSCounter(1681): fps: 59

12-28 00:27:39.642: DEBUG/FPSCounter(1006): fps: 61 12-28 00:27:40.652: DEBUG/FPSCounter(1006): fps: 61 12-28 00:27:41.662: DEBUG/FPSCounter(1006): fps: 61 12-28 00:27:42.662: DEBUG/FPSCounter(1006): fps: 61

Yes, I double-checked; this is not a typo. The Hero really achieves 60 FPS now. A workaround consisting of five lines of code increases our performance by 50 percent. The Droid also benefited from this fix a little.

The problem is fixed in the latest release of Android version 2.3. However, it will be quite some time before most phones run this version, so we should keep this workaround for the time being.

NOTE: There's another, even faster workaround. It involves a custom JNI method that does the memory move in native code. You can find it if you search for the "Android Game Development Wiki" on the Net. I use this most of the time instead of the pure Java workaround. However, including JNI methods is a bit more complex, which is why I described the pure-Java workaround here.

0 0

Post a comment