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)
@@ -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" />
|
||||
|
||||
29
tests/GamePerformance/res/drawable/animation.xml
Normal file
@@ -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>
|
||||
BIN
tests/GamePerformance/res/drawable/digit_0.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
tests/GamePerformance/res/drawable/digit_1.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
tests/GamePerformance/res/drawable/digit_2.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/GamePerformance/res/drawable/digit_3.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/GamePerformance/res/drawable/digit_4.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
tests/GamePerformance/res/drawable/digit_5.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
tests/GamePerformance/res/drawable/digit_6.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/GamePerformance/res/drawable/digit_7.png
Normal file
|
After Width: | Height: | Size: 949 B |
BIN
tests/GamePerformance/res/drawable/digit_8.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
tests/GamePerformance/res/drawable/digit_9.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/GamePerformance/res/drawable/logo.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
152
tests/GamePerformance/src/android/gameperformance/BaseTest.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,23 +17,36 @@ package android.gameperformance;
|
||||
|
||||
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 @@ public class CustomOpenGLView extends GLSurfaceView {
|
||||
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 @@ public class CustomOpenGLView extends GLSurfaceView {
|
||||
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 @@ public class CustomOpenGLView extends GLSurfaceView {
|
||||
(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,32 @@ import android.view.WindowManager;
|
||||
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 @@ public class GamePerformanceActivity extends Activity {
|
||||
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 @@ public class GamePerformanceActivity extends Activity {
|
||||
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 class GamePerformanceActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public class GamePerformanceActivity extends Activity {
|
||||
mSurfaceView.resetFrameTimes();
|
||||
} else if (mOpenGLView != null) {
|
||||
mOpenGLView.resetFrameTimes();
|
||||
} else if (mControlView != null) {
|
||||
mControlView.resetFrameTimes();
|
||||
} else {
|
||||
throw new IllegalStateException("Nothing attached");
|
||||
}
|
||||
@@ -115,6 +163,8 @@ public class GamePerformanceActivity extends Activity {
|
||||
return mSurfaceView.getFps();
|
||||
} else if (mOpenGLView != null) {
|
||||
return mOpenGLView.getFps();
|
||||
} else if (mControlView != null) {
|
||||
return mControlView.getFps();
|
||||
} else {
|
||||
throw new IllegalStateException("Nothing attached");
|
||||
}
|
||||
|
||||
@@ -17,14 +17,18 @@ package android.gameperformance;
|
||||
|
||||
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 @@ public class GamePerformanceTest extends
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||