Add selectable GPU backend to wasm debugger

Bug: skia:
Change-Id: If34e08cb320a141cb838caafeffa4f1f69c5234f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/201404
Commit-Queue: Nathaniel Nifong <nifong@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/experimental/wasm-skp-debugger/Makefile b/experimental/wasm-skp-debugger/Makefile
index 51c2d3c..33339c0 100644
--- a/experimental/wasm-skp-debugger/Makefile
+++ b/experimental/wasm-skp-debugger/Makefile
@@ -4,8 +4,6 @@
 	mkdir -p ./debugger/bin
 	cp ../../out/debugger_wasm/debugger.js   ./debugger/bin
 	cp ../../out/debugger_wasm/debugger.wasm ./debugger/bin
-	# only necessary while emscripten's --preload-file is being used for testing
-	cp ../../out/debugger_wasm/debugger.data ./debugger/bin
 
 local-example:
 	rm -rf node_modules/debugger
@@ -17,4 +15,4 @@
 test-continuous:
 	echo "Assuming npm install has been run by user"
 	echo "Also assuming make debug or release has also been run by a user (if needed)"
-	npx karma start ./karma.conf.js --no-single-run --watch-poll
\ No newline at end of file
+	npx karma start ./karma.conf.js --no-single-run --watch-poll
diff --git a/experimental/wasm-skp-debugger/compile.sh b/experimental/wasm-skp-debugger/compile.sh
index ed9cdf1..801436d 100755
--- a/experimental/wasm-skp-debugger/compile.sh
+++ b/experimental/wasm-skp-debugger/compile.sh
@@ -31,6 +31,10 @@
     --output $BASE_DIR/fonts/NotoMono-Regular.ttf.cpp \
     --align 4
 
+GN_GPU_FLAGS="\"-DIS_WEBGL=1\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\","
+WASM_GPU="-lEGL -lGLESv2 -DSK_SUPPORT_GPU=1 \
+          -DSK_DISABLE_LEGACY_SHADERCONTEXT --pre-js $BASE_DIR/cpu.js --pre-js $BASE_DIR/gpu.js"
+
 NINJA=`which ninja`
 
 ./bin/fetch-gn
@@ -42,7 +46,8 @@
   cxx=\"${EMCXX}\" \
   extra_cflags_cc=[\"-frtti\"] \
   extra_cflags=[\"-s\",\"USE_FREETYPE=1\",\"-s\",\"USE_LIBPNG=1\", \"-s\", \"WARN_UNALIGNED=1\",
-    \"-DSKNX_NO_SIMD\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_DAA\"
+    \"-DSKNX_NO_SIMD\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_DAA\",
+    ${GN_GPU_FLAGS}
   ] \
   is_debug=false \
   is_official_build=true \
@@ -111,7 +116,7 @@
     -DSK_DISABLE_AAA \
     -DSK_DISABLE_DAA \
     -std=c++17 \
-    --pre-js $BASE_DIR/cpu.js \
+    $WASM_GPU \
     --post-js $BASE_DIR/ready.js \
     --bind \
     $BASE_DIR/fonts/NotoMono-Regular.ttf.cpp \
@@ -129,6 +134,7 @@
     -s USE_LIBPNG=1 \
     -s WARN_UNALIGNED=1 \
     -s WASM=1 \
+    -s USE_WEBGL2=1 \
     -o $BUILD_DIR/debugger.js
 
 # TODO(nifong): write unit tests
