arc: Extends cheets_GamePerfmance test to capture more metrics.
This adds extra metrics per each frame to capture:
* Maximum number of triangles to render.
* Fill and blend rate in kpixels
* Maximum number of device calls to render.
* Maximum number of UI controls.
This also capture results in two mode, first mode without extra load
and second mode with extra 2 threads that load CPU and emulate heavy
app/game.
Test: Locally
Bug: 13553231
Bug: 1347063273
Change-Id: I87491634fa38bd5e04d47d62154a0da8e467213f
(cherry picked from commit f3dae277aa990dcb99d5a0bab1f9ccc141b9e4d9)
diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml
index b331e2c..2ff7fa6 100644
--- a/tests/GamePerformance/AndroidManifest.xml
+++ b/tests/GamePerformance/AndroidManifest.xml
@@ -16,7 +16,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.gameperformance">
+ package="android.gameperformance"
+ android:versionCode="3"
+ android:versionName="3.0" >
<uses-sdk android:minSdkVersion="25"/>
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
@@ -24,7 +26,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:theme="@style/noeffects">
<uses-library android:name="android.test.runner" />
- <activity android:name="android.gameperformance.GamePerformanceActivity" >
+ <activity android:name="android.gameperformance.GamePerformanceActivity"
+ android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
diff --git a/tests/GamePerformance/res/drawable/animation.xml b/tests/GamePerformance/res/drawable/animation.xml
new file mode 100644
index 0000000..b423ff0
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/animation.xml
@@ -0,0 +1,29 @@
+<!--
+ * Copyright (C) 2019 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.
+ -->
+
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/animation" android:oneshot="false">
+ <item android:drawable="@drawable/digit_0" android:duration="15" />
+ <item android:drawable="@drawable/digit_1" android:duration="15" />
+ <item android:drawable="@drawable/digit_2" android:duration="15" />
+ <item android:drawable="@drawable/digit_3" android:duration="15" />
+ <item android:drawable="@drawable/digit_4" android:duration="15" />
+ <item android:drawable="@drawable/digit_5" android:duration="15" />
+ <item android:drawable="@drawable/digit_6" android:duration="15" />
+ <item android:drawable="@drawable/digit_7" android:duration="15" />
+ <item android:drawable="@drawable/digit_8" android:duration="15" />
+ <item android:drawable="@drawable/digit_9" android:duration="15" />
+ </animation-list>
\ No newline at end of file
diff --git a/tests/GamePerformance/res/drawable/digit_0.png b/tests/GamePerformance/res/drawable/digit_0.png
new file mode 100644
index 0000000..7264e3e
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_0.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_1.png b/tests/GamePerformance/res/drawable/digit_1.png
new file mode 100644
index 0000000..f098a71
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_1.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_2.png b/tests/GamePerformance/res/drawable/digit_2.png
new file mode 100644
index 0000000..f08cd31
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_2.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_3.png b/tests/GamePerformance/res/drawable/digit_3.png
new file mode 100644
index 0000000..497df8a
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_3.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_4.png b/tests/GamePerformance/res/drawable/digit_4.png
new file mode 100644
index 0000000..10efe8c
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_4.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_5.png b/tests/GamePerformance/res/drawable/digit_5.png
new file mode 100644
index 0000000..1018a2f
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_5.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_6.png b/tests/GamePerformance/res/drawable/digit_6.png
new file mode 100644
index 0000000..593c467
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_6.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_7.png b/tests/GamePerformance/res/drawable/digit_7.png
new file mode 100644
index 0000000..041b95f
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_7.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_8.png b/tests/GamePerformance/res/drawable/digit_8.png
new file mode 100644
index 0000000..f8fa496
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_8.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/digit_9.png b/tests/GamePerformance/res/drawable/digit_9.png
new file mode 100644
index 0000000..303b1da
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/digit_9.png
Binary files differ
diff --git a/tests/GamePerformance/res/drawable/logo.png b/tests/GamePerformance/res/drawable/logo.png
new file mode 100644
index 0000000..61391df
--- /dev/null
+++ b/tests/GamePerformance/res/drawable/logo.png
Binary files differ
diff --git a/tests/GamePerformance/src/android/gameperformance/BaseTest.java b/tests/GamePerformance/src/android/gameperformance/BaseTest.java
new file mode 100644
index 0000000..b0640b44
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/BaseTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.text.DecimalFormat;
+import java.util.concurrent.TimeUnit;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Base class for a test that performs bisection to determine maximum
+ * performance of a metric test measures.
+ */
+public abstract class BaseTest {
+ private final static String TAG = "BaseTest";
+
+ // Time to wait for render warm up. No statistics is collected during this pass.
+ private final static long WARM_UP_TIME = TimeUnit.SECONDS.toMillis(5);
+
+ // Perform pass to probe the configuration using iterations. After each iteration current FPS is
+ // checked and if it looks obviously bad, pass gets stopped earlier. Once all iterations are
+ // done and final FPS is above PASS_THRESHOLD pass to probe is considered successful.
+ private final static long TEST_ITERATION_TIME = TimeUnit.SECONDS.toMillis(12);
+ private final static int TEST_ITERATION_COUNT = 5;
+
+ // FPS pass test threshold, in ratio from ideal FPS, that matches device
+ // refresh rate.
+ private final static double PASS_THRESHOLD = 0.95;
+ // FPS threshold, in ratio from ideal FPS, to identify that current pass to probe is obviously
+ // bad and to stop pass earlier.
+ private final static double OBVIOUS_BAD_THRESHOLD = 0.90;
+
+ private static DecimalFormat DOUBLE_FORMATTER = new DecimalFormat("#.##");
+
+ private final GamePerformanceActivity mActivity;
+
+ // Device's refresh rate.
+ private final double mRefreshRate;
+
+ public BaseTest(@NonNull GamePerformanceActivity activity) {
+ mActivity = activity;
+ final WindowManager windowManager =
+ (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
+ mRefreshRate = windowManager.getDefaultDisplay().getRefreshRate();
+ }
+
+ @NonNull
+ public Context getContext() {
+ return mActivity;
+ }
+
+ @NonNull
+ public GamePerformanceActivity getActivity() {
+ return mActivity;
+ }
+
+ // Returns name of the test.
+ public abstract String getName();
+
+ // Returns unit name.
+ public abstract String getUnitName();
+
+ // Returns number of measured units per one bisection unit.
+ public abstract double getUnitScale();
+
+ // Initializes test.
+ public abstract void initUnits(double unitCount);
+
+ // Initializes probe pass.
+ protected abstract void initProbePass(int probe);
+
+ // Frees probe pass.
+ protected abstract void freeProbePass();
+
+ /**
+ * Performs the test and returns maximum number of measured units achieved. Unit is test
+ * specific and name is returned by getUnitName. Returns 0 in case of failure.
+ */
+ public double run() {
+ try {
+ Log.i(TAG, "Test started " + getName());
+
+ final double passFps = PASS_THRESHOLD * mRefreshRate;
+ final double obviousBadFps = OBVIOUS_BAD_THRESHOLD * mRefreshRate;
+
+ // Bisection bounds. Probe value is taken as middle point. Then it used to initialize
+ // test with probe * getUnitScale units. In case probe passed, lowLimit is updated to
+ // probe, otherwise upLimit is updated to probe. lowLimit contains probe that passes
+ // and upLimit contains the probe that fails. Each iteration narrows the range.
+ // Iterations continue until range is collapsed and lowLimit contains actual test
+ // result.
+ int lowLimit = 0; // Initially 0, that is recognized as failure.
+ int upLimit = 250;
+
+ while (true) {
+ int probe = (lowLimit + upLimit) / 2;
+ if (probe == lowLimit) {
+ Log.i(TAG, "Test done: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
+ " " + getUnitName());
+ return probe * getUnitScale();
+ }
+
+ Log.i(TAG, "Start probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) + " " +
+ getUnitName());
+ initProbePass(probe);
+
+ Thread.sleep(WARM_UP_TIME);
+
+ getActivity().resetFrameTimes();
+
+ double fps = 0.0f;
+ for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+ Thread.sleep(TEST_ITERATION_TIME);
+ fps = getActivity().getFps();
+ if (fps < obviousBadFps) {
+ // Stop test earlier, we could not fit the loading.
+ break;
+ }
+ }
+
+ freeProbePass();
+
+ Log.i(TAG, "Finish probe: " + DOUBLE_FORMATTER.format(probe * getUnitScale()) +
+ " " + getUnitName() + " - " + DOUBLE_FORMATTER.format(fps) + " FPS.");
+ if (fps < passFps) {
+ upLimit = probe;
+ } else {
+ lowLimit = probe;
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java
new file mode 100644
index 0000000..fa6f03b
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CPULoadThread.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+/**
+ * Ballast thread that emulates CPU load by performing heavy computation in loop.
+ */
+public class CPULoadThread extends Thread {
+ private boolean mStopRequest;
+
+ public CPULoadThread() {
+ mStopRequest = false;
+ }
+
+ private static double computePi() {
+ double accumulator = 0;
+ double prevAccumulator = -1;
+ int index = 1;
+ while (true) {
+ accumulator += ((1.0 / (2.0 * index - 1)) - (1.0 / (2.0 * index + 1)));
+ if (accumulator == prevAccumulator) {
+ break;
+ }
+ prevAccumulator = accumulator;
+ index += 2;
+ }
+ return 4 * accumulator;
+ }
+
+ // Requests thread to stop.
+ public void issueStopRequest() {
+ synchronized (this) {
+ mStopRequest = true;
+ }
+ }
+
+ @Override
+ public void run() {
+ // Load CPU by PI computation.
+ while (computePi() != 0) {
+ synchronized (this) {
+ if (mStopRequest) {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/tests/GamePerformance/src/android/gameperformance/ControlsTest.java b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java
new file mode 100644
index 0000000..6c36ddc
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/ControlsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import android.annotation.NonNull;
+
+/**
+ * Tests that verifies how many UI controls can be handled to keep FPS close to device refresh rate.
+ * As a test UI control ImageView with an infinite animation is chosen. The animation has refresh
+ * rate ~67Hz that forces all devices to refresh UI at highest possible rate.
+ */
+public class ControlsTest extends BaseTest {
+ public ControlsTest(@NonNull GamePerformanceActivity activity) {
+ super(activity);
+ }
+
+ @NonNull
+ public CustomControlView getView() {
+ return getActivity().getControlView();
+ }
+
+ @Override
+ protected void initProbePass(int probe) {
+ try {
+ getActivity().attachControlView();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ initUnits(probe * getUnitScale());
+ }
+
+ @Override
+ protected void freeProbePass() {
+ }
+
+ @Override
+ public String getName() {
+ return "control_count";
+ }
+
+ @Override
+ public String getUnitName() {
+ return "controls";
+ }
+
+ @Override
+ public double getUnitScale() {
+ return 5.0;
+ }
+
+ @Override
+ public void initUnits(double controlCount) {
+ try {
+ getView().createControls(getActivity(), (int)Math.round(controlCount));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomControlView.java b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java
new file mode 100644
index 0000000..219085a
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/CustomControlView.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimationDrawable;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.AbsoluteLayout;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+/**
+ * View that holds requested number of UI controls as ImageView with an infinite animation.
+ */
+public class CustomControlView extends AbsoluteLayout {
+ private final static int CONTROL_DIMENTION = 48;
+
+ private final int mPerRowControlCount;
+ private List<Long> mFrameTimes = new ArrayList<>();
+
+ public CustomControlView(@NonNull Context context) {
+ super(context);
+
+ final WindowManager windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ mPerRowControlCount = windowManager.getDefaultDisplay().getWidth() / CONTROL_DIMENTION;
+ }
+
+ /**
+ * Helper class that overrides ImageView and observes draw requests. Only
+ * one such control is created which is the first control in the view.
+ */
+ class ReferenceImageView extends ImageView {
+ public ReferenceImageView(Context context) {
+ super(context);
+ }
+ @Override
+ public void draw(Canvas canvas) {
+ reportFrame();
+ super.draw(canvas);
+ }
+ }
+
+ @WorkerThread
+ public void createControls(
+ @NonNull Activity activity, int controlCount) throws InterruptedException {
+ synchronized (this) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ removeAllViews();
+
+ for (int i = 0; i < controlCount; ++i) {
+ final ImageView image = (i == 0) ?
+ new ReferenceImageView(activity) : new ImageView(activity);
+ final int x = (i % mPerRowControlCount) * CONTROL_DIMENTION;
+ final int y = (i / mPerRowControlCount) * CONTROL_DIMENTION;
+ final AbsoluteLayout.LayoutParams layoutParams =
+ new AbsoluteLayout.LayoutParams(
+ CONTROL_DIMENTION, CONTROL_DIMENTION, x, y);
+ image.setLayoutParams(layoutParams);
+ image.setBackgroundResource(R.drawable.animation);
+ final AnimationDrawable animation =
+ (AnimationDrawable)image.getBackground();
+ animation.start();
+ addView(image);
+ }
+
+ latch.countDown();
+ }
+ });
+ latch.await();
+ }
+ }
+
+ @MainThread
+ private void reportFrame() {
+ final long time = System.currentTimeMillis();
+ synchronized (mFrameTimes) {
+ mFrameTimes.add(time);
+ }
+ }
+
+ /**
+ * Resets frame times in order to calculate FPS for the different test pass.
+ */
+ public void resetFrameTimes() {
+ synchronized (mFrameTimes) {
+ mFrameTimes.clear();
+ }
+ }
+
+ /**
+ * Returns current FPS based on collected frame times.
+ */
+ public double getFps() {
+ synchronized (mFrameTimes) {
+ if (mFrameTimes.size() < 2) {
+ return 0.0f;
+ }
+ return 1000.0 * mFrameTimes.size() /
+ (mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
index 2b37280..08697ae 100644
--- a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
+++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java
@@ -17,23 +17,36 @@
import java.util.ArrayList;
import java.util.List;
-import java.util.Random;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
+import android.util.Log;
public class CustomOpenGLView extends GLSurfaceView {
- private Random mRandom;
- private List<Long> mFrameTimes;
+ public final static String TAG = "CustomOpenGLView";
- public CustomOpenGLView(Context context) {
+ private final List<Long> mFrameTimes;
+ private final Object mLock = new Object();
+ private boolean mRenderReady = false;
+ private FrameDrawer mFrameDrawer = null;
+
+ private float mRenderRatio;
+ private int mRenderWidth;
+ private int mRenderHeight;
+
+ public interface FrameDrawer {
+ public void drawFrame(@NonNull GL10 gl);
+ }
+
+ public CustomOpenGLView(@NonNull Context context) {
super(context);
- mRandom = new Random();
mFrameTimes = new ArrayList<Long>();
setEGLContextClientVersion(2);
@@ -41,25 +54,35 @@
setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ Log.i(TAG, "SurfaceCreated: " + config);
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
gl.glClearDepthf(1.0f);
- gl.glEnable(GL10.GL_DEPTH_TEST);
+ gl.glDisable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
- GL10.GL_NICEST); }
+ GL10.GL_NICEST);
+ synchronized (mLock) {
+ mRenderReady = true;
+ mLock.notify();
+ }
+ }
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
+ Log.i(TAG, "SurfaceChanged: " + width + "x" + height);
GLES20.glViewport(0, 0, width, height);
+ setRenderBounds(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
- GLES20.glClearColor(
- mRandom.nextFloat(), mRandom.nextFloat(), mRandom.nextFloat(), 1.0f);
+ GLES20.glClearColor(0.25f, 0.25f, 0.25f, 1.0f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
- synchronized (mFrameTimes) {
+ synchronized (mLock) {
+ if (mFrameDrawer != null) {
+ mFrameDrawer.drawFrame(gl);
+ }
mFrameTimes.add(System.currentTimeMillis());
}
}
@@ -67,20 +90,38 @@
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
+ public void setRenderBounds(int width, int height) {
+ mRenderWidth = width;
+ mRenderHeight = height;
+ mRenderRatio = (float) mRenderWidth / mRenderHeight;
+ }
+
+ public float getRenderRatio() {
+ return mRenderRatio;
+ }
+
+ public int getRenderWidth() {
+ return mRenderWidth;
+ }
+
+ public int getRenderHeight() {
+ return mRenderHeight;
+ }
+
/**
- * Resets frame times in order to calculate fps for different test pass.
+ * Resets frame times in order to calculate FPS for the different test pass.
*/
public void resetFrameTimes() {
- synchronized (mFrameTimes) {
+ synchronized (mLock) {
mFrameTimes.clear();
}
}
/**
- * Returns current fps based on collected frame times.
+ * Returns current FPS based on collected frame times.
*/
public double getFps() {
- synchronized (mFrameTimes) {
+ synchronized (mLock) {
if (mFrameTimes.size() < 2) {
return 0.0f;
}
@@ -88,4 +129,26 @@
(mFrameTimes.get(mFrameTimes.size() - 1) - mFrameTimes.get(0));
}
}
+
+ /**
+ * Waits for render attached to the view.
+ */
+ public void waitRenderReady() {
+ synchronized (mLock) {
+ while (!mRenderReady) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets/resets frame drawer.
+ */
+ public void setFrameDrawer(@Nullable FrameDrawer frameDrawer) {
+ mFrameDrawer = frameDrawer;
+ }
}
diff --git a/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java
new file mode 100644
index 0000000..df2ae5c
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.NonNull;
+
+/**
+ * Tests that verifies maximum number of device calls to render the geometry to keep FPS close to
+ * the device refresh rate. This uses trivial one triangle patch that is rendered multiple times.
+ */
+public class DeviceCallsOpenGLTest extends RenderPatchOpenGLTest {
+
+ public DeviceCallsOpenGLTest(@NonNull GamePerformanceActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ public String getName() {
+ return "device_calls";
+ }
+
+ @Override
+ public String getUnitName() {
+ return "calls";
+ }
+
+ @Override
+ public double getUnitScale() {
+ return 25.0;
+ }
+
+ @Override
+ public void initUnits(double deviceCallsD) {
+ final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+ final RenderPatch renderPatch = new RenderPatch(1 /* triangleCount */,
+ 0.05f /* dimension */,
+ RenderPatch.TESSELLATION_BASE);
+ final int deviceCalls = (int)Math.round(deviceCallsD);
+ for (int i = 0; i < deviceCalls; ++i) {
+ renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+ }
+ setRenderPatches(renderPatches);
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java
new file mode 100644
index 0000000..9b26193
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL;
+
+import android.annotation.NonNull;
+import android.opengl.GLES20;
+
+/**
+ * Tests that verifies maximum fill rate per frame can be used to keep FPS close to the device
+ * refresh rate. It works in two modes, blend disabled and blend enabled. This uses few big simple
+ * quad patches.
+ */
+public class FillRateOpenGLTest extends RenderPatchOpenGLTest {
+ private final float[] BLEND_COLOR = new float[] { 1.0f, 1.0f, 1.0f, 0.2f };
+
+ private final boolean mTestBlend;
+
+ public FillRateOpenGLTest(@NonNull GamePerformanceActivity activity, boolean testBlend) {
+ super(activity);
+ mTestBlend = testBlend;
+ }
+
+ @Override
+ public String getName() {
+ return mTestBlend ? "blend_rate" : "fill_rate";
+ }
+
+ @Override
+ public String getUnitName() {
+ return "screens";
+ }
+
+ @Override
+ public double getUnitScale() {
+ return 0.2;
+ }
+
+ @Override
+ public void initUnits(double screens) {
+ final CustomOpenGLView view = getView();
+ final int pixelRate = (int)Math.round(screens * view.getHeight() * view.getWidth());
+ final int maxPerPath = view.getHeight() * view.getHeight();
+
+ final int patchCount = (int)(pixelRate + maxPerPath -1) / maxPerPath;
+ final float patchDimension =
+ (float)((Math.sqrt(2.0f) * pixelRate / patchCount) / maxPerPath);
+
+ final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+ final RenderPatch renderPatch = new RenderPatch(2 /* triangleCount for quad */,
+ patchDimension,
+ RenderPatch.TESSELLATION_BASE);
+ for (int i = 0; i < patchCount; ++i) {
+ renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+ }
+ setRenderPatches(renderPatches);
+ }
+
+ @Override
+ public float[] getColor() {
+ return BLEND_COLOR;
+ }
+
+ @Override
+ public void onBeforeDraw(GL gl) {
+ if (!mTestBlend) {
+ return;
+ }
+
+ // Enable blend if needed.
+ GLES20.glEnable(GLES20.GL_BLEND);
+ OpenGLUtils.checkGlError("disableBlend");
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ OpenGLUtils.checkGlError("blendFunction");
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
index b0e6196..dc745f1 100644
--- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java
@@ -25,14 +25,32 @@
import android.widget.RelativeLayout;
/**
- * Minimal activity that holds SurfaceView or GLSurfaceView.
- * call attachSurfaceView or attachOpenGLView to switch views.
+ * Minimal activity that holds different types of views.
+ * call attachSurfaceView, attachOpenGLView or attachControlView to switch
+ * the view.
*/
public class GamePerformanceActivity extends Activity {
private CustomSurfaceView mSurfaceView = null;
private CustomOpenGLView mOpenGLView = null;
+ private CustomControlView mControlView = null;
+
private RelativeLayout mRootLayout;
+ private void detachAllViews() {
+ if (mOpenGLView != null) {
+ mRootLayout.removeView(mOpenGLView);
+ mOpenGLView = null;
+ }
+ if (mSurfaceView != null) {
+ mRootLayout.removeView(mSurfaceView);
+ mSurfaceView = null;
+ }
+ if (mControlView != null) {
+ mRootLayout.removeView(mControlView);
+ mControlView = null;
+ }
+ }
+
public void attachSurfaceView() throws InterruptedException {
synchronized (mRootLayout) {
if (mSurfaceView != null) {
@@ -42,10 +60,7 @@
runOnUiThread(new Runnable() {
@Override
public void run() {
- if (mOpenGLView != null) {
- mRootLayout.removeView(mOpenGLView);
- mOpenGLView = null;
- }
+ detachAllViews();
mSurfaceView = new CustomSurfaceView(GamePerformanceActivity.this);
mRootLayout.addView(mSurfaceView);
latch.countDown();
@@ -65,10 +80,7 @@
runOnUiThread(new Runnable() {
@Override
public void run() {
- if (mSurfaceView != null) {
- mRootLayout.removeView(mSurfaceView);
- mSurfaceView = null;
- }
+ detachAllViews();
mOpenGLView = new CustomOpenGLView(GamePerformanceActivity.this);
mRootLayout.addView(mOpenGLView);
latch.countDown();
@@ -78,6 +90,40 @@
}
}
+ public void attachControlView() throws InterruptedException {
+ synchronized (mRootLayout) {
+ if (mControlView != null) {
+ return;
+ }
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ detachAllViews();
+ mControlView = new CustomControlView(GamePerformanceActivity.this);
+ mRootLayout.addView(mControlView);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ }
+ }
+
+
+ public CustomOpenGLView getOpenGLView() {
+ if (mOpenGLView == null) {
+ throw new RuntimeException("OpenGL view is not attached");
+ }
+ return mOpenGLView;
+ }
+
+ public CustomControlView getControlView() {
+ if (mControlView == null) {
+ throw new RuntimeException("Control view is not attached");
+ }
+ return mControlView;
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -105,6 +151,8 @@
mSurfaceView.resetFrameTimes();
} else if (mOpenGLView != null) {
mOpenGLView.resetFrameTimes();
+ } else if (mControlView != null) {
+ mControlView.resetFrameTimes();
} else {
throw new IllegalStateException("Nothing attached");
}
@@ -115,6 +163,8 @@
return mSurfaceView.getFps();
} else if (mOpenGLView != null) {
return mOpenGLView.getFps();
+ } else if (mControlView != null) {
+ return mControlView.getFps();
} else {
throw new IllegalStateException("Nothing attached");
}
diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
index e5de7d7..d6e2861 100644
--- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
+++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java
@@ -17,14 +17,18 @@
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
+import android.annotation.NonNull;
import android.app.Activity;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Bundle;
+import android.os.Debug;
import android.os.Trace;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.SmallTest;
@@ -84,4 +88,50 @@
getInstrumentation().sendStatus(Activity.RESULT_OK, status);
}
+
+ @SmallTest
+ public void testPerformanceMetricsWithoutExtraLoad() throws IOException, InterruptedException {
+ final Bundle status = runPerformanceTests("no_extra_load_");
+ getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+ }
+
+ @SmallTest
+ public void testPerformanceMetricsWithExtraLoad() throws IOException, InterruptedException {
+ // Start CPU ballast threads first.
+ CPULoadThread[] cpuLoadThreads = new CPULoadThread[2];
+ for (int i = 0; i < cpuLoadThreads.length; ++i) {
+ cpuLoadThreads[i] = new CPULoadThread();
+ cpuLoadThreads[i].start();
+ }
+
+ final Bundle status = runPerformanceTests("extra_load_");
+
+ for (int i = 0; i < cpuLoadThreads.length; ++i) {
+ cpuLoadThreads[i].issueStopRequest();
+ cpuLoadThreads[i].join();
+ }
+
+ getInstrumentation().sendStatus(Activity.RESULT_OK, status);
+ }
+
+ @NonNull
+ private Bundle runPerformanceTests(@NonNull String prefix) {
+ final Bundle status = new Bundle();
+
+ final GamePerformanceActivity activity = getActivity();
+
+ final List<BaseTest> tests = new ArrayList<>();
+ tests.add(new TriangleCountOpenGLTest(activity));
+ tests.add(new FillRateOpenGLTest(activity, false /* testBlend */));
+ tests.add(new FillRateOpenGLTest(activity, true /* testBlend */));
+ tests.add(new DeviceCallsOpenGLTest(activity));
+ tests.add(new ControlsTest(activity));
+
+ for (BaseTest test : tests) {
+ final double result = test.run();
+ status.putDouble(prefix + test.getName(), result);
+ }
+
+ return status;
+ }
}
diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java
new file mode 100644
index 0000000..1d3f95c
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/OpenGLTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import javax.microedition.khronos.opengles.GL;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.annotation.NonNull;
+import android.gameperformance.CustomOpenGLView.FrameDrawer;
+
+/**
+ * Base class for all OpenGL based tests.
+ */
+public abstract class OpenGLTest extends BaseTest {
+ public OpenGLTest(@NonNull GamePerformanceActivity activity) {
+ super(activity);
+ }
+
+ @NonNull
+ public CustomOpenGLView getView() {
+ return getActivity().getOpenGLView();
+ }
+
+ // Performs test drawing.
+ protected abstract void draw(GL gl);
+
+ // Initializes the test on first draw call.
+ private class ParamFrameDrawer implements FrameDrawer {
+ private final double mUnitCount;
+ private boolean mInited;
+
+ public ParamFrameDrawer(double unitCount) {
+ mUnitCount = unitCount;
+ mInited = false;
+ }
+
+ @Override
+ public void drawFrame(GL10 gl) {
+ if (!mInited) {
+ initUnits(mUnitCount);
+ mInited = true;
+ }
+ draw(gl);
+ }
+ }
+
+ @Override
+ protected void initProbePass(int probe) {
+ try {
+ getActivity().attachOpenGLView();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ getView().waitRenderReady();
+ getView().setFrameDrawer(new ParamFrameDrawer(probe * getUnitScale()));
+ }
+
+ @Override
+ protected void freeProbePass() {
+ getView().setFrameDrawer(null);
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java
new file mode 100644
index 0000000..4f98c52
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.util.Log;
+
+/**
+ * Helper class for OpenGL.
+ */
+public class OpenGLUtils {
+ private final static String TAG = "OpenGLUtils";
+
+ public static void checkGlError(String glOperation) {
+ final int error = GLES20.glGetError();
+ if (error == GLES20.GL_NO_ERROR) {
+ return;
+ }
+ final String errorMessage = glOperation + ": glError " + error;
+ Log.e(TAG, errorMessage);
+ }
+
+ public static int loadShader(int type, String shaderCode) {
+ final int shader = GLES20.glCreateShader(type);
+ checkGlError("createShader");
+
+ GLES20.glShaderSource(shader, shaderCode);
+ checkGlError("shaderSource");
+ GLES20.glCompileShader(shader);
+ checkGlError("shaderCompile");
+
+ return shader;
+ }
+
+ public static int createProgram(@NonNull String vertexShaderCode,
+ @NonNull String fragmentShaderCode) {
+ final int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
+ final int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
+
+ final int program = GLES20.glCreateProgram();
+ checkGlError("createProgram");
+ GLES20.glAttachShader(program, vertexShader);
+ checkGlError("attachVertexShader");
+ GLES20.glAttachShader(program, fragmentShader);
+ checkGlError("attachFragmentShader");
+ GLES20.glLinkProgram(program);
+ checkGlError("linkProgram");
+
+ return program;
+ }
+
+ public static int createTexture(@NonNull Context context, int resource) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+
+ final int[] textureHandle = new int[1];
+ GLES20.glGenTextures(1, textureHandle, 0);
+ OpenGLUtils.checkGlError("GenTextures");
+ final int handle = textureHandle[0];
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, handle);
+ 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);
+ GLUtils.texImage2D(
+ GLES20.GL_TEXTURE_2D,
+ 0,
+ BitmapFactory.decodeResource(
+ context.getResources(), resource, options),
+ 0);
+
+ return handle;
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatch.java b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java
new file mode 100644
index 0000000..2e69a61
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatch.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Helper class that generates patch to render. Patch is a regular polygon with the center in 0.
+ * Regular polygon fits in circle with requested radius.
+ */
+public class RenderPatch {
+ public static final int FLOAT_SIZE = 4;
+ public static final int SHORT_SIZE = 2;
+ public static final int VERTEX_COORD_COUNT = 3;
+ public static final int VERTEX_STRIDE = VERTEX_COORD_COUNT * FLOAT_SIZE;
+ public static final int TEXTURE_COORD_COUNT = 2;
+ public static final int TEXTURE_STRIDE = TEXTURE_COORD_COUNT * FLOAT_SIZE;
+
+ // Tessellation is done using points on circle.
+ public static final int TESSELLATION_BASE = 0;
+ // Tesselation is done using extra point in 0.
+ public static final int TESSELLATION_TO_CENTER = 1;
+
+ // Radius of circle that fits polygon.
+ private final float mDimension;
+
+ private final ByteBuffer mVertexBuffer;
+ private final ByteBuffer mTextureBuffer;
+ private final ByteBuffer mIndexBuffer;
+
+ public RenderPatch(int triangleCount, float dimension, int tessellation) {
+ mDimension = dimension;
+
+ int pointCount;
+ int externalPointCount;
+
+ if (triangleCount < 1) {
+ throw new IllegalArgumentException("Too few triangles to perform tessellation");
+ }
+
+ switch (tessellation) {
+ case TESSELLATION_BASE:
+ externalPointCount = triangleCount + 2;
+ pointCount = externalPointCount;
+ break;
+ case TESSELLATION_TO_CENTER:
+ if (triangleCount < 3) {
+ throw new IllegalArgumentException(
+ "Too few triangles to perform tessellation to center");
+ }
+ externalPointCount = triangleCount;
+ pointCount = triangleCount + 1;
+ break;
+ default:
+ throw new IllegalArgumentException("Wrong tesselation requested");
+ }
+
+ if (pointCount > Short.MAX_VALUE) {
+ throw new IllegalArgumentException("Number of requested triangles is too big");
+ }
+
+ mVertexBuffer = ByteBuffer.allocateDirect(pointCount * VERTEX_STRIDE);
+ mVertexBuffer.order(ByteOrder.nativeOrder());
+
+ mTextureBuffer = ByteBuffer.allocateDirect(pointCount * TEXTURE_STRIDE);
+ mTextureBuffer.order(ByteOrder.nativeOrder());
+
+ for (int i = 0; i < externalPointCount; ++i) {
+ // Use 45 degree rotation to make quad aligned along axises in case
+ // triangleCount is four.
+ final double angle = Math.PI * 0.25 + (Math.PI * 2.0 * i) / (externalPointCount);
+ // Positions
+ mVertexBuffer.putFloat((float) (dimension * Math.sin(angle)));
+ mVertexBuffer.putFloat((float) (dimension * Math.cos(angle)));
+ mVertexBuffer.putFloat(0.0f);
+ // Texture coordinates.
+ mTextureBuffer.putFloat((float) (0.5 + 0.5 * Math.sin(angle)));
+ mTextureBuffer.putFloat((float) (0.5 - 0.5 * Math.cos(angle)));
+ }
+
+ if (tessellation == TESSELLATION_TO_CENTER) {
+ // Add center point.
+ mVertexBuffer.putFloat(0.0f);
+ mVertexBuffer.putFloat(0.0f);
+ mVertexBuffer.putFloat(0.0f);
+ mTextureBuffer.putFloat(0.5f);
+ mTextureBuffer.putFloat(0.5f);
+ }
+
+ mIndexBuffer =
+ ByteBuffer.allocateDirect(
+ triangleCount * 3 /* indices per triangle */ * SHORT_SIZE);
+ mIndexBuffer.order(ByteOrder.nativeOrder());
+
+ switch (tessellation) {
+ case TESSELLATION_BASE:
+ for (int i = 0; i < triangleCount; ++i) {
+ mIndexBuffer.putShort((short) 0);
+ mIndexBuffer.putShort((short) (i + 1));
+ mIndexBuffer.putShort((short) (i + 2));
+ }
+ break;
+ case TESSELLATION_TO_CENTER:
+ for (int i = 0; i < triangleCount; ++i) {
+ mIndexBuffer.putShort((short)i);
+ mIndexBuffer.putShort((short)((i + 1) % externalPointCount));
+ mIndexBuffer.putShort((short)externalPointCount);
+ }
+ break;
+ }
+
+ if (mVertexBuffer.remaining() != 0 || mTextureBuffer.remaining() != 0 || mIndexBuffer.remaining() != 0) {
+ throw new RuntimeException("Failed to fill buffers");
+ }
+
+ mVertexBuffer.position(0);
+ mTextureBuffer.position(0);
+ mIndexBuffer.position(0);
+ }
+
+ public float getDimension() {
+ return mDimension;
+ }
+
+ public ByteBuffer getVertexBuffer() {
+ return mVertexBuffer;
+ }
+
+ public ByteBuffer getTextureBuffer() {
+ return mTextureBuffer;
+ }
+
+ public ByteBuffer getIndexBuffer() {
+ return mIndexBuffer;
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java
new file mode 100644
index 0000000..7dcdb00
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.Random;
+
+import android.annotation.NonNull;
+import android.opengl.Matrix;
+
+/**
+ * Class that performs bouncing animation for RenderPatch on the screen.
+ */
+public class RenderPatchAnimation {
+ private final static Random RANDOM = new Random();
+
+ private final RenderPatch mRenderPatch;
+ // Bounds of animation
+ private final float mAvailableX;
+ private final float mAvailableY;
+
+ // Crurrent position.
+ private float mPosX;
+ private float mPosY;
+ // Direction of movement.
+ private float mDirX;
+ private float mDirY;
+
+ private float[] mMatrix;
+
+ public RenderPatchAnimation(@NonNull RenderPatch renderPatch, float ratio) {
+ mRenderPatch = renderPatch;
+
+ mAvailableX = ratio - mRenderPatch.getDimension();
+ mAvailableY = 1.0f - mRenderPatch.getDimension();
+
+ mPosX = 2.0f * mAvailableX * RANDOM.nextFloat() - mAvailableX;
+ mPosY = 2.0f * mAvailableY * RANDOM.nextFloat() - mAvailableY;
+ mMatrix = new float[16];
+
+ // Evenly distributed in cycle, normalized.
+ while (true) {
+ mDirX = 2.0f * RANDOM.nextFloat() - 1.0f;
+ mDirY = mRenderPatch.getDimension() < 1.0f ? 2.0f * RANDOM.nextFloat() - 1.0f : 0.0f;
+
+ final float length = (float)Math.sqrt(mDirX * mDirX + mDirY * mDirY);
+ if (length <= 1.0f && length > 0.0f) {
+ mDirX /= length;
+ mDirY /= length;
+ break;
+ }
+ }
+ }
+
+ @NonNull
+ public RenderPatch getRenderPatch() {
+ return mRenderPatch;
+ }
+
+ /**
+ * Performs the next update. t specifies the distance to travel along the direction. This checks
+ * if patch goes out of screen and invert axis direction if needed.
+ */
+ public void update(float t) {
+ mPosX += mDirX * t;
+ mPosY += mDirY * t;
+ if (mPosX < -mAvailableX) {
+ mDirX = Math.abs(mDirX);
+ } else if (mPosX > mAvailableX) {
+ mDirX = -Math.abs(mDirX);
+ }
+ if (mPosY < -mAvailableY) {
+ mDirY = Math.abs(mDirY);
+ } else if (mPosY > mAvailableY) {
+ mDirY = -Math.abs(mDirY);
+ }
+ }
+
+ /**
+ * Returns Model/View/Projection transform for the patch.
+ */
+ public float[] getTransform(@NonNull float[] vpMatrix) {
+ Matrix.setIdentityM(mMatrix, 0);
+ mMatrix[12] = mPosX;
+ mMatrix[13] = mPosY;
+ Matrix.multiplyMM(mMatrix, 0, vpMatrix, 0, mMatrix, 0);
+ return mMatrix;
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java
new file mode 100644
index 0000000..7492cc0
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.List;
+
+import javax.microedition.khronos.opengles.GL;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+
+/**
+ * Base class for all OpenGL based tests that use RenderPatch as a base.
+ */
+public abstract class RenderPatchOpenGLTest extends OpenGLTest {
+ private final float[] COLOR = new float[] { 1.0f, 1.0f, 1.0f, 1.0f };
+
+ private final String VERTEX_SHADER =
+ "uniform mat4 uMVPMatrix;"
+ + "attribute vec4 vPosition;"
+ + "attribute vec2 vTexture;"
+ + "varying vec2 vTex;"
+ + "void main() {"
+ + " vTex = vTexture;"
+ + " gl_Position = uMVPMatrix * vPosition;"
+ + "}";
+
+ private final String FRAGMENT_SHADER =
+ "precision mediump float;"
+ + "uniform sampler2D uTexture;"
+ + "uniform vec4 uColor;"
+ + "varying vec2 vTex;"
+ + "void main() {"
+ + " vec4 color = texture2D(uTexture, vTex);"
+ + " gl_FragColor = uColor * color;"
+ + "}";
+
+ private List<RenderPatchAnimation> mRenderPatches;
+
+ private int mProgram = -1;
+ private int mMVPMatrixHandle;
+ private int mTextureHandle;
+ private int mPositionHandle;
+ private int mColorHandle;
+ private int mTextureCoordHandle;
+
+ private final float[] mVPMatrix = new float[16];
+
+ public RenderPatchOpenGLTest(@NonNull GamePerformanceActivity activity) {
+ super(activity);
+ }
+
+ protected void setRenderPatches(@NonNull List<RenderPatchAnimation> renderPatches) {
+ mRenderPatches = renderPatches;
+ }
+
+ private void ensureInited() {
+ if (mProgram >= 0) {
+ return;
+ }
+
+ mProgram = OpenGLUtils.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
+
+ // get handle to fragment shader's uColor member
+ GLES20.glUseProgram(mProgram);
+ OpenGLUtils.checkGlError("useProgram");
+
+ // get handle to shape's transformation matrix
+ mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
+ OpenGLUtils.checkGlError("get uMVPMatrix");
+
+ mTextureHandle = GLES20.glGetUniformLocation(mProgram, "uTexture");
+ OpenGLUtils.checkGlError("uTexture");
+ // get handle to vertex shader's vPosition member
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
+ OpenGLUtils.checkGlError("vPosition");
+ mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "vTexture");
+ OpenGLUtils.checkGlError("vTexture");
+ mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ OpenGLUtils.checkGlError("uColor");
+
+ mTextureHandle = OpenGLUtils.createTexture(getContext(), R.drawable.logo);
+
+ final float[] projectionMatrix = new float[16];
+ final float[] viewMatrix = new float[16];
+
+ final float ratio = getView().getRenderRatio();
+ Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, -1, 1);
+ Matrix.setLookAtM(viewMatrix, 0, 0, 0, -0.5f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
+ Matrix.multiplyMM(mVPMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
+ }
+
+ /**
+ * Returns global color for patch.
+ */
+ public float[] getColor() {
+ return COLOR;
+ }
+
+ /**
+ * Extra setup for particular tests.
+ */
+ public void onBeforeDraw(GL gl) {
+ }
+
+ @Override
+ public void draw(GL gl) {
+ ensureInited();
+
+ GLES20.glUseProgram(mProgram);
+ OpenGLUtils.checkGlError("useProgram");
+
+ GLES20.glDisable(GLES20.GL_BLEND);
+ OpenGLUtils.checkGlError("disableBlend");
+
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+ OpenGLUtils.checkGlError("enableVertexAttributes");
+
+ GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
+ OpenGLUtils.checkGlError("enableTexturesAttributes");
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureHandle);
+ OpenGLUtils.checkGlError("setTexture");
+
+ GLES20.glUniform4fv(mColorHandle, 1, getColor(), 0);
+ OpenGLUtils.checkGlError("setColor");
+
+ onBeforeDraw(gl);
+
+ for (final RenderPatchAnimation renderPatchAnimation : mRenderPatches) {
+
+ renderPatchAnimation.update(0.01f);
+ GLES20.glUniformMatrix4fv(mMVPMatrixHandle,
+ 1,
+ false,
+ renderPatchAnimation.getTransform(mVPMatrix),
+ 0);
+ OpenGLUtils.checkGlError("setTransform");
+
+ GLES20.glVertexAttribPointer(
+ mPositionHandle,
+ RenderPatch.VERTEX_COORD_COUNT,
+ GLES20.GL_FLOAT,
+ false /* normalized */,
+ RenderPatch.VERTEX_STRIDE,
+ renderPatchAnimation.getRenderPatch().getVertexBuffer());
+ OpenGLUtils.checkGlError("setVertexAttribute");
+
+ GLES20.glVertexAttribPointer(
+ mTextureCoordHandle,
+ RenderPatch.TEXTURE_COORD_COUNT,
+ GLES20.GL_FLOAT,
+ false /* normalized */,
+ RenderPatch.TEXTURE_STRIDE,
+ renderPatchAnimation.getRenderPatch().getTextureBuffer());
+ OpenGLUtils.checkGlError("setTextureAttribute");
+
+ // Draw the patch.
+ final int indicesCount =
+ renderPatchAnimation.getRenderPatch().getIndexBuffer().capacity() /
+ RenderPatch.SHORT_SIZE;
+ GLES20.glDrawElements(
+ GLES20.GL_TRIANGLES,
+ indicesCount,
+ GLES20.GL_UNSIGNED_SHORT,
+ renderPatchAnimation.getRenderPatch().getIndexBuffer());
+ OpenGLUtils.checkGlError("drawPatch");
+ }
+
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
+ }
+}
\ No newline at end of file
diff --git a/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java
new file mode 100644
index 0000000..593f37b
--- /dev/null
+++ b/tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 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.gameperformance;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.annotation.NonNull;
+
+/**
+ * Test that measures maximum amount of triangles can be rasterized keeping FPS close to the device
+ * refresh rate. It is has very few devices call and each call contains big amount of triangles.
+ * Total filling area is around one screen.
+ */
+public class TriangleCountOpenGLTest extends RenderPatchOpenGLTest {
+ // Based on index buffer of short values.
+ private final static int MAX_TRIANGLES_IN_PATCH = 32000;
+
+ public TriangleCountOpenGLTest(@NonNull GamePerformanceActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ public String getName() {
+ return "triangle_count";
+ }
+
+ @Override
+ public String getUnitName() {
+ return "ktriangles";
+ }
+
+ @Override
+ public double getUnitScale() {
+ return 2.0;
+ }
+
+ @Override
+ public void initUnits(double trianlgeCountD) {
+ final int triangleCount = (int)Math.round(trianlgeCountD * 1000.0);
+ final List<RenderPatchAnimation> renderPatches = new ArrayList<>();
+ final int patchCount =
+ (triangleCount + MAX_TRIANGLES_IN_PATCH - 1) / MAX_TRIANGLES_IN_PATCH;
+ final int patchTriangleCount = triangleCount / patchCount;
+ for (int i = 0; i < patchCount; ++i) {
+ final RenderPatch renderPatch = new RenderPatch(patchTriangleCount,
+ 0.5f /* dimension */,
+ RenderPatch.TESSELLATION_TO_CENTER);
+ renderPatches.add(new RenderPatchAnimation(renderPatch, getView().getRenderRatio()));
+ }
+ setRenderPatches(renderPatches);
+ }
+}
\ No newline at end of file