add OpelGl performance test as a separate package
Currently runs two types of tests
1. rendering performance comparison with VBO vs without VBO
   This can also work as a stress test due to the amount of triangles used.

2. rendering performance comparison with varying numbers of index buffers
   This is for bug 5660942

Change-Id: I8a7d05cf0bfaa7063302efdd7fb24519d3a3c5bf
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 3ccf169..c0be204 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -48,6 +48,7 @@
 	CtsMediaTestCases \
 	CtsNdefTestCases \
 	CtsNetTestCases \
+	CtsOpenGlPerfTestCases \
 	CtsOsTestCases \
 	CtsPermissionTestCases \
 	CtsPermission2TestCases \
diff --git a/tests/tests/openglperf/Android.mk b/tests/tests/openglperf/Android.mk
new file mode 100644
index 0000000..0663c73
--- /dev/null
+++ b/tests/tests/openglperf/Android.mk
@@ -0,0 +1,34 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# don't include this package in any target
+LOCAL_MODULE_TAGS := optional
+# and when built explicitly put it in the data partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsOpenGlPerfTestCases
+
+# uncomment when dalvik.annotation.Test* are removed or part of SDK
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
diff --git a/tests/tests/openglperf/AndroidManifest.xml b/tests/tests/openglperf/AndroidManifest.xml
new file mode 100644
index 0000000..95a7cd1
--- /dev/null
+++ b/tests/tests/openglperf/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.openglperf"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+    <instrumentation
+        android:targetPackage="com.android.cts.openglperf"
+        android:name="android.test.InstrumentationTestRunner" />
+    <application
+        android:label="@string/app_name" >
+        <uses-library android:name="android.test.runner" />
+        <activity
+            android:name="android.openglperf.cts.GlPlanetsActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/tests/openglperf/assets/world_512_512.jpg b/tests/tests/openglperf/assets/world_512_512.jpg
new file mode 100644
index 0000000..b92bfae
--- /dev/null
+++ b/tests/tests/openglperf/assets/world_512_512.jpg
Binary files differ
diff --git a/tests/tests/openglperf/res/values/strings.xml b/tests/tests/openglperf/res/values/strings.xml
new file mode 100644
index 0000000..2cdba7f
--- /dev/null
+++ b/tests/tests/openglperf/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <string name="app_name">OpenGlPerfTest</string>
+</resources>
\ No newline at end of file
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/GlPlanetsActivity.java b/tests/tests/openglperf/src/android/openglperf/cts/GlPlanetsActivity.java
new file mode 100644
index 0000000..5a8c0a86
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/GlPlanetsActivity.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity which runs GL test and measure FPS with given parameters passed from
+ * Intent. For the meaning of parameters, check {@PlanetsRenderingParam}
+ */
+public class GlPlanetsActivity extends Activity implements
+        RenderCompletionListener {
+    public static final String INTENT_EXTRA_NUM_PLANETS = "numPlanets";
+    public static final String INTENT_EXTRA_USE_VBO_VERTICES = "useVboVertices";
+    public static final String INTENT_EXTRA_USE_VBO_INDICES = "useVboIndiices";
+    public static final String INTENT_EXTRA_NUM_FRAMES = "numFrames";
+    public static final String INTENT_EXTRA_INDICES_PER_VERTEX = "numIndicesPerVertex";
+
+    public static final String INTENT_RESULT_FPS = "fps";
+    public static final String INTENT_RESULT_NUM_TRIANGLES = "numTrigngles";
+
+    private final Semaphore mSem = new Semaphore(0);
+    private float mFps;
+    private int mNumTriangles;
+
+    private PlanetsSurfaceView mView;
+
+    public boolean waitForGlPlanetsCompletionWithTimeout(long timeoutInSecs)
+            throws InterruptedException {
+        return mSem.tryAcquire(timeoutInSecs, TimeUnit.SECONDS);
+    }
+
+    public float getFps() {
+        return mFps;
+    }
+
+    public int getNumTriangles() {
+        return mNumTriangles;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+        Intent intent = getIntent();
+        PlanetsRenderingParam param = new PlanetsRenderingParam();
+        param.mNumPlanets = intent.getIntExtra(INTENT_EXTRA_NUM_PLANETS, param.mNumPlanets);
+        param.mUseVboForVertices = intent.getBooleanExtra(INTENT_EXTRA_USE_VBO_VERTICES,
+                param.mUseVboForVertices);
+        param.mUseVboForIndices = intent.getBooleanExtra(INTENT_EXTRA_USE_VBO_INDICES,
+                param.mUseVboForIndices);
+        param.mNumFrames = intent.getIntExtra(INTENT_EXTRA_NUM_FRAMES, param.mNumFrames);
+        param.mNumIndicesPerVertex = intent.getIntExtra(INTENT_EXTRA_INDICES_PER_VERTEX,
+                param.mNumIndicesPerVertex);
+        mView = new PlanetsSurfaceView(this, param, this);
+        setContentView(mView);
+    }
+
+    @Override
+    public void onRenderCompletion(float fps, int numTriangles) {
+        mFps = fps;
+        mNumTriangles = numTriangles;
+        mSem.release();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mView.onResume();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mView.onPause();
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/GlVboPerfTest.java b/tests/tests/openglperf/src/android/openglperf/cts/GlVboPerfTest.java
new file mode 100644
index 0000000..afd435c
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/GlVboPerfTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+public class GlVboPerfTest extends
+        ActivityInstrumentationTestCase2<GlPlanetsActivity> {
+    private static final String TAG = "GlVboPerfTest";
+    private static final int NUM_FRAMES_TO_RENDER = 100;
+    private static final long RENDERING_TIMEOUT = 5 * 60;
+    // 10% of fps_no_vbo is allowed to compensate variations in measurement
+    private static final float FPS_COMPARISON_MARGIN = 0.1f;
+    // the worst case should be above 70% of the best case
+    private static final float FPS_MIN_MAX_COMPARISON_PERCENTILE = 0.7f;
+
+    private float mFps;
+    private int mNumTriangles;
+
+    public GlVboPerfTest() {
+        super(GlPlanetsActivity.class);
+    }
+
+    public void testVboWithVaryingIndexBufferNumbers() throws Exception {
+        final int[] numIndexBuffers = {1, 10, 100, 200, 400}; // per vertex buffer
+        float[] fpsVbo = new float[numIndexBuffers.length];
+        float[] fpsNonVbo = new float[numIndexBuffers.length];
+
+        for (int i = 0; i < numIndexBuffers.length; i++) {
+            runRendering(0, true, true, numIndexBuffers[i]);
+            fpsVbo[i] = mFps;
+            runRendering(0, true, false, numIndexBuffers[i]);
+            fpsNonVbo[i] = mFps;
+        }
+        StringBuilder msgIndex = new StringBuilder();
+        StringBuilder msgVbo = new StringBuilder();
+        StringBuilder msgNonVbo = new StringBuilder();
+        msgIndex.append("index buffer ");
+        msgVbo.append("Vbo ");
+        msgNonVbo.append("Non-Vbo ");
+        for (int i = 0; i < numIndexBuffers.length; i++) {
+            msgIndex.append(numIndexBuffers[i]).append(" ");
+            msgVbo.append(fpsVbo[i]).append(" ");
+            msgNonVbo.append(fpsNonVbo[i]).append(" ");
+        }
+        Log.i(TAG, msgIndex.toString());
+        Log.i(TAG, msgVbo.toString());
+        Log.i(TAG, msgNonVbo.toString());
+
+        float[] minMaxVbo = findMinMax(fpsVbo);
+        float[] minMaxNonVbo = findMinMax(fpsNonVbo);
+
+        float delta = minMaxVbo[1] - (1f - FPS_COMPARISON_MARGIN)
+                * minMaxNonVbo[1];
+        assertTrue("VBO performance worse than non-VBO " + msgVbo + msgNonVbo, delta > 0f);
+        assertTrue(
+                "Too much FPS drop for VBO case " + msgVbo,
+                minMaxVbo[0] > (FPS_MIN_MAX_COMPARISON_PERCENTILE * minMaxVbo[1]));
+        assertTrue(
+                "Too much FPS drop for No VBO case " + msgNonVbo,
+                minMaxNonVbo[0] > (FPS_MIN_MAX_COMPARISON_PERCENTILE * minMaxNonVbo[1]));
+    }
+
+    public void testVboVsNonVboPerfGeometry0() throws Exception {
+        doRunVboVsNonVboPerfTest(0);
+    }
+
+    public void testVboVsNonVboPerfGeometry1() throws Exception {
+        doRunVboVsNonVboPerfTest(4);
+    }
+
+    private void runRendering(int numPlanets, boolean useVboVertex, boolean useVboIndex,
+            int indicesPerVertex) throws Exception {
+        Intent intent = new Intent();
+        intent.putExtra(GlPlanetsActivity.INTENT_EXTRA_NUM_FRAMES,
+                NUM_FRAMES_TO_RENDER);
+        intent.putExtra(GlPlanetsActivity.INTENT_EXTRA_NUM_PLANETS, numPlanets);
+        intent.putExtra(GlPlanetsActivity.INTENT_EXTRA_USE_VBO_VERTICES, useVboVertex);
+        intent.putExtra(GlPlanetsActivity.INTENT_EXTRA_USE_VBO_INDICES, useVboIndex);
+        intent.putExtra(GlPlanetsActivity.INTENT_EXTRA_INDICES_PER_VERTEX, indicesPerVertex);
+
+        setActivityIntent(intent);
+        final GlPlanetsActivity activity = getActivity();
+        boolean waitResult = activity
+                .waitForGlPlanetsCompletionWithTimeout(RENDERING_TIMEOUT);
+        assertTrue("timeout while waiting for rendering completion", waitResult);
+
+        mFps = activity.getFps();
+        mNumTriangles = activity.getNumTriangles();
+
+        cleanUpActivity();
+    }
+
+    private void cleanUpActivity()  throws Exception {
+        // finish the current activity and do clean-up so that a new activity
+        // can be launched in the same test run
+        super.tearDown();
+        super.setUp();
+        // wait until clean-up / set-up finishes
+        getInstrumentation().waitForIdleSync();
+    }
+
+    private void doRunVboVsNonVboPerfTest(int numPlanets) throws Exception {
+        runRendering(numPlanets, true, true, 1); // VBO
+        int numTrianglesVbo = mNumTriangles;
+        float fpsVbo = mFps;
+        runRendering(numPlanets, false, false, 1); // non-VBO
+
+        assertTrue("Number of triangles mismatch",
+                numTrianglesVbo == mNumTriangles);
+
+        // Margin amount of error is allowed due to measuring irregularity
+        float delta = fpsVbo - (1f - FPS_COMPARISON_MARGIN) * mFps;
+        StringBuilder testMsg = new StringBuilder();
+        testMsg.append("VBO performance worse than non-VBO ").append(fpsVbo).append(" ");
+        testMsg.append(mFps);
+        assertTrue(testMsg.toString(), delta > 0f);
+    }
+
+    private float[] findMinMax(float[] data) {
+        float min = data[0];
+        float max = data[0];
+
+        for (int i = 1; i < data.length; i++) {
+            if (data[i] > max) max = data[i];
+            if (data[i] < min) min = data[i];
+        }
+        float[] result = {min, max};
+        return result;
+    }
+}
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderer.java b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderer.java
new file mode 100644
index 0000000..59a7782
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderer.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.System;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+
+public class PlanetsRenderer implements GLSurfaceView.Renderer {
+
+    private static final String TAG = "PlanetsRenderer";
+    // texture is from
+    // http://en.wikipedia.org/wiki/File:Mercator_projection_SW.jpg
+    private static final String TEXTURE_FILE = "world_512_512.jpg";
+
+    private final Context mContext;
+    private final PlanetsRenderingParam mParam;
+    private final RenderCompletionListener mListener;
+
+    private final Sphere[] mSpheres;
+    private final int mNumSpheres;
+    private final int mNumIndices;
+    private final int mVboVertices[];
+    private final int mVboIndices[];
+
+    // configurations for sun and planets
+    private static final int SPHERE_SLICES = 180;
+    private static final float RADIUS_SUN = 0.4f;
+    private static final float RADIUS_PLANET = 0.08f;
+    private static final float RADIUS_ORBIT = 0.9f;
+
+    private int mWidth;
+    private int mHeight;
+
+    private int mFrameCount = 0;
+    private static final int FPS_DISPLAY_INTERVAL = 50;
+    private long mLastFPSTime;
+    // for total FPS measurement
+    private long mRenderingStartTime;
+    private long mMeasurementStartTime;
+
+    private int mProgram; // shader program
+    private int mMVPMatrixHandle;
+    private float[] mMVPMatrix = new float[16];
+    private float[] mMMatrix = new float[16];
+    private float[] mVMatrix = new float[16];
+    private float[] mProjMatrix = new float[16];
+
+    private int mOffsetHandle;
+    private static final float[] mDefaultOffset = { 0f, 0f, 0f, 1f };
+    private int mPositionHandle;
+    private int mTexCoord0Handle;
+    private int mTextureHandle;
+    private int mTextureId;
+
+    /**
+     * @param numSlices
+     *            complexity of sphere used. A sphere will have (numSlices + 1)
+     *            x (numSlices x 1) much of vertices
+     * @param useVbo
+     *            whether to use Vertex Buffer Object in rendering or not
+     * @param framesToGo
+     *            number of frames to render before calling completion to
+     *            listener
+     * @param listener
+     */
+    public PlanetsRenderer(Context context, PlanetsRenderingParam param,
+            RenderCompletionListener listener) {
+        resetTimer();
+        mContext = context;
+        mParam = param;
+        mNumSpheres = mParam.mNumPlanets + 1; // 1 for sun
+        mNumIndices = mNumSpheres * mParam.mNumIndicesPerVertex;
+        mSpheres = new Sphere[mNumSpheres];
+
+        printParams();
+
+        // for big model, this construction phase takes time...
+        mSpheres[0] = new Sphere(SPHERE_SLICES, 0f, 0f, 0f, RADIUS_SUN,
+                mParam.mNumIndicesPerVertex);
+        for (int i = 1; i < mNumSpheres; i++) {
+            mSpheres[i] = new Sphere(SPHERE_SLICES,
+                    RADIUS_ORBIT * (float) Math.sin(((float) i) / (mNumSpheres - 1) * 2 * Math.PI),
+                    RADIUS_ORBIT * (float) Math.cos(((float) i) / (mNumSpheres - 1) * 2 * Math.PI),
+                    0f, RADIUS_PLANET, mParam.mNumIndicesPerVertex);
+        }
+        mVboVertices = new int[mNumSpheres];
+        mVboIndices = new int[mNumIndices];
+        mListener = listener;
+        measureTime("construction");
+    }
+
+    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
+        mProgram = createProgram(getVertexShader(), getFragmentShader());
+        if (mProgram == 0) {
+            // error, cannot proceed
+            throw new IllegalStateException("createProgram failed");
+        }
+        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
+        mOffsetHandle = GLES20.glGetUniformLocation(mProgram, "uOffset");
+        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+        mTexCoord0Handle = GLES20.glGetAttribLocation(mProgram, "vTexCoord0");
+        mTextureHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
+
+        // Load the texture
+        mTextureId = createTexture2D();
+    }
+
+    public void onDrawFrame(GL10 glUnused) {
+        long currentTime = System.currentTimeMillis();
+
+        mFrameCount++;
+        if ((mFrameCount % FPS_DISPLAY_INTERVAL == 0) && (mFrameCount != 0)) {
+            float fps = (((float) FPS_DISPLAY_INTERVAL)
+                    / ((float) (currentTime - mLastFPSTime)) * 1000.0f);
+            Log.i(TAG, "FPS " + fps);
+            mLastFPSTime = currentTime;
+        }
+
+        if ((mFrameCount == mParam.mNumFrames) && (mParam.mNumFrames > 0)) {
+            long timePassed = currentTime - mRenderingStartTime;
+            float fps = ((float) mParam.mNumFrames) / ((float) timePassed) * 1000.0f;
+            printGlInfos();
+            printParams();
+            int numTriangles = mNumSpheres * mSpheres[0].getTotalIndices() / 3;
+            Log.i(TAG, "Final FPS " + fps + " Num triangles " + numTriangles);
+            if (mListener != null) {
+                mListener.onRenderCompletion(fps, numTriangles);
+                return;
+            }
+        }
+
+        float angle = 0.090f * ((int) (currentTime % 4000L));
+        Matrix.setRotateM(mMMatrix, 0, angle, 0, 0, 1.0f);
+        Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0);
+        Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
+
+        GLES20.glUseProgram(mProgram);
+        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
+        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
+
+        // Apply a ModelView Projection transformation
+        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
+        GLES20.glUniform4f(mOffsetHandle, mDefaultOffset[0], mDefaultOffset[1],
+                mDefaultOffset[2], mDefaultOffset[3]);
+
+        GLES20.glEnableVertexAttribArray(mPositionHandle);
+        GLES20.glEnableVertexAttribArray(mTexCoord0Handle);
+
+        // Bind the texture
+        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
+        // Set the sampler texture unit to 0
+        GLES20.glUniform1i(mTextureHandle, 0);
+
+        for (int i = 0; i < mNumSpheres; i++) {
+            if (mParam.mUseVboForVertices) {
+                // generating VBOs for each sphere is not efficient way for drawing
+                // multiple spheres
+                // But this is done for testing performance with big VBO buffers.
+                // So please do not copy this code as it is.
+                GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboVertices[i]);
+                // Load the vertex position
+                GLES20.glVertexAttribPointer(mPositionHandle, 3,
+                        GLES20.GL_FLOAT, false, mSpheres[i].getVeticesStride(),
+                        0);
+                // Load the texture coordinate
+                GLES20.glVertexAttribPointer(mTexCoord0Handle, 3,
+                        GLES20.GL_FLOAT, false, mSpheres[i].getVeticesStride(),
+                        3 * Sphere.FLOAT_SIZE);
+            } else {
+                // Load the vertex position
+                GLES20.glVertexAttribPointer(mPositionHandle, 3,
+                        GLES20.GL_FLOAT, false, mSpheres[i].getVeticesStride(),
+                        mSpheres[i].getVertices());
+                // Load the texture coordinate
+                GLES20.glVertexAttribPointer(mTexCoord0Handle, 3,
+                        GLES20.GL_FLOAT, false, mSpheres[i].getVeticesStride(),
+                        mSpheres[i].getVertices().duplicate().position(3));
+            }
+            int[] numIndices = mSpheres[i].getNumIndices();
+            ShortBuffer[] indices = mSpheres[i].getIndices();
+            if (mParam.mUseVboForIndices) {
+                int indexVboBase = i * mParam.mNumIndicesPerVertex;
+                for (int j = 0; j < numIndices.length; j++) {
+                    GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER,
+                            mVboIndices[indexVboBase + j]);
+                    GLES20.glDrawElements(GLES20.GL_TRIANGLES,
+                            numIndices[j], GLES20.GL_UNSIGNED_SHORT,
+                            0);
+                }
+            } else {
+                for (int j = 0; j < numIndices.length; j++) {
+                    GLES20.glDrawElements(GLES20.GL_TRIANGLES,
+                            numIndices[j], GLES20.GL_UNSIGNED_SHORT,
+                            indices[j]);
+                }
+            }
+        }
+    }
+
+    public void onSurfaceChanged(GL10 glUnused, int width, int height) {
+        mWidth = width;
+        mHeight = height;
+        GLES20.glViewport(0, 0, width, height);
+        float ratio = (float) width / height;
+        Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
+        Matrix.setLookAtM(mVMatrix, 0, 0, 3, 3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
+
+        createVbo();
+
+        // reset timer to remove delays added for FPS calculation.
+        mLastFPSTime = System.currentTimeMillis();
+        mRenderingStartTime = System.currentTimeMillis();
+    }
+
+    protected final String getVertexShader() {
+        // simple shader with MVP matrix and text coord
+        final String vShaderStr =
+                  "uniform mat4 uMVPMatrix;                               \n"
+                + "uniform vec4 uOffset;                                  \n"
+                + "attribute vec4 vPosition;                              \n"
+                + "attribute vec2 vTexCoord0;                             \n"
+                + "varying vec2 vTexCoord;                                \n"
+                + "void main()                                            \n"
+                + "{                                                      \n"
+                + "   gl_Position = uMVPMatrix * (vPosition + uOffset);   \n"
+                + "   vTexCoord = vTexCoord0;                             \n"
+                + "}                                                      \n";
+        return vShaderStr;
+    }
+
+    protected final String getFragmentShader() {
+        // simple shader with one texture for color
+        final String fShaderStr =
+                  "precision mediump float;                          \n"
+                + "varying vec2 vTexCoord;                           \n"
+                + "uniform sampler2D sTexture;                       \n"
+                + "void main()                                       \n"
+                + "{                                                 \n"
+                + "  gl_FragColor = texture2D( sTexture, vTexCoord );\n"
+                + "}                                                 \n";
+        return fShaderStr;
+    }
+
+    private int loadShader(int shaderType, String source) {
+        int shader = GLES20.glCreateShader(shaderType);
+        if (shader != 0) {
+            GLES20.glShaderSource(shader, source);
+            GLES20.glCompileShader(shader);
+            int[] compiled = new int[1];
+            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+            if (compiled[0] == 0) {
+                Log.e(TAG, "Could not compile shader " + shaderType + ":");
+                Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
+                GLES20.glDeleteShader(shader);
+                shader = 0;
+            }
+        }
+        return shader;
+    }
+
+    private int createProgram(String vertexSource, String fragmentSource) {
+        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+        if (vertexShader == 0) {
+            return 0;
+        }
+
+        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+        if (pixelShader == 0) {
+            return 0;
+        }
+
+        int program = GLES20.glCreateProgram();
+        if (program != 0) {
+            GLES20.glAttachShader(program, vertexShader);
+            checkGlError("glAttachShader");
+            GLES20.glAttachShader(program, pixelShader);
+            checkGlError("glAttachShader");
+            GLES20.glLinkProgram(program);
+            int[] linkStatus = new int[1];
+            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+            if (linkStatus[0] != GLES20.GL_TRUE) {
+                Log.e(TAG, "Could not link program: ");
+                Log.e(TAG, GLES20.glGetProgramInfoLog(program));
+                GLES20.glDeleteProgram(program);
+                program = 0;
+            }
+        }
+        return program;
+    }
+
+    private int createTexture2D() {
+        // Texture object handle
+        int[] textureId = new int[1];
+
+        InputStream in = null;
+        try {
+            in = mContext.getAssets().open(TEXTURE_FILE);
+            Bitmap bitmap = BitmapFactory.decodeStream(in);
+
+            // Generate a texture object
+            GLES20.glGenTextures(1, textureId, 0);
+
+            // Bind the texture object
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
+            GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
+
+            // Set the filtering mode
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+                    GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+                    GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+
+        } catch (IOException e) {
+            throw new IllegalStateException("Couldn't load texture '" + TEXTURE_FILE
+                    + "'", e);
+        } finally {
+            if (in != null)
+                try {
+                    in.close();
+                } catch (IOException e) {
+                }
+        }
+
+        return textureId[0];
+    }
+
+    private void checkGlError(String op) {
+        int error;
+        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+            Log.e(TAG, op + ": glError " + error);
+            throw new IllegalStateException(op + ": glError " + error);
+        }
+    }
+
+    private void createVbo() {
+        resetTimer();
+        if (mParam.mUseVboForVertices) {
+            GLES20.glGenBuffers(mNumSpheres, mVboVertices, 0);
+            checkGlError("glGenBuffers Vertex");
+            for (int i = 0; i < mNumSpheres; i++) {
+                GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboVertices[i]);
+                FloatBuffer vertices = mSpheres[i].getVertices();
+                GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.limit()
+                        * Sphere.FLOAT_SIZE, vertices, GLES20.GL_STATIC_DRAW);
+                checkGlError("glBufferData Vertex");
+                mSpheres[i].releaseVertexBuffer(); // release memory
+            }
+        }
+        if (mParam.mUseVboForIndices) {
+            GLES20.glGenBuffers(mNumIndices, mVboIndices, 0);
+            checkGlError("glGenBuffers Index");
+            for (int i = 0; i < mNumSpheres; i++) {
+                int[] numIndices = mSpheres[i].getNumIndices();
+                ShortBuffer[] indices = mSpheres[i].getIndices();
+                int indexVboBase = i * mParam.mNumIndicesPerVertex;
+                for (int j = 0; j < numIndices.length; j++) {
+                    GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER,
+                            mVboIndices[indexVboBase + j]);
+                    GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER,
+                            indices[j].limit() * Sphere.SHORT_SIZE, indices[j],
+                            GLES20.GL_STATIC_DRAW);
+                    checkGlError("glBufferData Index");
+
+                }
+                mSpheres[i].releaseIndexBuffer(); // release memory
+            }
+        }
+        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
+        measureTime("VBO creation");
+    }
+
+    private void resetTimer() {
+        mMeasurementStartTime = System.currentTimeMillis();
+    }
+
+    private void measureTime(String description) {
+        long currentTime = System.currentTimeMillis();
+        float timePassedInSecs = (float) (currentTime - mMeasurementStartTime) / 1000f;
+        Log.i(TAG, description + " time in secs: " + timePassedInSecs);
+    }
+
+    private void printGlInfos() {
+        Log.i(TAG, "Vendor " + GLES20.glGetString(GLES20.GL_VENDOR));
+        Log.i(TAG, "Version " + GLES20.glGetString(GLES20.GL_VERSION));
+        Log.i(TAG, "Renderer " + GLES20.glGetString(GLES20.GL_RENDERER));
+        Log.i(TAG, "Extensions " + GLES20.glGetString(GLES20.GL_EXTENSIONS));
+    }
+    private void printParams() {
+        Log.i(TAG, "UseVboForVertex " + mParam.mUseVboForVertices);
+        Log.i(TAG, "UseVboForIndex " + mParam.mUseVboForIndices);
+        Log.i(TAG, "No Spheres " + mNumSpheres);
+        Log.i(TAG, "No indices buffer per vertex " + mParam.mNumIndicesPerVertex);
+    }
+}
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderingParam.java b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderingParam.java
new file mode 100644
index 0000000..5e2dfd0
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsRenderingParam.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+public class PlanetsRenderingParam {
+    /** whether to use VBO for vertices data */
+    public boolean mUseVboForVertices = true;
+    /** whether to use VBO for indices data */
+    public boolean mUseVboForIndices = true;
+    /** number of indices buffer per one vertices buffer */
+    public int mNumIndicesPerVertex = 1;
+    /** number of planets to render. There is always a sun */
+    public int mNumPlanets = 0;
+    /** number of frames to render to calculate FPS and finish */
+    public int mNumFrames = 100;
+}
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/PlanetsSurfaceView.java b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsSurfaceView.java
new file mode 100644
index 0000000..526f706
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/PlanetsSurfaceView.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+import android.content.Context;
+import android.opengl.GLSurfaceView;
+
+class PlanetsSurfaceView extends GLSurfaceView {
+
+    public PlanetsSurfaceView(Context context, PlanetsRenderingParam param,
+            RenderCompletionListener listener) {
+        super(context);
+
+        setEGLContextClientVersion(2);
+        setRenderer(new PlanetsRenderer(context, param, listener));
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        setRenderMode(RENDERMODE_WHEN_DIRTY);
+    }
+
+    @Override
+    public void onResume() {
+        setRenderMode(RENDERMODE_CONTINUOUSLY);
+        super.onResume();
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/RenderCompletionListener.java b/tests/tests/openglperf/src/android/openglperf/cts/RenderCompletionListener.java
new file mode 100644
index 0000000..1643087e
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/RenderCompletionListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+public interface RenderCompletionListener {
+    /**
+     * @param fps total frame rate
+     * @param numTriangles Number of triangles in geometric model
+     */
+    void onRenderCompletion(float fps, int numTriangles);
+
+}
diff --git a/tests/tests/openglperf/src/android/openglperf/cts/Sphere.java b/tests/tests/openglperf/src/android/openglperf/cts/Sphere.java
new file mode 100644
index 0000000..e8876dd
--- /dev/null
+++ b/tests/tests/openglperf/src/android/openglperf/cts/Sphere.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.openglperf.cts;
+
+import java.lang.Math;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+/*
+ * Class for generating a sphere model for given input params
+ * The generated class will have vertices and indices
+ * Vertices data is composed of vertex coordinates in x, y, z followed by
+ *  texture coordinates s, t for each vertex
+ * Indices store vertex indices for the whole sphere.
+ * Formula for generating sphere is originally coming from source code of
+ * OpenGL ES2.0 Programming guide
+ * which is available from http://code.google.com/p/opengles-book-samples/,
+ * but some changes were made to make texture look right.
+ */
+public class Sphere {
+    public static final int FLOAT_SIZE = 4;
+    public static final int SHORT_SIZE = 2;
+
+    private FloatBuffer mVertices;
+    private ShortBuffer[] mIndices;
+    private int[] mNumIndices;
+    private int mTotalIndices;
+
+    /*
+     * @param nSlices how many slice in horizontal direction.
+     *                The same slice for vertical direction is applied.
+     *                nSlices should be > 1 and should be <= 180
+     * @param x,y,z the origin of the sphere
+     * @param r the radius of the sphere
+     */
+    public Sphere(int nSlices, float x, float y, float z, float r, int indicesPerVertex) {
+
+        int iMax = nSlices + 1;
+        int nVertices = iMax * iMax;
+        if (nVertices > Short.MAX_VALUE) {
+            // this cannot be handled in one vertices / indices pair
+            throw new RuntimeException("nSlices " + nSlices + " too big for vertex");
+        }
+        mTotalIndices = nSlices * nSlices * 6;
+        float angleStepI = ((float) Math.PI / nSlices);
+        float angleStepJ = ((2.0f * (float) Math.PI) / nSlices);
+
+        // 3 vertex coords + 2 texture coords
+        mVertices = ByteBuffer.allocateDirect(nVertices * 5 * FLOAT_SIZE)
+                .order(ByteOrder.nativeOrder()).asFloatBuffer();
+        mIndices = new ShortBuffer[indicesPerVertex];
+        mNumIndices = new int[indicesPerVertex];
+        // first evenly distribute to n-1 buffers, then put remaining ones to the last one.
+        int noIndicesPerBuffer = (mTotalIndices / indicesPerVertex / 6) * 6;
+        for (int i = 0; i < indicesPerVertex - 1; i++) {
+            mNumIndices[i] = noIndicesPerBuffer;
+        }
+        mNumIndices[indicesPerVertex - 1] = mTotalIndices - noIndicesPerBuffer *
+                (indicesPerVertex - 1);
+
+        for (int i = 0; i < indicesPerVertex; i++) {
+            mIndices[i] = ByteBuffer.allocateDirect(mNumIndices[i] * SHORT_SIZE)
+                    .order(ByteOrder.nativeOrder()).asShortBuffer();
+        }
+        // calling put for each float took too much CPU time, so put by line instead
+        float[] vLineBuffer = new float[iMax * 5];
+        for (int i = 0; i < iMax; i++) {
+            for (int j = 0; j < iMax; j++) {
+                int vertexBase = j * 5;
+                float sini = (float) Math.sin(angleStepI * i);
+                float sinj = (float) Math.sin(angleStepJ * j);
+                float cosi = (float) Math.cos(angleStepI * i);
+                float cosj = (float) Math.cos(angleStepJ * j);
+                // vertex x,y,z
+                vLineBuffer[vertexBase + 0] = x + r * sini * sinj;
+                vLineBuffer[vertexBase + 1] = y + r * sini * cosj;
+                vLineBuffer[vertexBase + 2] = z + r * cosi;
+                // texture s,t
+                vLineBuffer[vertexBase + 3] = (float) j / (float) nSlices;
+                vLineBuffer[vertexBase + 4] = (1.0f - i) / (float)nSlices;
+            }
+            mVertices.put(vLineBuffer, 0, vLineBuffer.length);
+        }
+
+        short[] indexBuffer = new short[max(mNumIndices)];
+        int index = 0;
+        int bufferNum = 0;
+        for (int i = 0; i < nSlices; i++) {
+            for (int j = 0; j < nSlices; j++) {
+                int i1 = i + 1;
+                int j1 = j + 1;
+                if (index >= mNumIndices[bufferNum]) {
+                    // buffer ready for moving to target
+                    mIndices[bufferNum].put(indexBuffer, 0, mNumIndices[bufferNum]);
+                    // move to the next one
+                    index = 0;
+                    bufferNum++;
+                }
+                indexBuffer[index++] = (short) (i * iMax + j);
+                indexBuffer[index++] = (short) (i1 * iMax + j);
+                indexBuffer[index++] = (short) (i1 * iMax + j1);
+                indexBuffer[index++] = (short) (i * iMax + j);
+                indexBuffer[index++] = (short) (i1 * iMax + j1);
+                indexBuffer[index++] = (short) (i * iMax + j1);
+            }
+        }
+        mIndices[bufferNum].put(indexBuffer, 0, mNumIndices[bufferNum]);
+
+        mVertices.position(0);
+        for (int i = 0; i < indicesPerVertex; i++) {
+            mIndices[i].position(0);
+        }
+    }
+
+    public FloatBuffer getVertices() {
+        return mVertices;
+    }
+
+    public int getVeticesStride() {
+        return 5*FLOAT_SIZE;
+    }
+
+    public ShortBuffer[] getIndices() {
+        return mIndices;
+    }
+
+    public int[] getNumIndices() {
+        return mNumIndices;
+    }
+
+    public int getTotalIndices() {
+        return mTotalIndices;
+    }
+    /** once VBO buffer is generated, model data can be dropped */
+    public void releaseVertexBuffer() {
+        mVertices = null;
+    }
+
+    public void releaseIndexBuffer() {
+        mVertices = null;
+    }
+
+    private int max(int[] array) {
+        int max = array[0];
+        for (int i = 1; i < array.length; i++) {
+            if (array[i] > max) max = array[i];
+        }
+        return max;
+    }
+}