diff --git a/experimental/wasm-skp-debugger/cpu.js b/experimental/wasm-skp-debugger/cpu.js
index 2e5ca81..ddd0ac1 100644
--- a/experimental/wasm-skp-debugger/cpu.js
+++ b/experimental/wasm-skp-debugger/cpu.js
@@ -3,7 +3,7 @@
 (function(DebuggerView){
     // Takes in an html id or a canvas element
     DebuggerView.MakeSWCanvasSurface = function(idOrElement) {
-        var canvas = idOrElement;
+        let canvas = idOrElement;
         if (canvas.tagName !== 'CANVAS') {
           canvas = document.getElementById(idOrElement);
           if (!canvas) {
@@ -12,10 +12,11 @@
         }
       // Maybe better to use clientWidth/height.  See:
       // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
-      var surface = DebuggerView.MakeSurface(canvas.width, canvas.height);
+      let surface = DebuggerView.MakeSurface(canvas.width, canvas.height);
       if (surface) {
         surface._canvas = canvas;
       }
+      console.log('Made HTML Canvas Surface');
       return surface;
     };
 
@@ -26,7 +27,7 @@
 
     DebuggerView.MakeSurface = function(width, height) {
       /* @dict */
-      var imageInfo = {
+      let imageInfo = {
         'width':  width,
         'height': height,
         'colorType': DebuggerView.ColorType.RGBA_8888,
@@ -34,11 +35,11 @@
         // (and those pixels are un-premultiplied, i.e. straight r,g,b,a)
         'alphaType': DebuggerView.AlphaType.Unpremul,
       }
-      var pixelLen = width * height * 4; // it's 8888, so 4 bytes per pixel
+      let pixelLen = width * height * 4; // it's 8888, so 4 bytes per pixel
       // Allocate the buffer of pixels to be drawn into.
-      var pixelPtr = DebuggerView._malloc(pixelLen);
+      let pixelPtr = DebuggerView._malloc(pixelLen);
 
-      var surface = this._getRasterDirectSurface(imageInfo, pixelPtr, width*4);
+      let surface = this._getRasterDirectSurface(imageInfo, pixelPtr, width*4);
       if (surface) {
         surface._canvas = null;
         surface._width = width;
@@ -61,8 +62,8 @@
         // Do we have an HTML canvas to write the pixels to?
         // We will not if this a GPU build or a raster surface, for example.
         if (this._canvas) {
-          var pixels = new Uint8ClampedArray(DebuggerView.buffer, this._pixelPtr, this._pixelLen);
-          var imageData = new ImageData(pixels, this._width, this._height);
+          let pixels = new Uint8ClampedArray(DebuggerView.buffer, this._pixelPtr, this._pixelLen);
+          let imageData = new ImageData(pixels, this._width, this._height);
           this._canvas.getContext('2d').putImageData(imageData, 0, 0);
         }
       };
diff --git a/experimental/wasm-skp-debugger/debugger/index.html b/experimental/wasm-skp-debugger/debugger/index.html
index 469716c..17223f1 100644
--- a/experimental/wasm-skp-debugger/debugger/index.html
+++ b/experimental/wasm-skp-debugger/debugger/index.html
@@ -5,12 +5,12 @@
     <title>Debugger Demo</title>
   <script src="bin/debugger.js"></script>
   <script>
-var index = 0;
+let index = 0;
+let surface;
 
 DebuggerInit({
   locateFile: (file) => '/node_modules/debugger/bin/'+file,
 }).ready().then((Debugger) => {
-  let surface;
   const player = new Debugger.SkpDebugPlayer();
 
   // Define an event handler for the file input dialog
@@ -41,7 +41,8 @@
       canvas = document.getElementById('debugger_view');
       canvas.width = bounds.fRight - bounds.fLeft;
       canvas.height = bounds.fBottom - bounds.fTop;
-      surface = Debugger.MakeSWCanvasSurface('debugger_view');
+      //Assume GPU selected initially
+      surface = Debugger.MakeWebGLCanvasSurface('debugger_view', canvas.width, canvas.height);
 
       document.getElementById('command-count').innerHTML = player.getSize();
       player.setClipVizColor(0);
@@ -51,7 +52,7 @@
 
   function playFile() {
     interval = parseFloat(document.getElementById('interval').value);
-    var intervalID = setInterval(function() {
+    let intervalID = setInterval(function() {
         if (index < 789){
           player.drawTo(surface, index);
           surface.flush();
@@ -60,6 +61,14 @@
     }, interval);
   }
 
+  // Discard canvas when switching between cpu/gpu backend because it's bound to a context.
+  function replaceCanvas() {
+      canvas = document.getElementById('debugger_view');
+      let newCanvas = canvas.cloneNode(true);
+      let parent = canvas.parentNode;
+      parent.replaceChild(newCanvas, canvas);
+  }
+
   document.getElementById('file-input')
     .addEventListener('change', readSkpFile);
 
@@ -99,12 +108,30 @@
       console.log('parsed json');
     });
 
+  document.getElementById('backend_gpu')
+    .addEventListener('change', function(e) {
+      if (e.target.checked) {
+        replaceCanvas();
+        surface = Debugger.MakeWebGLCanvasSurface('debugger_view', canvas.width, canvas.height);
+      }
+    });
+
+  document.getElementById('backend_cpu')
+    .addEventListener('change', function(e) {
+      if (e.target.checked) {
+        replaceCanvas();
+        surface = Debugger.MakeSWCanvasSurface('debugger_view');
+      }
+    });
+
 });
   </script>
   </head>
   <body>
     <canvas id=debugger_view width=400 height=400></canvas>
     <div style="float:right">
+      <input type="radio" name="backend" value="CPU" id="backend_cpu"> CPU
+      <input type="radio" name="backend" value="WebGL" id="backend_gpu" checked> WebGL<br>
       <input type="file" id="file-input" /> | <span id="command-count">0</span> commands<br>
       <input type="button" id="play_button" value="Play" />
       command interval in ms
diff --git a/experimental/wasm-skp-debugger/debugger_bindings.cpp b/experimental/wasm-skp-debugger/debugger_bindings.cpp
index 339bbe2..c278dc0 100644
--- a/experimental/wasm-skp-debugger/debugger_bindings.cpp
+++ b/experimental/wasm-skp-debugger/debugger_bindings.cpp
@@ -14,6 +14,16 @@
 #include <emscripten.h>
 #include <emscripten/bind.h>
 
+#if SK_SUPPORT_GPU
+#include "GrBackendSurface.h"
+#include "GrContext.h"
+#include "GrGLInterface.h"
+#include "GrGLTypes.h"
+
+#include <GL/gl.h>
+#include <emscripten/html5.h>
+#endif
+
 using JSColor = int32_t;
 
 struct SimpleImageInfo {
@@ -113,6 +123,67 @@
     SkIRect fBounds;
 };
 
+#if SK_SUPPORT_GPU
+sk_sp<GrContext> MakeGrContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context)
+{
+    EMSCRIPTEN_RESULT r = emscripten_webgl_make_context_current(context);
+    if (r < 0) {
+        SkDebugf("failed to make webgl context current %d\n", r);
+        return nullptr;
+    }
+    // setup GrContext
+    auto interface = GrGLMakeNativeInterface();
+    // setup contexts
+    sk_sp<GrContext> grContext(GrContext::MakeGL(interface));
+    return grContext;
+}
+
+sk_sp<SkSurface> MakeOnScreenGLSurface(sk_sp<GrContext> grContext, int width, int height) {
+    glClearColor(0, 0, 0, 0);
+    glClearStencil(0);
+    glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+
+    // Wrap the frame buffer object attached to the screen in a Skia render
+    // target so Skia can render to it
+    GrGLint buffer;
+    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &buffer);
+    GrGLFramebufferInfo info;
+    info.fFBOID = (GrGLuint) buffer;
+    SkColorType colorType;
+
+    info.fFormat = GL_RGBA8;
+    colorType = kRGBA_8888_SkColorType;
+
+    GrBackendRenderTarget target(width, height, 0, 8, info);
+
+    sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(grContext.get(), target,
+                                                                    kBottomLeft_GrSurfaceOrigin,
+                                                                    colorType, nullptr, nullptr));
+    return surface;
+}
+
+sk_sp<SkSurface> MakeRenderTarget(sk_sp<GrContext> grContext, int width, int height) {
+    SkImageInfo info = SkImageInfo::MakeN32(width, height, SkAlphaType::kPremul_SkAlphaType);
+
+    sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(grContext.get(),
+                             SkBudgeted::kYes,
+                             info, 0,
+                             kBottomLeft_GrSurfaceOrigin,
+                             nullptr, true));
+    return surface;
+}
+
+sk_sp<SkSurface> MakeRenderTarget(sk_sp<GrContext> grContext, SimpleImageInfo sii) {
+    sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(grContext.get(),
+                             SkBudgeted::kYes,
+                             toSkImageInfo(sii), 0,
+                             kBottomLeft_GrSurfaceOrigin,
+                             nullptr, true));
+    return surface;
+}
+#endif
+
 using namespace emscripten;
 EMSCRIPTEN_BINDINGS(my_module) {
 
@@ -166,4 +237,18 @@
       // Add a optional_override to change it out.
       self.clear(SkColor(color));
     }));
