From e41ec354bb0a6802fa0db4695ce443573f767b4c Mon Sep 17 00:00:00 2001 From: "khmel@google.com" Date: Fri, 21 Jun 2019 08:49:03 -0700 Subject: [PATCH] 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) --- tests/GamePerformance/AndroidManifest.xml | 7 +- .../res/drawable/animation.xml | 29 +++ .../GamePerformance/res/drawable/digit_0.png | Bin 0 -> 1335 bytes .../GamePerformance/res/drawable/digit_1.png | Bin 0 -> 428 bytes .../GamePerformance/res/drawable/digit_2.png | Bin 0 -> 1047 bytes .../GamePerformance/res/drawable/digit_3.png | Bin 0 -> 1275 bytes .../GamePerformance/res/drawable/digit_4.png | Bin 0 -> 613 bytes .../GamePerformance/res/drawable/digit_5.png | Bin 0 -> 945 bytes .../GamePerformance/res/drawable/digit_6.png | Bin 0 -> 1488 bytes .../GamePerformance/res/drawable/digit_7.png | Bin 0 -> 949 bytes .../GamePerformance/res/drawable/digit_8.png | Bin 0 -> 1440 bytes .../GamePerformance/res/drawable/digit_9.png | Bin 0 -> 1501 bytes tests/GamePerformance/res/drawable/logo.png | Bin 0 -> 48689 bytes .../src/android/gameperformance/BaseTest.java | 152 ++++++++++++++ .../gameperformance/CPULoadThread.java | 61 ++++++ .../android/gameperformance/ControlsTest.java | 73 +++++++ .../gameperformance/CustomControlView.java | 128 ++++++++++++ .../gameperformance/CustomOpenGLView.java | 91 +++++++-- .../DeviceCallsOpenGLTest.java | 60 ++++++ .../gameperformance/FillRateOpenGLTest.java | 93 +++++++++ .../GamePerformanceActivity.java | 70 ++++++- .../gameperformance/GamePerformanceTest.java | 50 +++++ .../android/gameperformance/OpenGLTest.java | 76 +++++++ .../android/gameperformance/OpenGLUtils.java | 92 +++++++++ .../android/gameperformance/RenderPatch.java | 150 ++++++++++++++ .../gameperformance/RenderPatchAnimation.java | 101 ++++++++++ .../RenderPatchOpenGLTest.java | 188 ++++++++++++++++++ .../TriangleCountOpenGLTest.java | 66 ++++++ 28 files changed, 1461 insertions(+), 26 deletions(-) create mode 100644 tests/GamePerformance/res/drawable/animation.xml create mode 100644 tests/GamePerformance/res/drawable/digit_0.png create mode 100644 tests/GamePerformance/res/drawable/digit_1.png create mode 100644 tests/GamePerformance/res/drawable/digit_2.png create mode 100644 tests/GamePerformance/res/drawable/digit_3.png create mode 100644 tests/GamePerformance/res/drawable/digit_4.png create mode 100644 tests/GamePerformance/res/drawable/digit_5.png create mode 100644 tests/GamePerformance/res/drawable/digit_6.png create mode 100644 tests/GamePerformance/res/drawable/digit_7.png create mode 100644 tests/GamePerformance/res/drawable/digit_8.png create mode 100644 tests/GamePerformance/res/drawable/digit_9.png create mode 100644 tests/GamePerformance/res/drawable/logo.png create mode 100644 tests/GamePerformance/src/android/gameperformance/BaseTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/CPULoadThread.java create mode 100644 tests/GamePerformance/src/android/gameperformance/ControlsTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/CustomControlView.java create mode 100644 tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/FillRateOpenGLTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/OpenGLTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/OpenGLUtils.java create mode 100644 tests/GamePerformance/src/android/gameperformance/RenderPatch.java create mode 100644 tests/GamePerformance/src/android/gameperformance/RenderPatchAnimation.java create mode 100644 tests/GamePerformance/src/android/gameperformance/RenderPatchOpenGLTest.java create mode 100644 tests/GamePerformance/src/android/gameperformance/TriangleCountOpenGLTest.java diff --git a/tests/GamePerformance/AndroidManifest.xml b/tests/GamePerformance/AndroidManifest.xml index b331e2c07e14d..2ff7fa65664e4 100644 --- a/tests/GamePerformance/AndroidManifest.xml +++ b/tests/GamePerformance/AndroidManifest.xml @@ -16,7 +16,9 @@ --> + package="android.gameperformance" + android:versionCode="3" + android:versionName="3.0" > @@ -24,7 +26,8 @@ - + diff --git a/tests/GamePerformance/res/drawable/animation.xml b/tests/GamePerformance/res/drawable/animation.xml new file mode 100644 index 0000000000000..b423ff0d1ee46 --- /dev/null +++ b/tests/GamePerformance/res/drawable/animation.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ 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 0000000000000000000000000000000000000000..7264e3ee3771be94452f260719d98d7067c52e8e GIT binary patch literal 1335 zcmV-71<3k|P)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00f^&L_t(|+U=W5NOD0G$B#WR zP)qUDT#_1!-!9nn_-J zh^hF#&bJCaKIeWt9Q8`~z-<0^>0pI{|05|{~ z01m)!wW4hOk0Rn(8mGSX$ba8QklF4MItA{cMAel@etyYU963Lev{%@I^n?qi& zH~#>{<8idSy!_*a|6O`}dy&iK${zs9WD>2dtr2PbN9pP5LC??6`2j$u(=j$gAP^vp zMuRLC3kn1Rh^A>2jYg5%?M4QJ0SyliGd?>tHHD(lD7yhr6oo`05w-!Nqoc^_bpH4G zSS*INx3?J~?C%?6KC>)AAdMM6i*hKp;S& zP>4M}K&#ba^~T1=wEUKqmcqouM2a2=1i)^$vtv8#_xs`C;2=d;C=^gzTbp(T+uPf* z`oqHmI{;7=h1CZK2cfXAFzpITrBbYpn{#pk;N|57t2Z<>5UF5wbv0JMxw&Bn0FTFm z)zg{%|F(*X3aqZz>w(0*ZpL~*d3iYz0DL|lBoaxAZnN1S6bj`YfYH>j)2ml_B z2c4ar*x$n8aP9%Ty}dCmVWh04riQ%$0)YTlFDWS@20$njV)b}Do_hdHZ=s-o+=+Wp z5&rO+rs>=Rh(@DWokR@qVa3J8>;=Fy)|1I3F#t?modo2x0r2_!i~~p{66^)QwA?Wb zji2r}zB4qk3ZSa0D&qi{f-R=rmsJ3jm6cdM7z~m+uYk+TOZEa_3Q~MN9}xiIa2RYh zTZ%51%b~2SEcXCJA`w=1yWKo2Ub@o6zl*X6bfNrU;x_#m`;nnV^>#KSe@ylDC+Fxjbt(L#L;w?7JIWUtiDK z-QfHCJNp3G?RKKK!?UxqSYMl)o9xXGn4h1|Xnuf5BtjmKhrIyYZufsPL>?a>Kg|%? z+1X+2Nqc*H?9UN8K0f|5N62cmqF^wHXqrZ`SPW4Vg$xD*8W|a3e0Fkj5=A1B{Kyg` zl_K|1y1Kd$MN#>cC%Ck`(lIxzybK(_6J!CPoOo#bSMA-002ovPDHLkV1n4`UakND literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/digit_1.png b/tests/GamePerformance/res/drawable/digit_1.png new file mode 100644 index 0000000000000000000000000000000000000000..f098a71a4ab44c873d6bdc9eb4372af59c4dd39c GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b z3=De8Ak0{?)V>TT$X?><>&pI^O+-wUO_U|(3s6Y1#5JPCIX^cyHLrxhxhOTUBsE2$ zJhLQ2!QIn0AiR-JoPmK+#M8wwq~g}w+lH%}9Yk6mK33-55X9Nh+uJ)eg8Q9{bRlb# z^aio%Ns0N1nGP)68#m9?QCM5~#P&b)cImXX{+-*UPfH}1JpA+G5hKt_1hAX@*uunj z@~(Bkx3_6^Ic3f|wQ0?Et<#^bGv=k|ZOfhgtn%9DpI0pUUj2^qy_|9X`}$hO<%~L~ zm9Cd&GBW&&$eVAcaN6i;9?&qu> WNv{u`tD15KWQwP&pUXO@geCx!43&HU literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/digit_2.png b/tests/GamePerformance/res/drawable/digit_2.png new file mode 100644 index 0000000000000000000000000000000000000000..f08cd31b41184c4e5df9a813fc1435f710aec0b3 GIT binary patch literal 1047 zcmV+y1nB#TP)e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00VzXL_t(|+U?rCOY>Y92Jq9D z2hpLp1(l*up-90BLO~EggwmmdgSd3DAQWA7(nWFbZxAVVP(deMoE#jAi$bZ`8b9!( ziM3HlLP@D&i`TQioIJeGYmy$#xgpC5q`7{Co|F5;0|3Ou^$&4*aR3g$0XP5$-~b$e z18@Mh)-O9RuBs}LWtkLJ+e^B;yNMtOBoGLYzP>)v)6+vb zIy#8Q<8e*^7FMItfGCRC+1bI=)D#FIe|;pANn|n^yuH1_iLwMhmSybj?jjnE{;$xq zBN~n3?Cgwv0992j3r#!L*Vpm!@!?(oLI_q?R#30kzaN0l=fmRSBKG(9adB}0MNwcF z1{#e9-rwJ$X&TPY&#}3=iC{4JM}$-=^?d-AmzQyRdWu@D_Mfj~7zWnX*8Yf)&*#|% z@cDd5r_)dr`jAKSFKhv zol^Jq_ALD^CYQo(ZEa+FdYXDJm&>{1U7)|e-`xQ1?d{ZYQ54+)(9+UE9dB)Ibq7GT zTBVK$gF$xyNRmVy9~c;L2f)$Mks|;e7B@!~MIoJ?oz!!sQi*hRb^YuoU~6lOIzB%? zZ)teTJ3>}eE|)W11{VqiHakLAm9FcEL?YDitE;Q*w#2NcdcBT?g#~K(a5xNARev@> zDwQ&Q^iNJs*lvn`wGIvrOv9(sY4*FqU#a8cW7F_UOG~ykM4Jwf&1T{Cda2>(=H?vP z8)zCpKA(pm2-NViv$LpHtKVm1S}OhlYkANmA2Sp|$~(%VpEe zjlsb|+~40b@eX1O!1MDnVzC%Cd?*yMe0z&2fY;YojE|2~!v_KZTwh-^)qt@SKvh*t zPEJz8`~7~1qR3hU$~pjD*Ab7$O+S>dV{e}sfJ&u;L?S^AF9?DoA3HS-z%UFXlSyiL zuh)xgHVc;P%K({7#`ce-{(Z=@Y#9LmR+s~D0CyLYo1^>*-~b$e18@L`>Nf;&fc;>g Rl$rnl002ovPDHLkV1i&m+Oz-w literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/digit_3.png b/tests/GamePerformance/res/drawable/digit_3.png new file mode 100644 index 0000000000000000000000000000000000000000..497df8a9f4730ff0206190611e80a4d7e903d794 GIT binary patch literal 1275 zcmVe zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00d)6L_t(|+U=V=NGnYg$N$$y zR8Y_x;)_)=k0Jpr1R)}pR$^fpQ4t}9jdqG4D2SbvkV+AhV6YLa6v9FpABCVo2qFn4 zX5*R_QKB(q^Eh91;rqx&zN;HqH#soP8JOMqW#*hS|JnQ^B0>>c8H(IW07?K#07?K# z07?K#07?K#0E*Qr3N>&dkpR#02#3Sqc^(|cArJ^48jXVEIPg4=uC6XH3 zgTa7~jt;c8wtgQ0lF4GR7+qdolGEv=si`Rf@abl^+lggaipS$rXp;@V@bK`LCN0$l z2M6iw?2NM6Y{391ItIAiZdk2WtgWpfnM@XM5S*Kv!|LiPQmNF>34)W8lj!g7htX(+ zPN&1~-@j2?TMLawgUZTEWV2a_qKKE57w|lf^Ye48udl=B^GS}kva$k`$%OIoab*U< zg@py$-Q6Xx*Zb+<_4f8gPN(z3xDX13lo2r-}Ccxail?m!64}uMX@*lR8&Z3 z>6)6F;s6kdMDlJA#nl0F+xz=FF$|NF1$ujXDVJ`7UkvT>Rtgi zH#el$>vMkn?(S}SdU{fCA(Tp`$nADZP96XqA0I1M5BOWlWHOXUBH(1NZo%bp<%Q7G(?ii{RQ-z3_4PGbEEY)!^YinR$z;?oiTQlK zyer}A>Pr2GA+yJ~(rnwl^+ zHkRXmeSMWYDy#Bptf!|((jSY(iUWXFtCjS}SCR2fz@fJ`QX zqobq0+8U}#gMPmsCX*@0-`LoQySqDCbOOb$03yQ9&W>c?^z^iRB8gHZ?S=-y&n3$MQmL~XT1r)kD91dCU1ZCd>g=%`eo{o-= z3ih$2q5&)|Es-Dy1N?HH64GavB%jKeQIQ%W?R`yCTlgU63 z1aKS&j^l8De~(Zogit61K@i~g`_almHaB{R38!igu7il@|a2002ovPDHLkV1iH9MOy#> literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/digit_4.png b/tests/GamePerformance/res/drawable/digit_4.png new file mode 100644 index 0000000000000000000000000000000000000000..10efe8cf11b2c045d4da714f3688f2bafed8d3fa GIT binary patch literal 613 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b z3=De8Ak0{?)V>TT$X?><>&pI^O+-vpO)sm)1}G$1;u=xnoS&PUnpeW$T$GwvlA5AW zo>`Ki;O^-g5Z=fq&cMKU*3-o?q~g}w*@~wRImoovTgN+!@Ul(LJX_>;d68odTXUgj zcZA511&#tDA}o`ABet*l5&EW%WAAt7J>^fY?)+T;pMBk$92=YR+j(b#y)-R)8Xj+8 z5IevW!N8rssMEj-=1h8AwDZ7co5vRO&zG>?d0$?#JJy`L!ft-V`s+LH=H+qih+7|_ zH$871qv!JZD;R=*Ex!2T@%2y1-oFAx6sMoQsP8bnZuecc)mInUv--R|pJtq5B>9V} z z(@U_L`_FRPlYjU22k0v-4Pg+H4)Xe{^`~+UF#L`OiWS<(ZSB7MvZU+n92-`Ykc~O| i*xtR83mnQa&&0c&{Ue`jmQ4Z1K7*&LpUXO@geCwA!~n?v literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/digit_5.png b/tests/GamePerformance/res/drawable/digit_5.png new file mode 100644 index 0000000000000000000000000000000000000000..1018a2fad73308d8d488739aefab2d5d8220c3d0 GIT binary patch literal 945 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 D%sIoF literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/res/drawable/logo.png b/tests/GamePerformance/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..61391df52077bb08c21546688d2b40e693c3adbe GIT binary patch literal 48689 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 literal 0 HcmV?d00001 diff --git a/tests/GamePerformance/src/android/gameperformance/BaseTest.java b/tests/GamePerformance/src/android/gameperformance/BaseTest.java new file mode 100644 index 0000000000000..b0640b4449145 --- /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 0000000000000..fa6f03bf88cf2 --- /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 0000000000000..6c36ddcc620d2 --- /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 0000000000000..219085a83b2cd --- /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 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 2b37280ae9b55..08697ae953765 100644 --- a/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java +++ b/tests/GamePerformance/src/android/gameperformance/CustomOpenGLView.java @@ -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 mFrameTimes; + public final static String TAG = "CustomOpenGLView"; - public CustomOpenGLView(Context context) { + private final List 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(); 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; + } } diff --git a/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java b/tests/GamePerformance/src/android/gameperformance/DeviceCallsOpenGLTest.java new file mode 100644 index 0000000000000..df2ae5cf670a3 --- /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 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 0000000000000..9b2619372d16a --- /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 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 b0e6196b53d74..dc745f17e6988 100644 --- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceActivity.java @@ -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"); } diff --git a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java index e5de7d75886ed..d6e2861c03a71 100644 --- a/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java +++ b/tests/GamePerformance/src/android/gameperformance/GamePerformanceTest.java @@ -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 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 0000000000000..1d3f95cdd5b80 --- /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 0000000000000..4f98c52b4b398 --- /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 0000000000000..2e69a61475db0 --- /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 0000000000000..7dcdb00e10145 --- /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 0000000000000..7492cc034234c --- /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 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 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 0000000000000..593f37bf91288 --- /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 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