1 /*
2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 #include "Renderer.h"
17
18 #include <GLES3/gl3.h>
19 #include <android/imagedecoder.h>
20
21 #include <memory>
22 #include <numeric>
23 #include <string>
24 #include <vector>
25 // #include <iostream>
26
27 #include <chrono>
28
29 #include "AndroidOut.h"
30 #include "JNIManager.h"
31 #include "Shader.h"
32 #include "TextureAsset.h"
33 #include "Utility.h"
34 #include "android/performance_hint.h"
35
36 using namespace std::chrono_literals;
37 //! executes glGetString and outputs the result to logcat
38 #define PRINT_GL_STRING(s) \
39 { aout << #s ": " << glGetString(s) << std::endl; }
40
41 /*!
42 * @brief if glGetString returns a space separated list of elements, prints each one on a new line
43 *
44 * This works by creating an istringstream of the input c-style string. Then that is used to create
45 * a vector -- each element of the vector is a new element in the input string. Finally a foreach
46 * loop consumes this and outputs it to logcat using @a aout
47 */
48 #define PRINT_GL_STRING_AS_LIST(s) \
49 { \
50 std::istringstream extensionStream((const char *)glGetString(s)); \
51 std::vector<std::string> \
52 extensionList(std::istream_iterator<std::string>{extensionStream}, \
53 std::istream_iterator<std::string>()); \
54 aout << #s ":\n"; \
55 for (auto &extension : extensionList) { \
56 aout << extension << "\n"; \
57 } \
58 aout << std::endl; \
59 }
60
61 //! Color for cornflower blue. Can be sent directly to glClearColor
62 #define CORNFLOWER_BLUE 100 / 255.f, 149 / 255.f, 237 / 255.f, 1
63
64 // Vertex shader, you'd typically load this from assets
65 static const char *vertex = R"vertex(#version 300 es
66 in vec3 inPosition;
67 in vec2 inUV;
68
69 out vec2 fragUV;
70
71 uniform mat4 uProjection;
72
73 void main() {
74 fragUV = inUV;
75 gl_Position = uProjection * vec4(inPosition, 1.0);
76 }
77 )vertex";
78
79 // Fragment shader, you'd typically load this from assets
80 static const char *fragment = R"fragment(#version 300 es
81 precision mediump float;
82
83 in vec2 fragUV;
84
85 uniform sampler2D uTexture;
86
87 out vec4 outColor;
88
89 void main() {
90 outColor = texture(uTexture, fragUV);
91 }
92 )fragment";
93
94 /*!
95 * Half the height of the projection matrix. This gives you a renderable area of height 4 ranging
96 * from -2 to 2
97 */
98 static constexpr float kProjectionHalfHeight = 2.f;
99
100 /*!
101 * The near plane distance for the projection matrix. Since this is an orthographic projection
102 * matrix, it's convenient to have negative values for sorting (and avoiding z-fighting at 0).
103 */
104 static constexpr float kProjectionNearPlane = -1.f;
105
106 /*!
107 * The far plane distance for the projection matrix. Since this is an orthographic porjection
108 * matrix, it's convenient to have the far plane equidistant from 0 as the near plane.
109 */
110 static constexpr float kProjectionFarPlane = 1.f;
111
~Renderer()112 Renderer::~Renderer() {
113 if (display_ != EGL_NO_DISPLAY) {
114 eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
115 if (context_ != EGL_NO_CONTEXT) {
116 eglDestroyContext(display_, context_);
117 context_ = EGL_NO_CONTEXT;
118 }
119 if (surface_ != EGL_NO_SURFACE) {
120 eglDestroySurface(display_, surface_);
121 surface_ = EGL_NO_SURFACE;
122 }
123 eglTerminate(display_);
124 display_ = EGL_NO_DISPLAY;
125 }
126 }
127
render()128 jlong Renderer::render() {
129 // Check to see if the surface has changed size. This is _necessary_ to do every frame when
130 // using immersive mode as you'll get no other notification that your renderable area has
131 // changed.
132
133 updateRenderArea();
134 assert(display_ != nullptr);
135 assert(surface_ != nullptr);
136 assert(shader_ != nullptr);
137
138 // When the renderable area changes, the fprojection matrix has to also be updated. This is true
139 // even if you change from the sample orthographic projection matrix as your aspect ratio has
140 // likely changed.
141 if (shaderNeedsNewProjectionMatrix_) {
142 // a placeholder projection matrix allocated on the stack. Column-major memory layout
143 float projectionMatrix[16] = {0};
144
145 // build an orthographic projection matrix for 2d rendering
146 Utility::buildOrthographicMatrix(projectionMatrix, kProjectionHalfHeight,
147 float(width_) / height_, kProjectionNearPlane,
148 kProjectionFarPlane);
149
150 // send the matrix to the shader
151 // Note: the shader must be active for this to work.
152 assert(projectionMatrix != nullptr);
153
154 if (shader_ != nullptr) {
155 shader_->setProjectionMatrix(projectionMatrix);
156 }
157
158 // make sure the matrix isn't generated every frame
159 shaderNeedsNewProjectionMatrix_ = false;
160 }
161
162 // clear the color buffer
163 glClear(GL_COLOR_BUFFER_BIT);
164
165 // Rotate the models
166 const std::chrono::steady_clock::duration rpm = 2s;
167 static std::chrono::steady_clock::time_point startTime = std::chrono::steady_clock::now();
168 std::chrono::steady_clock::time_point renderTime = std::chrono::steady_clock::now();
169 // Figure out what angle the models need to be at
170 std::chrono::steady_clock::duration offset = (renderTime - startTime) % rpm;
171 auto spin = static_cast<double>(offset.count()) / static_cast<double>(rpm.count());
172
173 // Render all the models. There's no depth testing in this sample so they're accepted in the
174 // order provided. But the sample EGL setup requests a 24 bit depth buffer so you could
175 // configure it at the end of initRenderer
176 auto start = std::chrono::steady_clock::now();
177
178 if (!heads_.empty()) {
179 for (auto &model : heads_) {
180 model.setRotation(M_PI * 2.0 * spin);
181 shader_->drawModel(model);
182 }
183 }
184
185 auto end = std::chrono::steady_clock::now();
186
187 // Present the rendered image. This is an implicit glFlush.
188 auto swapResult = eglSwapBuffers(display_, surface_);
189 assert(swapResult == EGL_TRUE);
190 return (end - start).count();
191 }
192
initRenderer()193 void Renderer::initRenderer() {
194 // Choose your render attributes
195 constexpr EGLint attribs[] = {EGL_RENDERABLE_TYPE,
196 EGL_OPENGL_ES3_BIT,
197 EGL_SURFACE_TYPE,
198 EGL_WINDOW_BIT,
199 EGL_BLUE_SIZE,
200 8,
201 EGL_GREEN_SIZE,
202 8,
203 EGL_RED_SIZE,
204 8,
205 EGL_DEPTH_SIZE,
206 24,
207 EGL_NONE};
208
209 // The default display is probably what you want on Android
210 auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
211 eglInitialize(display, nullptr, nullptr);
212
213 // figure out how many configs there are
214 EGLint numConfigs;
215 eglChooseConfig(display, attribs, nullptr, 0, &numConfigs);
216
217 // get the list of configurations
218 std::unique_ptr<EGLConfig[]> supportedConfigs(new EGLConfig[numConfigs]);
219 eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);
220
221 // Find a config we like.
222 // Could likely just grab the first if we don't care about anything else in the config.
223 // Otherwise hook in your own heuristic
224 auto config =
225 *std::find_if(supportedConfigs.get(), supportedConfigs.get() + numConfigs,
226 [&display](const EGLConfig &config) {
227 EGLint red, green, blue, depth;
228 if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red) &&
229 eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green) &&
230 eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue) &&
231 eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) {
232 aout << "Found config with " << red << ", " << green << ", "
233 << blue << ", " << depth << std::endl;
234 return red == 8 && green == 8 && blue == 8 && depth == 24;
235 }
236 return false;
237 });
238
239 // create the proper window surface
240 EGLint format;
241 eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
242 EGLSurface surface = eglCreateWindowSurface(display, config, app_->window, nullptr);
243
244 // Create a GLES 3 context
245 EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
246 EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);
247
248 // get some window metrics
249 auto madeCurrent = eglMakeCurrent(display, surface, surface, context);
250 assert(madeCurrent);
251
252 display_ = display;
253 surface_ = surface;
254 context_ = context;
255
256 // make width and height invalid so it gets updated the first frame in @a updateRenderArea()
257 width_ = -1;
258 height_ = -1;
259
260 PRINT_GL_STRING(GL_VENDOR);
261 PRINT_GL_STRING(GL_RENDERER);
262 PRINT_GL_STRING(GL_VERSION);
263 PRINT_GL_STRING_AS_LIST(GL_EXTENSIONS);
264
265 shader_ = std::unique_ptr<Shader>(
266 Shader::loadShader(vertex, fragment, "inPosition", "inUV", "uProjection"));
267 assert(shader_);
268
269 // Note: there's only one shader in this demo, so I'll activate it here. For a more complex game
270 // you'll want to track the active shader and activate/deactivate it as necessary
271 shader_->activate();
272
273 // setup any other gl related global states
274 glClearColor(CORNFLOWER_BLUE);
275
276 // enable alpha globally for now, you probably don't want to do this in a game
277 glEnable(GL_BLEND);
278 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
279
280 // get some demo models into memory
281 // setNumHeads(1000);
282 }
283
updateRenderArea()284 void Renderer::updateRenderArea() {
285 EGLint width;
286 eglQuerySurface(display_, surface_, EGL_WIDTH, &width);
287
288 EGLint height;
289 eglQuerySurface(display_, surface_, EGL_HEIGHT, &height);
290
291 if (width != width_ || height != height_) {
292 width_ = width;
293 height_ = height;
294 glViewport(0, 0, width, height);
295
296 // make sure that we lazily recreate the projection matrix before we render
297 shaderNeedsNewProjectionMatrix_ = true;
298 }
299 }
300
addHead()301 void Renderer::addHead() {
302 thread_local auto assetManager = app_->activity->assetManager;
303 thread_local auto spAndroidRobotTexture = TextureAsset::loadAsset(assetManager, "android.png");
304 thread_local std::vector<Vertex> vertices = {
305 Vertex(Vector3{{0.3, 0.3, 0}}, Vector2{{0, 0}}), // 0
306 Vertex(Vector3{{-0.3, 0.3, 0}}, Vector2{{1, 0}}), // 1
307 Vertex(Vector3{{-0.3, -0.3, 0}}, Vector2{{1, 1}}), // 2
308 Vertex(Vector3{{0.3, -0.3, 0}}, Vector2{{0, 1}}) // 3
309 };
310 thread_local std::vector<Index> indices = {0, 1, 2, 0, 2, 3};
311 thread_local Model baseModel{vertices, indices, spAndroidRobotTexture};
312 float angle = 2 * M_PI * (static_cast<float>(rand()) / static_cast<float>(RAND_MAX));
313 float x = 1.5 * static_cast<float>(rand()) / static_cast<float>(RAND_MAX) - 0.75;
314 float y = 3.0 * static_cast<float>(rand()) / static_cast<float>(RAND_MAX) - 1.5;
315 Vector3 offset{{x, y, 0}};
316 Model toAdd{baseModel};
317 toAdd.move(offset);
318 toAdd.setRotationOffset(angle);
319 heads_.push_back(toAdd);
320 }
321
setNumHeads(int headCount)322 void Renderer::setNumHeads(int headCount) {
323 if (headCount > heads_.size()) {
324 int to_add = headCount - heads_.size();
325 for (int i = 0; i < to_add; ++i) {
326 addHead();
327 }
328 } else if (headCount < heads_.size()) {
329 heads_.erase(heads_.begin() + headCount, heads_.end());
330 }
331 }
332
getAdpfSupported()333 bool Renderer::getAdpfSupported() {
334 if (hintManager_ == nullptr) {
335 hintManager_ = APerformanceHint_getManager();
336 }
337 long preferredRate = APerformanceHint_getPreferredUpdateRateNanos(hintManager_);
338 results_["isHintSessionSupported"] = preferredRate < 0 ? "false" : "true";
339 results_["preferredRate"] = std::to_string(preferredRate);
340 return preferredRate >= 0;
341 }
342
startHintSession(std::vector<int32_t> & tids,int64_t target)343 void Renderer::startHintSession(std::vector<int32_t> &tids, int64_t target) {
344 if (hintManager_ == nullptr) {
345 hintManager_ = APerformanceHint_getManager();
346 }
347 if (hintSession_ == nullptr && hintManager_ != nullptr) {
348 lastTarget_ = target;
349 hintSession_ =
350 APerformanceHint_createSession(hintManager_, tids.data(), tids.size(), target);
351 if (hintSession_ == nullptr) {
352 Utility::setFailure("Failed to create session", this);
353 }
354 }
355 }
356
reportActualWorkDuration(int64_t duration)357 void Renderer::reportActualWorkDuration(int64_t duration) {
358 if (isHintSessionRunning()) {
359 int ret = APerformanceHint_reportActualWorkDuration(hintSession_, duration);
360 if (ret < 0) {
361 Utility::setFailure("Failed to report actual work duration with code " +
362 std::to_string(ret),
363 this);
364 }
365 }
366 }
367
updateTargetWorkDuration(int64_t target)368 void Renderer::updateTargetWorkDuration(int64_t target) {
369 lastTarget_ = target;
370 if (isHintSessionRunning()) {
371 int ret = APerformanceHint_updateTargetWorkDuration(hintSession_, target);
372 if (ret < 0) {
373 Utility::setFailure("Failed to update target duration with code " + std::to_string(ret),
374 this);
375 }
376 }
377 }
378
getTargetWorkDuration()379 int64_t Renderer::getTargetWorkDuration() {
380 return lastTarget_;
381 }
382
isHintSessionRunning()383 bool Renderer::isHintSessionRunning() {
384 return hintSession_ != nullptr;
385 }
386
closeHintSession()387 void Renderer::closeHintSession() {
388 APerformanceHint_closeSession(hintSession_);
389 }
390
addResult(std::string name,std::string value)391 void Renderer::addResult(std::string name, std::string value) {
392 results_[name] = value;
393 }
394
getResults()395 std::map<std::string, std::string> &Renderer::getResults() {
396 return results_;
397 }
398
setBaselineMedian(int64_t median)399 void Renderer::setBaselineMedian(int64_t median) {
400 baselineMedian_ = median;
401 }
402
403 template <typename T>
getMedian(std::vector<T> values)404 T getMedian(std::vector<T> values) {
405 std::sort(values.begin(), values.end());
406 return values[values.size() / 2];
407 }
408
getFrameStats(std::vector<int64_t> & durations,std::vector<int64_t> & intervals,std::string & testName)409 FrameStats Renderer::getFrameStats(std::vector<int64_t> &durations, std::vector<int64_t> &intervals,
410 std::string &testName) {
411 FrameStats stats;
412 // Double precision is int-precise up to 2^52 so we should be fine for this range
413 double sum = std::accumulate(durations.begin(), durations.end(), 0);
414 double mean = static_cast<double>(sum) / static_cast<double>(durations.size());
415 int dropCount = 0;
416 double varianceSum = 0;
417 for (int64_t &duration : durations) {
418 if (isHintSessionRunning() && duration > lastTarget_) {
419 ++dropCount;
420 }
421 varianceSum += (duration - mean) * (duration - mean);
422 }
423 if (durations.size() > 0) {
424 stats.medianWorkDuration = getMedian(durations);
425 }
426 if (intervals.size() > 0) {
427 stats.medianFrameInterval = getMedian(intervals);
428 }
429 stats.deviation = std::sqrt(varianceSum / static_cast<double>(durations.size() - 1));
430 if (isHintSessionRunning()) {
431 stats.exceededCount = dropCount;
432 stats.exceededFraction =
433 static_cast<double>(dropCount) / static_cast<double>(durations.size());
434 stats.efficiency = static_cast<double>(sum) /
435 static_cast<double>(durations.size() * std::min(lastTarget_, baselineMedian_));
436 }
437
438 if (testName.size() > 0) {
439 addResult(testName + "_median", std::to_string(stats.medianWorkDuration));
440 addResult(testName + "_median_interval", std::to_string(stats.medianFrameInterval));
441 addResult(testName + "_deviation", std::to_string(stats.deviation));
442 if (isHintSessionRunning()) {
443 addResult(testName + "_target", std::to_string(getTargetWorkDuration()));
444 addResult(testName + "_target_exceeded_count", std::to_string(*stats.exceededCount));
445 addResult(testName + "_target_exceeded_fraction",
446 std::to_string(*stats.exceededFraction));
447 addResult(testName + "_efficiency", std::to_string(*stats.efficiency));
448 }
449 }
450
451 return stats;
452 }
453