+
+  #if SK_SUPPORT_GPU
+    class_<GrContext>("GrContext")
+        .smart_ptr<sk_sp<GrContext>>("sk_sp<GrContext>");
+    function("currentContext", &emscripten_webgl_get_current_context);
+    function("setCurrentContext", &emscripten_webgl_make_context_current);
+    function("MakeGrContext", &MakeGrContext);
+    function("MakeOnScreenGLSurface", &MakeOnScreenGLSurface);
+    function("MakeRenderTarget", select_overload<sk_sp<SkSurface>(
+      sk_sp<GrContext>, int, int)>(&MakeRenderTarget));
+    function("MakeRenderTarget", select_overload<sk_sp<SkSurface>(
+      sk_sp<GrContext>, SimpleImageInfo)>(&MakeRenderTarget));
+    constant("gpu", true);
+  #endif
 }
diff --git a/experimental/wasm-skp-debugger/gpu.js b/experimental/wasm-skp-debugger/gpu.js
new file mode 100644
index 0000000..61b8787
--- /dev/null
+++ b/experimental/wasm-skp-debugger/gpu.js
@@ -0,0 +1,92 @@
+// Adds compile-time JS functions to augment the DebuggerView interface.
+// Specifically, anything that should only be on the GPU version of DebuggerView.
+(function(DebuggerView){
+    function makeWebGLContext(canvas, attrs) {
+      // These defaults come from the emscripten _emscripten_webgl_create_context
+      // TODO(nifong): All these settings appear to be ignored. investigate.
+      var contextAttributes = {
+        alpha: 1,
+        depth: 1,
+        stencil: 0,
+        antialias: 1,
+        premultipliedAlpha: 1,
+        preserveDrawingBuffer: 0,
+        preferLowPowerToHighPerformance: 0,
+        failIfMajorPerformanceCaveat: 0,
+        majorVersion: 1,
+        minorVersion: 0,
+        enableExtensionsByDefault: 1,
+        explicitSwapControl: 0,
+        renderViaOffscreenBackBuffer: 0,
+      };
+      if (!canvas) {
+        console.log('null canvas passed into makeWebGLContext');
+        return 0;
+      }
+      // This check is from the emscripten version
+      if (contextAttributes['explicitSwapControl']) {
+        console.log('explicitSwapControl is not supported');
+        return 0;
+      }
+      // GL is an enscripten provided helper
+      // See https://github.com/emscripten-core/emscripten/blob/incoming/src/library_webgl.js
+      let context = GL.createContext(canvas, contextAttributes);
+      if (!context) {
+        console.log('Could not get a WebGL context from the canvas element.');
+      }
+      console.log('Made Web Gl Canvas Surface');
+      return context
+    }
+
+    DebuggerView.GetWebGLContext = function(canvas, attrs) {
+      return makeWebGLContext(canvas, attrs);
+    };
+
+    // arg can be of types:
+    //  - String - in which case it is interpreted as an id of a
+    //          canvas element.
+    //  - HTMLCanvasElement - in which the provided canvas element will
+    //          be used directly.
+    // Width and height can be provided to override those on the canvas
+    // element, or specify a height for when a context is provided.
+    DebuggerView.MakeWebGLCanvasSurface = function(arg, width, height) {
+      var canvas = arg;
+      if (canvas.tagName !== 'CANVAS') {
+        canvas = document.getElementById(arg);
+        if (!canvas) {
+          throw 'Canvas with id ' + arg + ' was not found';
+        }
+      }
+      // we are ok with all the defaults
+      var ctx = DebuggerView.GetWebGLContext(canvas);
+
+      if (!ctx || ctx < 0) {
+        throw 'failed to create webgl context: err ' + ctx;
+      }
+
+      if (!canvas && (!width || !height)) {
+        throw 'height and width must be provided with context';
+      }
+
+      var grcontext = this.MakeGrContext(ctx);
+      if (!grcontext) {
+        throw (
+          'failed to create grcontext. Open GL driver may not support all needed functions: err '
+          + grcontext);
+      }
+
+      // Maybe better to use clientWidth/height.  See:
+      // https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html
+      var surface = this.MakeOnScreenGLSurface(grcontext,
+                                               width  || canvas.width,
+                                               height || canvas.height);
+      if (!surface) {
+        // Don't fall back silently in the debugger, the user explicitly controls which backend he
+        // wants via the UI. Calling function may catch this and show the user an error.
+        throw ('Failed to create OpenGL surface. GPU Backend unavailable.');
+      }
+      return surface;
+    };
+    // Default to trying WebGL first.
+    DebuggerView.MakeCanvasSurface = DebuggerView.MakeWebGLCanvasSurface;
+}(Module)); // When this file is loaded in, the high level object is "Module";