Add WebGl2 benchmarking example

This makes it so that we can specify a custom HTML shell file for the
emcc_binary rule.

Change-Id: I919e917d6fd92fed6ab77775c5373d2886edacca
diff --git a/build_tests/BUILD b/build_tests/BUILD
index 07b84fe..bf6e9ee 100644
--- a/build_tests/BUILD
+++ b/build_tests/BUILD
@@ -15,12 +15,15 @@
 
 emcc_binary(
     name = "webgl2.html",
-    srcs = ["webgl2_draw_packed_triangle.c"],
+    srcs = ["webgl2_benchmark.cc"],
+    html_shell = "minimal_shell.html",
     # Enable WEBGL2 (-s is used by the emscripten
     # compiler to specify sundry options).
     linkopts = [
         "-s",
         "USE_WEBGL2=1",
+        "-s",
+        "TOTAL_MEMORY=" + repr(256 * 1024 * 1024),
     ],
 )
 
diff --git a/build_tests/minimal_shell.html b/build_tests/minimal_shell.html
new file mode 100644
index 0000000..6158571
--- /dev/null
+++ b/build_tests/minimal_shell.html
@@ -0,0 +1,128 @@
+<!doctype html>
+<!--This file is adapted from https://github.com/emscripten-core/emscripten/blob/incoming/src/shell_minimal.html-->
+<html lang="en-us">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Emscripten-Generated Code</title>
+    <style>
+      .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
+      div.emscripten { text-align: center; }
+      /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
+      canvas.emscripten {
+        border: 0px none;
+        background-color: black;
+      }
+
+      .spinner {
+        height: 50px;
+        width: 50px;
+        margin: 0px auto;
+        -webkit-animation: rotation .8s linear infinite;
+        -moz-animation: rotation .8s linear infinite;
+        -o-animation: rotation .8s linear infinite;
+        animation: rotation 0.8s linear infinite;
+        border-left: 10px solid rgb(0,150,240);
+        border-right: 10px solid rgb(0,150,240);
+        border-bottom: 10px solid rgb(0,150,240);
+        border-top: 10px solid rgb(100,0,200);
+        border-radius: 100%;
+        background-color: rgb(200,100,250);
+      }
+      @-webkit-keyframes rotation {
+        from {-webkit-transform: rotate(0deg);}
+        to {-webkit-transform: rotate(360deg);}
+      }
+      @-moz-keyframes rotation {
+        from {-moz-transform: rotate(0deg);}
+        to {-moz-transform: rotate(360deg);}
+      }
+      @-o-keyframes rotation {
+        from {-o-transform: rotate(0deg);}
+        to {-o-transform: rotate(360deg);}
+      }
+      @keyframes rotation {
+        from {transform: rotate(0deg);}
+        to {transform: rotate(360deg);}
+      }
+
+    </style>
+  </head>
+  <body>
+    <figure style="overflow:visible;" id="spinner"><div class="spinner"></div><center style="margin-top:0.5em"><strong>emscripten</strong></center></figure>
+    <div class="emscripten" id="status">Downloading...</div>
+    <div class="emscripten">
+      <progress value="0" max="100" id="progress" hidden=1></progress>
+    </div>
+    <!--The width and height values in the canvas specify the pixel width/height for the WebGL canvas.
+        The actual on-screen size is controlled by the stylesheet.-->
+    <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" width=3000 height=2000></canvas>
+
+    <script type='text/javascript'>
+      var statusElement = document.getElementById('status');
+      var progressElement = document.getElementById('progress');
+      var spinnerElement = document.getElementById('spinner');
+
+      var Module = {
+        preRun: [],
+        postRun: [],
+        print: (function() {
+          return function(text) {
+            if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+            console.log(text);
+          };
+        })(),
+        printErr: function(text) {
+          if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+          console.error(text);
+        },
+        canvas: (function() {
+          var canvas = document.getElementById('canvas');
+
+          // As a default initial behavior, pop up an alert when webgl context is lost. To make your
+          // application robust, you may want to override this behavior before shipping!
+          // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
+          canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
+
+          return canvas;
+        })(),
+        setStatus: function(text) {
+          if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
+          if (text === Module.setStatus.last.text) return;
+          var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
+          var now = Date.now();
+          if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
+          Module.setStatus.last.time = now;
+          Module.setStatus.last.text = text;
+          if (m) {
+            text = m[1];
+            progressElement.value = parseInt(m[2])*100;
+            progressElement.max = parseInt(m[4])*100;
+            progressElement.hidden = false;
+            spinnerElement.hidden = false;
+          } else {
+            progressElement.value = null;
+            progressElement.max = null;
+            progressElement.hidden = true;
+            if (!text) spinnerElement.hidden = true;
+          }
+          statusElement.innerHTML = text;
+        },
+        totalDependencies: 0,
+        monitorRunDependencies: function(left) {
+          this.totalDependencies = Math.max(this.totalDependencies, left);
+          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
+        }
+      };
+      Module.setStatus('Downloading...');
+      window.onerror = function() {
+        Module.setStatus('Exception thrown, see JavaScript console');
+        spinnerElement.style.display = 'none';
+        Module.setStatus = function(text) {
+          if (text) Module.printErr('[post-exception status] ' + text);
+        };
+      };
+    </script>
+    {{{ SCRIPT }}}
+  </body>
+</html>
diff --git a/build_tests/webgl2_benchmark.cc b/build_tests/webgl2_benchmark.cc
new file mode 100644
index 0000000..f9f15fc
--- /dev/null
+++ b/build_tests/webgl2_benchmark.cc
@@ -0,0 +1,161 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <cmath>
+#include <emscripten/emscripten.h>
+#include <emscripten/html5.h>
+#include <iostream>
+#include <unistd.h>
+#include <GLES3/gl3.h>
+
+namespace {
+constexpr int kNPoints = 10 * 1000 * 1000;
+}  // namespace
+
+// Shader and program construction taken from examples at
+// https://github.com/emscripten-core/emscripten/blob/incoming/tests/webgl2_draw_packed_triangle.c
+GLuint compile_shader(GLenum shaderType, const char *src) {
+  GLuint shader = glCreateShader(shaderType);
+  glShaderSource(shader, 1, &src, NULL);
+  glCompileShader(shader);
+
+  GLint isCompiled = 0;
+  glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
+  if (!isCompiled) {
+    GLint maxLength = 0;
+    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
+    char *buf = (char *)malloc(maxLength + 1);
+    glGetShaderInfoLog(shader, maxLength, &maxLength, buf);
+    printf("%s\n", buf);
+    free(buf);
+    return 0;
+  }
+
+  return shader;
+}
+
+GLuint create_program(GLuint vertexShader, GLuint fragmentShader) {
+   GLuint program = glCreateProgram();
+   glAttachShader(program, vertexShader);
+   glAttachShader(program, fragmentShader);
+   glBindAttribLocation(program, 0, "apos");
+   glBindAttribLocation(program, 1, "acolor");
+   glLinkProgram(program);
+   return program;
+}
+
+struct Vector {
+  int x;
+  int y;
+  int z;
+  int w;
+};
+
+// Packs a vector for use with GL_INT_2_10_10_10_REV.
+uint32_t PackVector(const Vector &vec) {
+  uint32_t retval = 0;
+  retval = vec.w;
+  retval <<= 10;
+  retval |= vec.z & 0x3FF;
+  retval <<= 10;
+  retval |= vec.y & 0x3FF;
+  retval <<= 10;
+  retval |= vec.x & 0x3FF;
+  return retval;
+}
+
+struct AnimationState {
+  // The time, in seconds, at which the last animation frame occurred.
+  double last_animation_time = 0.0;
+  // The location for the "scale" uniform to modify on each animation call.
+  GLint scale_uniform_location;
+};
+
+// This function modifies the "scale" uniform to vary from 0.5->1.0->0.5
+// in a cycle, and redraws the points on each iteration.
+int Redraw(double time, void *data) {
+  AnimationState *state = reinterpret_cast<AnimationState*>(data);
+  time /= 1000.0;
+  const double difftime = time - state->last_animation_time;
+  const double wrap_time = std::fmod(time, 1.0);
+  const double offset = wrap_time > 0.5 ? 1.0 - wrap_time : wrap_time;
+  glUniform1f(state->scale_uniform_location, std::min(0.5 + offset, 100.0));
+  std::cout << 1.0 / difftime  << "fps\n";
+  glDrawArrays(GL_POINTS, 0, kNPoints);
+  state->last_animation_time = time;
+  assert(glGetError() == GL_NO_ERROR && "glDrawArray failed");
+  return 1;
+}
+
+int main() {
+  EmscriptenWebGLContextAttributes attr;
+  emscripten_webgl_init_context_attributes(&attr);
+  attr.majorVersion = 2;
+  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attr);
+  assert(ctx && "Failed to create WebGL2 context");
+  emscripten_webgl_make_context_current(ctx);
+
+  static const char vertex_shader[] =
+    "#version 100\n"
+    "attribute vec4 apos;"
+    "attribute vec4 acolor;"
+    "varying vec4 color;"
+    "uniform float scale;"
+    "void main() {"
+      "color = acolor;"
+      "gl_Position = apos;"
+      "gl_Position.x = apos.x * scale;"
+      "gl_Position.y = apos.y * 0.5 / scale;"
+      "gl_PointSize = 1.0;"
+    "}";
+  GLuint vs = compile_shader(GL_VERTEX_SHADER, vertex_shader);
+
+  static const char fragment_shader[] =
+    "#version 100\n"
+    "precision lowp float;"
+    "varying vec4 color;"
+    "void main() {"
+      "gl_FragColor = color;"
+    "}";
+  GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fragment_shader);
+
+  GLuint program = create_program(vs, fs);
+  glUseProgram(program);
+
+  // Go through and generate randomly located and colored points.
+  static uint32_t pos_and_color[kNPoints * 2];
+  for (int ii = 0; ii < kNPoints; ++ii) {
+    uint16_t w = 1;
+    uint16_t z = 0;
+    uint16_t y = rand() % 1024;
+    uint16_t x = rand() % 1024;
+    uint32_t wzyx = PackVector({x, y, z, w});
+    uint16_t a = 3;
+    uint16_t b = rand() % 1024;
+    uint16_t g = rand() % 1024;
+    uint16_t r = rand() % 1024;
+    uint32_t argb = PackVector({r, g, b, a});
+    pos_and_color[2 * ii] = wzyx;
+    pos_and_color[2 * ii + 1] = argb;
+  }
+
+  GLuint vbo;
+  glGenBuffers(1, &vbo);
+  glBindBuffer(GL_ARRAY_BUFFER, vbo);
+  glBufferData(GL_ARRAY_BUFFER, sizeof(pos_and_color), pos_and_color, GL_STATIC_DRAW);
+  glVertexAttribPointer(0, 4, GL_INT_2_10_10_10_REV, GL_TRUE, 8, 0);
+  assert(glGetError() == GL_NO_ERROR && "glVertexAttribPointer with GL_INT_2_10_10_10_REV failed");
+  glVertexAttribPointer(1, 4, GL_UNSIGNED_INT_2_10_10_10_REV, GL_TRUE, 8, (void*)4);
+  assert(glGetError() == GL_NO_ERROR && "glVertexAttribPointer with GL_UNSIGNED_INT_2_10_10_10_REV failed");
+
+  glEnableVertexAttribArray(0);
+  glEnableVertexAttribArray(1);
+
+  // Note that the animation_state must last until Redraw stops being called,
+  // which we cannot provide any bound on. As such, we don't currently destroy
+  // the memory until the webpage is closed.
+  AnimationState *animation_state = new AnimationState();
+  animation_state->scale_uniform_location =
+      glGetUniformLocation(program, "scale");
+  emscripten_request_animation_frame_loop(&Redraw, animation_state);
+}
diff --git a/build_tests/webgl2_draw_packed_triangle.c b/build_tests/webgl2_draw_packed_triangle.c
deleted file mode 100644
index 5dd74d7..0000000
--- a/build_tests/webgl2_draw_packed_triangle.c
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2018 The Emscripten Authors.  All rights reserved.
- * Emscripten is available under two separate licenses, the MIT license and the
- * University of Illinois/NCSA Open Source License.  Both these licenses can be
- * found in the LICENSE file.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <assert.h>
-#include <emscripten/emscripten.h>
-#include <emscripten/html5.h>
-#include <GLES3/gl3.h>
-
-GLuint compile_shader(GLenum shaderType, const char *src)
-{
-  GLuint shader = glCreateShader(shaderType);
-  glShaderSource(shader, 1, &src, NULL);
-  glCompileShader(shader);
-
-  GLint isCompiled = 0;
-  glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
-  if (!isCompiled)
-  {
-    GLint maxLength = 0;
-    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
-    char *buf = (char*)malloc(maxLength+1);
-    glGetShaderInfoLog(shader, maxLength, &maxLength, buf);
-    printf("%s\n", buf);
-    free(buf);
-    return 0;
-  }
-
-   return shader;
-}
-
-GLuint create_program(GLuint vertexShader, GLuint fragmentShader)
-{
-   GLuint program = glCreateProgram();
-   glAttachShader(program, vertexShader);
-   glAttachShader(program, fragmentShader);
-   glBindAttribLocation(program, 0, "apos");
-   glBindAttribLocation(program, 1, "acolor");
-   glLinkProgram(program);
-   return program;
-}
-
-int main()
-{
-  EmscriptenWebGLContextAttributes attr;
-  emscripten_webgl_init_context_attributes(&attr);
-#ifdef EXPLICIT_SWAP
-  attr.explicitSwapControl = 1;
-#endif
-  attr.majorVersion = 2;
-  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attr);
-  assert(ctx && "Failed to create WebGL2 context");
-  emscripten_webgl_make_context_current(ctx);
-
-  static const char vertex_shader[] =
-    "#version 100\n"
-    "attribute vec4 apos;"
-    "attribute vec4 acolor;"
-    "varying vec4 color;"
-    "void main() {"
-      "color = acolor;"
-      "gl_Position = apos;"
-    "}";
-  GLuint vs = compile_shader(GL_VERTEX_SHADER, vertex_shader);
-
-  static const char fragment_shader[] =
-    "#version 100\n"
-    "precision lowp float;"
-    "varying vec4 color;"
-    "void main() {"
-      "gl_FragColor = color;"
-    "}";
-  GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fragment_shader);
-
-  GLuint program = create_program(vs, fs);
-  glUseProgram(program);
-
-  static const uint32_t pos_and_color[] = {
-  //  1,0,y,x,    a,b,g,r
-    0x400b36cd, 0xc00003ff,
-    0x400b3533, 0xc00ffc00,
-    0x4004cc00, 0xfff00000,
-  };
-
-  GLuint vbo;
-  glGenBuffers(1, &vbo);
-  glBindBuffer(GL_ARRAY_BUFFER, vbo);
-  glBufferData(GL_ARRAY_BUFFER, sizeof(pos_and_color), pos_and_color, GL_STATIC_DRAW);
-  glVertexAttribPointer(0, 4, GL_INT_2_10_10_10_REV, GL_TRUE, 8, 0);
-//  printf("%d\n", glGetError());
-#if 0
-  switch (glGetError()) {
-    case GL_NO_ERROR:
-      std::cout << "No error!";
-      break;
-    case GL_INVALID_ENUM:
-      std::cout << "Invliad enum.";
-      break;
-  }
-#endif
-  assert(glGetError() == GL_NO_ERROR && "glVertexAttribPointer with GL_INT_2_10_10_10_REV failed");
-  glVertexAttribPointer(1, 4, GL_UNSIGNED_INT_2_10_10_10_REV, GL_TRUE, 8, (void*)4);
-  assert(glGetError() == GL_NO_ERROR && "glVertexAttribPointer with GL_UNSIGNED_INT_2_10_10_10_REV failed");
-
-  glEnableVertexAttribArray(0);
-  glEnableVertexAttribArray(1);
-
-  glClearColor(0.3f,0.3f,0.3f,1);
-  glClear(GL_COLOR_BUFFER_BIT);
-  glDrawArrays(GL_TRIANGLES, 0, 3);
-
-#ifdef EXPLICIT_SWAP
-  emscripten_webgl_commit_frame();
-#endif
-
-#ifdef REPORT_RESULT
-  REPORT_RESULT(0);
-#endif
-}