SkQP: make_gmkb, gm_knowledge (GM Knowledgebase)

Add a real implementation for gm_knowledge.h  This depends on
the presence of files in the form $GMK_DIR/foo/{max,min}.png

The implementation also writes out failures in a report directory.

Add a utility: experimental/make_gmkb which is a stand-alone
go executable that generates the foo/{max,min}.png data.

tools/skqp/README.md has instructions on running SkQP.

Also: add SkFontMgrPriv.h

Change-Id: Ibe1e9a7e7de143d14eee3877f5f2d2d8713f7f49
Reviewed-on: https://skia-review.googlesource.com/65380
Reviewed-by: Yuqian Li <liyuqian@google.com>
Commit-Queue: Hal Canary <halcanary@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 4d1850a..b03f111 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1701,19 +1701,30 @@
     ]
   }
 
-  if (!is_win) {
-    test_app("gpucts") {
+  if (!is_win && skia_enable_gpu) {
+    test_lib("skqp_lib") {
+      public_include_dirs = [ "tools/skqp" ]
       sources = [
+        "dm/DMFontMgr.cpp",
         "dm/DMGpuTestProcs.cpp",
-        "tools/gpucts/gm_knowledge.c",
-        "tools/gpucts/gm_runner.cpp",
-        "tools/gpucts/gpucts.cpp",
+        "tools/skqp/gm_knowledge.cpp",
+        "tools/skqp/gm_runner.cpp",
       ]
       deps = [
         ":gm",
         ":gpu_tool_utils",
         ":skia",
         ":tests",
+        ":tool_utils",
+      ]
+    }
+    test_app("skqp") {
+      sources = [
+        "tools/skqp/skqp.cpp",
+      ]
+      deps = [
+        ":skia",
+        ":skqp_lib",
         "//third_party/googletest",
       ]
     }
diff --git a/dm/DM.cpp b/dm/DM.cpp
index 4deaa2f..058fb9d 100644
--- a/dm/DM.cpp
+++ b/dm/DM.cpp
@@ -22,10 +22,11 @@
 #include "SkCommonFlagsGpuThreads.h"
 #include "SkCommonFlagsPathRenderer.h"
 #include "SkData.h"
-#include "SkDocument.h"
 #include "SkDebugfTracer.h"
+#include "SkDocument.h"
 #include "SkEventTracingPriv.h"
 #include "SkFontMgr.h"
+#include "SkFontMgrPriv.h"
 #include "SkGraphics.h"
 #include "SkHalf.h"
 #include "SkLeanWindows.h"
@@ -1299,9 +1300,6 @@
 
 extern sk_sp<SkTypeface> (*gCreateTypefaceDelegate)(const char [], SkFontStyle );
 
-extern sk_sp<SkFontMgr> (*gSkFontMgr_DefaultFactory)();
-
-
 int main(int argc, char** argv) {
     SkCommandLineFlags::Parse(argc, argv);
 
diff --git a/platform_tools/android/.gitignore b/platform_tools/android/.gitignore
index cb3f2d6..26e14aa 100644
--- a/platform_tools/android/.gitignore
+++ b/platform_tools/android/.gitignore
@@ -9,3 +9,4 @@
 app/bin
 app/gen
 app/lint.xml
+/apps/skqp/src/main/assets
diff --git a/src/core/SkFontMgrPriv.h b/src/core/SkFontMgrPriv.h
new file mode 100644
index 0000000..ba37015
--- /dev/null
+++ b/src/core/SkFontMgrPriv.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef SkFontMgrPriv_DEFINED
+#define SkFontMgrPriv_DEFINED
+
+#include "SkFontMgr.h"
+
+extern sk_sp<SkFontMgr> (*gSkFontMgr_DefaultFactory)();
+
+#endif  // SkFontMgrPriv_DEFINED
diff --git a/tools/gpucts/gm_knowledge.c b/tools/gpucts/gm_knowledge.c
deleted file mode 100644
index 8dbdd00..0000000
--- a/tools/gpucts/gm_knowledge.c
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "gm_knowledge.h"
-
-// placeholder function definitions:
-float GMK_Check(GMK_ImageData d, const char* n) { return 0; }
-bool GMK_IsGoodGM(const char* n) { return true; }
diff --git a/tools/gpucts/gm_knowledge.h b/tools/gpucts/gm_knowledge.h
deleted file mode 100644
index d9d71e0..0000000
--- a/tools/gpucts/gm_knowledge.h
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-#ifndef gm_knowledge_DEFINED
-#define gm_knowledge_DEFINED
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include <stdint.h>
-#include <stdbool.h>
-
-/**
-A structure representing an image.  pix should either be nullptr (representing
-a missing image) or point to a block of memory width*height in size.
-
-Each pixel is an un-pre-multiplied RGBA color:
-    void set_color(GMK_ImageData* data, int x, int y,
-                   unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
-        data->pix[x + data->width * y] = (r << 0) | (g << 8) | (b << 16) | (a << 24);
-    }
- */
-typedef struct {
-    const uint32_t* pix;
-    int width;
-    int height;
-} GMK_ImageData;
-
-/**
-Check if the given test image matches the expected results.
-
-@param data     the image
-@param gm_name  the name of the rendering test that produced the image
-
-@return 0 if the test passes, otherwise a positive number representing how
-         badly it failed.
- */
-float GMK_Check(GMK_ImageData data, const char* gm_name);
-
-/**
-Check to see if the given test has expected results.
-
-@param gm_name  the name of a rendering test.
-
-@return true of expected results are known for the given test.
-*/
-bool GMK_IsGoodGM(const char* gm_name);
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif  // gm_knowledge_DEFINED
diff --git a/tools/gpucts/gm_runner.cpp b/tools/gpucts/gm_runner.cpp
deleted file mode 100644
index a18c457..0000000
--- a/tools/gpucts/gm_runner.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "gm_runner.h"
-
-#include <algorithm>
-
-#include "SkSurface.h"
-#include "gm.h"
-
-#if SK_SUPPORT_GPU
-
-#include "GrContextFactory.h"
-
-using sk_gpu_test::GrContextFactory;
-
-namespace gm_runner {
-
-static GrContextFactory::ContextType to_context_type(SkiaBackend backend) {
-    switch (backend) {
-        case SkiaBackend::kGL:     return GrContextFactory::kGL_ContextType;
-        case SkiaBackend::kGLES:   return GrContextFactory::kGLES_ContextType;
-        case SkiaBackend::kVulkan: return GrContextFactory::kVulkan_ContextType;
-    }
-    SkDEBUGFAIL(""); return (GrContextFactory::ContextType)0;
-}
-
-const char* GetBackendName(SkiaBackend backend) {
-    return GrContextFactory::ContextTypeName(to_context_type(backend));
-}
-
-bool BackendSupported(SkiaBackend backend) {
-    GrContextFactory factory;
-    return factory.get(to_context_type(backend)) != nullptr;
-}
-
-
-GMK_ImageData Evaluate(SkiaBackend backend,
-                       GMFactory gmFact,
-                       std::vector<uint32_t>* storage) {
-    SkASSERT(gmFact);
-    SkASSERT(storage);
-    std::unique_ptr<skiagm::GM> gm(gmFact(nullptr));
-    SkASSERT(gm.get());
-    int w = SkScalarRoundToInt(gm->width());
-    int h = SkScalarRoundToInt(gm->height());
-    GrContextFactory contextFactory;
-    GrContext* context = contextFactory.get(to_context_type(backend));
-    if (!context) {
-        return GMK_ImageData{nullptr, w, h};
-    }
-    SkASSERT(context);
-    constexpr SkColorType ct = kRGBA_8888_SkColorType;
-
-    sk_sp<SkSurface> s = SkSurface::MakeRenderTarget(
-            context, SkBudgeted::kNo, SkImageInfo::Make(w, h, ct, kPremul_SkAlphaType));
-    if (!s) {
-        return GMK_ImageData{nullptr, w, h};
-    }
-    gm->draw(s->getCanvas());
-
-    storage->resize(w * h);
-    uint32_t* pix = storage->data();
-    SkASSERT(SkColorTypeBytesPerPixel(ct) == sizeof(uint32_t));
-    SkAssertResult(s->readPixels(SkImageInfo::Make(w, h, ct, kUnpremul_SkAlphaType),
-                                 pix, w * sizeof(uint32_t), 0, 0));
-    return GMK_ImageData{pix, w, h};
-}
-
-}  // namespace gm_runner
-
-#else
-namespace sk_gpu_test {
-    class GrContextFactory {};
-}
-namespace gm_runner {
-bool BackendSupported(SkiaBackend) { return false; }
-GMK_ImageData Evaluate(SkiaBackend, GMFactory, std::vector<uint32_t>*) {
-    return GMK_ImageData{nullptr, 0, 0};
-}
-const char* GetBackendName(SkiaBackend backend) { return "Unknown"; }
-}  // namespace gm_runner
-#endif
-
-namespace gm_runner {
-
-std::vector<GMFactory> GetGMFactories() {
-    std::vector<GMFactory> result;
-    for (const skiagm::GMRegistry* r = skiagm::GMRegistry::Head(); r; r = r->next()) {
-        result.push_back(r->factory());
-    }
-    struct {
-        bool operator()(GMFactory u, GMFactory v) const { return GetGMName(u) < GetGMName(v); }
-    } less;
-    std::sort(result.begin(), result.end(), less);
-    return result;
-}
-
-std::string GetGMName(GMFactory gmFactory) {
-    SkASSERT(gmFactory);
-    std::unique_ptr<skiagm::GM> gm(gmFactory(nullptr));
-    SkASSERT(gm);
-    return std::string(gm->getName());
-}
-}  // namespace gm_runner
diff --git a/tools/gpucts/gm_runner.h b/tools/gpucts/gm_runner.h
deleted file mode 100644
index cd0d7d3..0000000
--- a/tools/gpucts/gm_runner.h
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-#ifndef gm_runner_DEFINED
-#define gm_runner_DEFINED
-
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "gm_knowledge.h"
-
-/**
-A Skia GM is a single rendering test that can be executed on any Skia backend Canvas.
-*/
-namespace skiagm {
-   class GM;
-}
-
-namespace sk_gpu_test {
-    class GrContextFactory;
-}
-
-namespace gm_runner {
-
-using GMFactory = skiagm::GM* (*)(void*);
-
-enum class SkiaBackend {
-    kGL,
-    kGLES,
-    kVulkan,
-};
-
-bool BackendSupported(SkiaBackend);
-
-/**
-@return a list of all Skia GMs in lexicographic order.
-*/
-std::vector<GMFactory> GetGMFactories();
-
-/**
-@return a descriptive name for the GM.
-*/
-std::string GetGMName(GMFactory);
-/**
-@return a descriptive name for the backend.
-*/
-const char* GetBackendName(SkiaBackend);
-
-/**
-Execute the given GM on the given Skia backend.  Then copy the pixels into the
-storage (overwriting existing contents of storage).
-
-@return the rendered image.  Return a null ImageData on error.
-*/
-GMK_ImageData Evaluate(SkiaBackend,
-                       GMFactory,
-                       std::vector<uint32_t>* storage);
-
-}  // namespace gm_runner
-
-#endif  // gm_runner_DEFINED
diff --git a/tools/gpucts/gpucts.cpp b/tools/gpucts/gpucts.cpp
deleted file mode 100644
index c50519c..0000000
--- a/tools/gpucts/gpucts.cpp
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright 2017 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "SkGraphics.h"
-#include "gm_runner.h"
-
-#ifdef __clang__
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wused-but-marked-unused"
-#endif
-
-#include "gtest/gtest.h"
-
-#ifdef __clang__
-#pragma clang diagnostic pop
-#endif
-
-#include "Test.h"
-
-////////////////////////////////////////////////////////////////////////////////
-
-struct GMTestCase {
-    gm_runner::GMFactory fGMFactory;
-    gm_runner::SkiaBackend fBackend;
-};
-
-struct GMTest : public testing::Test {
-    GMTestCase fTest;
-    GMTest(GMTestCase t) : fTest(t) {}
-    void TestBody() override {
-        if (!fTest.fGMFactory) {
-            EXPECT_TRUE(gm_runner::BackendSupported(fTest.fBackend));
-            return;
-        }
-        std::vector<uint32_t> pixels;
-        GMK_ImageData imgData = gm_runner::Evaluate(fTest.fBackend, fTest.fGMFactory, &pixels);
-        EXPECT_TRUE(imgData.pix);
-        if (!imgData.pix) {
-            return;
-        }
-        std::string gmName = gm_runner::GetGMName(fTest.fGMFactory);
-        float result = GMK_Check(imgData, gmName.c_str());
-        EXPECT_EQ(result, 0);
-    }
-};
-
-struct GMTestFactory : public testing::internal::TestFactoryBase {
-    GMTestCase fTest;
-    GMTestFactory(GMTestCase t) : fTest(t) {}
-    testing::Test* CreateTest() override { return new GMTest(fTest); }
-};
-
-////////////////////////////////////////////////////////////////////////////////
-
-#if !SK_SUPPORT_GPU
-struct GrContextOptions {};
-#endif
-
-struct UnitTest : public testing::Test {
-    skiatest::TestProc fProc;
-    UnitTest(skiatest::TestProc proc) : fProc(proc) {}
-    void TestBody() override {
-        struct : skiatest::Reporter {
-            void reportFailed(const skiatest::Failure& failure) override {
-                SkString desc = failure.toString();
-                SK_ABORT("");
-                GTEST_NONFATAL_FAILURE_(desc.c_str());
-            }
-        } r;
-        fProc(&r, GrContextOptions());
-    }
-};
-
-struct UnitTestFactory : testing::internal::TestFactoryBase {
-    skiatest::TestProc fProc;
-    UnitTestFactory(skiatest::TestProc proc) : fProc(proc) {}
-    testing::Test* CreateTest() override { return new UnitTest(fProc); }
-};
-
-std::vector<const skiatest::Test*> GetUnitTests() {
-    // Unit Tests
-    std::vector<const skiatest::Test*> tests;
-    for (const skiatest::TestRegistry* r = skiatest::TestRegistry::Head(); r; r = r->next()) {
-        tests.push_back(&r->factory());
-    }
-    struct {
-        bool operator()(const skiatest::Test* u, const skiatest::Test* v) const {
-            return strcmp(u->name, v->name) < 0;
-        }
-    } less;
-    std::sort(tests.begin(), tests.end(), less);
-    return tests;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-static void reg_test(const char* test, const char* testCase,
-                     testing::internal::TestFactoryBase* fact) {
-    testing::internal::MakeAndRegisterTestInfo(
-                        test,
-                        testCase,
-                        nullptr,
-                        nullptr,
-                        testing::internal::CodeLocation(__FILE__, __LINE__),
-                        testing::internal::GetTestTypeId(),
-                        testing::Test::SetUpTestCase,
-                        testing::Test::TearDownTestCase,
-                        fact);
-}
-
-int main(int argc, char** argv) {
-    testing::InitGoogleTest(&argc, argv);
-    SkGraphics::Init();
-
-    // Rendering Tests
-    gm_runner::SkiaBackend backends[] = {
-        #ifndef SK_BUILD_FOR_ANDROID
-        gm_runner::SkiaBackend::kGL,  // Used for testing on desktop machines.
-        #endif
-        gm_runner::SkiaBackend::kGLES,
-        gm_runner::SkiaBackend::kVulkan,
-    };
-    std::vector<gm_runner::GMFactory> gms = gm_runner::GetGMFactories();
-    for (auto backend : backends) {
-        const char* backendName = GetBackendName(backend);
-        std::string test = std::string("SkiaGM_") + backendName;
-        reg_test(test.c_str(), "BackendSupported", new GMTestFactory(GMTestCase{nullptr, backend}));
-
-        if (!gm_runner::BackendSupported(backend)) {
-            continue;
-        }
-        for (auto gmFactory : gms) {
-            std::string gmName = gm_runner::GetGMName(gmFactory);
-            if (!GMK_IsGoodGM(gmName.c_str())) {
-                continue;
-            }
-            #ifdef SK_DEBUG
-                // The following test asserts on my phone.
-                // TODO(halcanary):  fix this.
-                if(gmName == std::string("complexclip3_simple") &&
-                   backend == gm_runner::SkiaBackend::kGLES) {
-                    continue;
-                }
-            #endif
-                reg_test(test.c_str(), gmName.c_str(),
-                         new GMTestFactory(GMTestCase{gmFactory, backend}));
-      }
-    }
-
-    for (const skiatest::Test* test : GetUnitTests()) {
-        reg_test("Skia_Unit_Tests", test->name, new UnitTestFactory(test->proc));
-    }
-    return RUN_ALL_TESTS();
-}
-
diff --git a/tools/ok_vias.cpp b/tools/ok_vias.cpp
index 7c24664..c1f8d93 100644
--- a/tools/ok_vias.cpp
+++ b/tools/ok_vias.cpp
@@ -6,6 +6,7 @@
  */
 
 #include "../dm/DMFontMgr.h"
+#include "../src/core/SkFontMgrPriv.h"
 #include "ProcStats.h"
 #include "SkColorFilter.h"
 #include "SkEventTracingPriv.h"
@@ -292,8 +293,6 @@
                       "enable tracing in mode=atrace, mode=debugf, or mode=trace.json",
                       Trace::Create};
 
-extern sk_sp<SkFontMgr> (*gSkFontMgr_DefaultFactory)();
-
 struct PortableFonts : Dst {
     std::unique_ptr<Dst> target;
 
diff --git a/tools/skqp/README.md b/tools/skqp/README.md
new file mode 100644
index 0000000..beac911
--- /dev/null
+++ b/tools/skqp/README.md
@@ -0,0 +1,66 @@
+
+SkQP
+====
+
+**Motivation**: Test an Android device’s GPU and OpenGLES & Vulkan drivers with
+Skia and Skia’s existing unit & rendering tests.
+
+How To Use SkQP on your Android device:
+
+1.  To build SkQP you need to install the
+    [Android NDK](https://developer.android.com/ndk/).
+
+2.  Checkout Skia, then go to the source directory:
+
+        cd $SKIA_SOURCE_DIRECTORY
+
+3.  Configure and build Skia for your device's architecture:
+
+        arch='arm64'  # Also valid: 'arm', 'x68', 'x64'
+        android_ndk="${HOME}/ndk"  # Or wherever you installed the NDK.
+        mkdir -p out/${arch}-rel
+        cat > out/${arch}-rel/args.gn << EOF
+            ndk = "$android_ndk"
+            ndk_api = 24
+            target_cpu = "$arch"
+            skia_embed_resources = true
+            is_debug = false
+        EOF
+        tools/git-sync-deps
+        bin/gn gen out/${arch}-rel
+        ninja -C out/${arch}-rel skqp_lib
+
+4.  Download meta.json from [https://goo.gl/jBw3Dd](https://goo.gl/jBw3Dd) .
+    This is the data used to build the validation model.
+
+5.  Generate the validation model data:
+
+        rm -rf platform_tools/android/apps/skqp/src/main/assets/gmkb
+        go get go.skia.org/infra/golden/go/search
+        go run tools/skqp/make_gmkb.go ~/Downloads/meta.json \
+            platform_tools/android/apps/skqp/src/main/assets/gmkb
+
+Run as an executable
+--------------------
+
+1.  Build the SkQP program, load files on the device, and run skqp:
+
+        ninja -C out/${arch}-rel skqp
+        adb shell "cd /data/local/tmp; rm -rf gmkb report"
+        adb push platform_tools/android/apps/skqp/src/main/assets/gmkb \
+            /data/local/tmp/
+        adb push out/${arch}-rel/skqp /data/local/tmp/
+        adb shell "cd /data/local/tmp; ./skqp gmkb report"
+
+2.  Produce a one-page error report if there are errors:
+
+        rm -rf /tmp/report
+        if adb shell test -d /data/local/tmp/report; then
+            adb pull /data/local/tmp/report /tmp/
+            tools/skqp/make_report.py /tmp/report
+        fi
+
+Run as an APK
+-------------
+
+[TODO]
diff --git a/tools/skqp/gm_knowledge.cpp b/tools/skqp/gm_knowledge.cpp
new file mode 100644
index 0000000..bc2ca82
--- /dev/null
+++ b/tools/skqp/gm_knowledge.cpp
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm_knowledge.h"
+
+#include <cfloat>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "../../src/core/SkStreamPriv.h"
+#include "SkBitmap.h"
+#include "SkCodec.h"
+#include "SkOSFile.h"
+#include "SkPngEncoder.h"
+#include "SkStream.h"
+
+#include "skqp_asset_manager.h"
+
+#define PATH_MAX_PNG "max.png"
+#define PATH_MIN_PNG "min.png"
+#define PATH_IMG_PNG "image.png"
+#define PATH_ERR_PNG "errors.png"
+#define PATH_REPORT  "report.html"
+
+////////////////////////////////////////////////////////////////////////////////
+inline void path_join_append(std::ostringstream* o) { }
+
+template<class... Types>
+void path_join_append(std::ostringstream* o, const char* v, Types... args) {
+    constexpr char kPathSeparator[] = "/";
+    *o << kPathSeparator << v;
+    path_join_append(o, args...);
+}
+
+template<class... Types>
+std::string path_join(const char* v, Types... args) {
+    std::ostringstream o;
+    o << v;
+    path_join_append(&o, args...);
+    return o.str();
+}
+template<class... Types>
+std::string path_join(const std::string& v, Types... args) {
+    return path_join(v.c_str(), args...);
+}
+////////////////////////////////////////////////////////////////////////////////
+
+static int get_error(uint32_t value, uint32_t value_max, uint32_t value_min) {
+    int error = 0;
+    for (int j : {0, 8, 16, 24}) {
+        uint8_t    v = (value     >> j) & 0xFF,
+                vmin = (value_min >> j) & 0xFF,
+                vmax = (value_max >> j) & 0xFF;
+        if (v > vmax) {
+            error = std::max(v - vmax, error);
+        } else if (v < vmin) {
+            error = std::max(vmin - v, error);
+        }
+    }
+    return error;
+}
+
+static float set_error_code(gmkb::Error* error_out, gmkb::Error error) {
+    SkASSERT(error != gmkb::Error::kNone);
+    if (error_out) {
+        *error_out = error;
+    }
+    return FLT_MAX;
+}
+
+static SkPixmap to_pixmap(const SkBitmap& bitmap) {
+    SkPixmap pixmap;
+    SkAssertResult(bitmap.peekPixels(&pixmap));
+    return pixmap;
+}
+
+
+static bool WritePixmapToFile(const SkPixmap& pixmap, const char* path) {
+    SkFILEWStream wStream(path);
+    SkPngEncoder::Options options;
+    options.fUnpremulBehavior = SkTransferFunctionBehavior::kIgnore;
+    return wStream.isValid() && SkPngEncoder::Encode(&wStream, pixmap, options);
+}
+
+constexpr SkColorType kColorType = kRGBA_8888_SkColorType;
+constexpr SkAlphaType kAlphaType = kUnpremul_SkAlphaType;
+
+static SkPixmap rgba8888_to_pixmap(const uint32_t* pixels, int width, int height) {
+    SkImageInfo info = SkImageInfo::Make(width, height, kColorType, kAlphaType);
+    return SkPixmap(info, pixels, width * sizeof(uint32_t));
+}
+
+static bool asset_exists(skqp::AssetManager* mgr, const char* path) {
+    return mgr && nullptr != mgr->open(path);
+}
+
+static bool copy(skqp::AssetManager* mgr, const char* path, const char* dst) {
+    if (mgr) {
+        if (auto stream = mgr->open(path)) {
+            SkFILEWStream wStream(dst);
+            return wStream.isValid() && SkStreamCopy(&wStream, stream.get());
+        }
+    }
+    return false;
+}
+
+static SkBitmap ReadPngRgba8888FromFile(skqp::AssetManager* assetManager, const char* path) {
+    SkBitmap bitmap;
+    if (auto codec = SkCodec::MakeFromStream(assetManager->open(path))) {
+        SkISize size = codec->getInfo().dimensions();
+        SkASSERT(!size.isEmpty());
+        SkImageInfo info = SkImageInfo::Make(size.width(), size.height(), kColorType, kAlphaType);
+        bitmap.allocPixels(info);
+        SkASSERT(bitmap.rowBytes() == (unsigned)bitmap.width() * sizeof(uint32_t));
+        if (SkCodec::kSuccess != codec->getPixels(to_pixmap(bitmap))) {
+            bitmap.reset();
+        }
+    }
+    return bitmap;
+}
+
+namespace gmkb {
+// Assumes that for each GM foo, asset_manager has files foo/{max,min}.png
+float Check(const uint32_t* pixels,
+            int width,
+            int height,
+            const char* name,
+            const char* backend,
+            skqp::AssetManager* assetManager,
+            const char* report_directory_path,
+            Error* error_out) {
+    using std::string;
+    if (width <= 0 || height <= 0) {
+        return set_error_code(error_out, Error::kBadInput);
+    }
+    size_t N = (unsigned)width * (unsigned)height;
+    string max_path = path_join(name, PATH_MAX_PNG);
+    string min_path = path_join(name, PATH_MIN_PNG);
+    SkBitmap max_image = ReadPngRgba8888FromFile(assetManager, max_path.c_str());
+    if (max_image.isNull()) {
+        return set_error_code(error_out, Error::kBadData);
+    }
+    SkBitmap min_image = ReadPngRgba8888FromFile(assetManager, min_path.c_str());
+    if (min_image.isNull()) {
+        return set_error_code(error_out, Error::kBadData);
+    }
+    if (max_image.width()  != min_image.width() ||
+        max_image.height() != min_image.height())
+    {
+        return set_error_code(error_out, Error::kBadData);
+    }
+    if (max_image.width() != width || max_image.height() != height) {
+        return set_error_code(error_out, Error::kBadInput);
+    }
+    int badness = 0;
+    int badPixelCount = 0;
+    const uint32_t* max_pixels = (uint32_t*)max_image.getPixels();
+    const uint32_t* min_pixels = (uint32_t*)min_image.getPixels();
+
+    for (size_t i = 0; i < N; ++i) {
+        int error = get_error(pixels[i], max_pixels[i], min_pixels[i]);
+        if (error > 0) {
+            badness = SkTMax(error, badness);
+            ++badPixelCount;
+        }
+    }
+    if (report_directory_path && badness > 0 && report_directory_path[0] != '\0') {
+        sk_mkdir(report_directory_path);
+        if (!backend) {
+            backend = "skia";
+        }
+        string report_directory = path_join(report_directory_path, backend);
+        sk_mkdir(report_directory.c_str());
+        string report_subdirectory = path_join(report_directory, name);
+        sk_mkdir(report_subdirectory.c_str());
+        string error_path = path_join(report_subdirectory, PATH_IMG_PNG);
+        SkAssertResult(WritePixmapToFile(rgba8888_to_pixmap(pixels, width, height),
+                                         error_path.c_str()));
+        SkBitmap errorBitmap;
+        errorBitmap.allocPixels(SkImageInfo::Make(width, height, kColorType, kAlphaType));
+        uint32_t* errors = (uint32_t*)errorBitmap.getPixels();
+        for (size_t i = 0; i < N; ++i) {
+            int error = get_error(pixels[i], max_pixels[i], min_pixels[i]);
+            errors[i] = error > 0 ? 0xFF000000 + (unsigned)error : 0x00000000;
+        }
+        error_path = path_join(report_subdirectory, PATH_ERR_PNG);
+        SkAssertResult(WritePixmapToFile(to_pixmap(errorBitmap), error_path.c_str()));
+
+        auto report_path = path_join(report_subdirectory, PATH_REPORT);
+        auto rdir = path_join("..", "..", backend, name);
+
+        auto max_path_out = path_join(report_subdirectory, PATH_MAX_PNG);
+        auto min_path_out = path_join(report_subdirectory, PATH_MIN_PNG);
+        (void)copy(assetManager, max_path.c_str(), max_path_out.c_str());
+        (void)copy(assetManager, min_path.c_str(), min_path_out.c_str());
+
+        SkString text = SkStringPrintf(
+            "backend: %s\n<br>\n"
+            "gm name: %s\n<br>\n"
+            "maximum error: %d\n<br>\n"
+            "bad pixel counts: %d\n<br>\n"
+            "<a  href=\"%s/" PATH_IMG_PNG "\">"
+            "<img src=\"%s/" PATH_IMG_PNG "\" alt='img'></a>\n"
+            "<a  href=\"%s/" PATH_ERR_PNG "\">"
+            "<img src=\"%s/" PATH_ERR_PNG "\" alt='err'></a>\n<br>\n"
+            "<a  href=\"%s/" PATH_MAX_PNG "\">max</a>\n<br>\n"
+            "<a  href=\"%s/" PATH_MIN_PNG "\">min</a>\n<hr>\n",
+            backend, name, badness, badPixelCount,
+            rdir.c_str(), rdir.c_str(), rdir.c_str(), rdir.c_str(), rdir.c_str(), rdir.c_str());
+        SkFILEWStream(report_path.c_str()).write(text.c_str(), text.size());
+    }
+    if (error_out) {
+        *error_out = Error::kNone;
+    }
+    return (float)badness;
+}
+
+bool IsGoodGM(const char* name, skqp::AssetManager* assetManager) {
+    std::string max_path = path_join(name, PATH_MAX_PNG);
+    std::string min_path = path_join(name, PATH_MIN_PNG);
+    return asset_exists(assetManager, max_path.c_str())
+        && asset_exists(assetManager, min_path.c_str());
+}
+}  // namespace gmkb
diff --git a/tools/skqp/gm_knowledge.h b/tools/skqp/gm_knowledge.h
new file mode 100644
index 0000000..4aca00e
--- /dev/null
+++ b/tools/skqp/gm_knowledge.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef gm_knowledge_DEFINED
+#define gm_knowledge_DEFINED
+
+#include <cstdint>
+
+namespace skqp {
+class AssetManager;
+}
+
+namespace gmkb {
+
+enum class Error {
+    kNone,     /**< No error. */
+    kBadInput, /**< Error with the given image data. */
+    kBadData,  /**< Error with the given gmkb data directory. */
+};
+
+/**
+Check if the given test image matches the expected results.
+
+Each pixel is an un-pre-multiplied RGBA color:
+    uint32_t make_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+        return (r << 0) | (g << 8) | (b << 16) | (a << 24);
+    }
+
+The image's rowBytes is width*sizeof(uint32_t):
+    uint32_t* get_pixel_addr(uint32_t* pixels, int width, int height, int x, int y) {
+        assert(x >= 0 && x < width);
+        assert(y >= 0 && y < height);
+        return &pixels[x + (width * y)];
+    }
+
+@param pixels, width, height  the image
+@param gm_name                the name of the rendering test that produced the image
+@param backend                (optional) name of the backend
+@param asset_manager          GM KnowledgeBase data files
+@param report_directory_path  (optional) locatation to write report to.
+@param error_out              (optional) error return code.
+
+@return 0 if the test passes, otherwise a positive number representing how
+         badly it failed.  Return FLT_MAX on error.
+ */
+
+float Check(const uint32_t* pixels,
+            int width,
+            int height,
+            const char* name,
+            const char* backend,
+            skqp::AssetManager* asset_manager,
+            const char* report_directory_path,
+            Error* error_out);
+
+/**
+Check to see if the given test has expected results.
+
+@param name          The name of a rendering test.
+@param assetManager  GM KnowledgeBase data files
+
+@return true of expected results are known for the given test.
+*/
+bool IsGoodGM(const char* name, skqp::AssetManager* assetManager);
+
+}  // namespace gmkb
+
+#endif  // gm_knowledge_DEFINED
diff --git a/tools/skqp/gm_runner.cpp b/tools/skqp/gm_runner.cpp
new file mode 100644
index 0000000..3c3885e
--- /dev/null
+++ b/tools/skqp/gm_runner.cpp
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm_runner.h"
+
+#include <algorithm>
+
+#include "../dm/DMFontMgr.h"
+#include "GrContext.h"
+#include "GrContextOptions.h"
+#include "SkFontMgrPriv.h"
+#include "SkFontStyle.h"
+#include "SkGraphics.h"
+#include "SkSurface.h"
+#include "Test.h"
+#include "gl/GLTestContext.h"
+#include "gm.h"
+#include "gm_knowledge.h"
+#include "vk/VkTestContext.h"
+
+namespace gm_runner {
+
+const char* GetErrorString(Error e) {
+    switch (e) {
+        case Error::None:          return "";
+        case Error::BadSkiaOutput: return "Bad Skia Output";
+        case Error::BadGMKBData:   return "Bad GMKB Data";
+        case Error::SkiaFailure:   return "Skia Failure";
+        default:                   SkASSERT(false);
+                                   return "unknown";
+    }
+}
+
+std::vector<std::string> ExecuteTest(UnitTest test) {
+    struct : public skiatest::Reporter {
+        std::vector<std::string> fErrors;
+        void reportFailed(const skiatest::Failure& failure) override {
+            SkString desc = failure.toString();
+            fErrors.push_back(std::string(desc.c_str(), desc.size()));
+        }
+    } r;
+    GrContextOptions options;
+    if (test->fContextOptionsProc) {
+        test->fContextOptionsProc(&options);
+    }
+    test->proc(&r, options);
+    return std::move(r.fErrors);
+}
+
+const char* GetUnitTestName(UnitTest test) { return test->name; }
+
+std::vector<UnitTest> GetUnitTests() {
+    std::vector<UnitTest> tests;
+    for (const skiatest::TestRegistry* r = skiatest::TestRegistry::Head(); r; r = r->next()) {
+        const skiatest::Test& test = r->factory();
+        if (test.needsGpu) {
+            tests.push_back(&test);
+        }
+    }
+    return tests;
+}
+
+const char* GetBackendName(SkiaBackend backend) {
+    switch (backend) {
+        case SkiaBackend::kGL:     return "gl";
+        case SkiaBackend::kGLES:   return "gles";
+        case SkiaBackend::kVulkan: return "vk";
+        default:                   SkASSERT(false);
+                                   return "error";
+    }
+}
+
+static std::unique_ptr<sk_gpu_test::TestContext> make_test_context(SkiaBackend backend) {
+    using U = std::unique_ptr<sk_gpu_test::TestContext>;
+    switch (backend) {
+        case SkiaBackend::kGL:
+            return U(sk_gpu_test::CreatePlatformGLTestContext(kGL_GrGLStandard, nullptr));
+        case SkiaBackend::kGLES:
+            return U(sk_gpu_test::CreatePlatformGLTestContext(kGLES_GrGLStandard, nullptr));
+#ifdef SK_VULKAN
+        case SkiaBackend::kVulkan:
+            return U(sk_gpu_test::CreatePlatformVkTestContext(nullptr));
+#endif
+        default:
+            return nullptr;
+    }
+}
+
+static GrContextOptions context_options(skiagm::GM* gm = nullptr) {
+    GrContextOptions grContextOptions;
+    grContextOptions.fAllowPathMaskCaching = true;
+    grContextOptions.fSuppressPathRendering = true;
+    if (gm) {
+        gm->modifyGrContextOptions(&grContextOptions);
+    }
+    return grContextOptions;
+}
+
+std::vector<SkiaBackend> GetSupportedBackends() {
+    std::vector<SkiaBackend> result;
+    SkiaBackend backends[] = {
+        #ifndef SK_BUILD_FOR_ANDROID
+        SkiaBackend::kGL,  // Used for testing on desktop machines.
+        #endif
+        SkiaBackend::kGLES,
+        SkiaBackend::kVulkan,
+    };
+    for (SkiaBackend backend : backends) {
+        std::unique_ptr<sk_gpu_test::TestContext> testCtx = make_test_context(backend);
+        if (testCtx) {
+            testCtx->makeCurrent();
+            if (nullptr != testCtx->makeGrContext(context_options())) {
+                result.push_back(backend);
+            }
+        }
+    }
+    return result;
+}
+
+static bool evaluate_gm(SkiaBackend backend,
+                        skiagm::GM* gm,
+                        int* width,
+                        int* height,
+                        std::vector<uint32_t>* storage) {
+    constexpr SkColorType ct = kRGBA_8888_SkColorType;
+    SkASSERT(storage);
+    SkASSERT(gm);
+    SkASSERT(width);
+    SkASSERT(height);
+    SkISize size = gm->getISize();
+    int w = size.width(),
+        h = size.height();
+    *width = w;
+    *height = h;
+    SkImageInfo info = SkImageInfo::Make(w, h, ct, kPremul_SkAlphaType, nullptr);
+    SkSurfaceProps props(0, SkSurfaceProps::kLegacyFontHost_InitType);
+
+    std::unique_ptr<sk_gpu_test::TestContext> testCtx = make_test_context(backend);
+    if (!testCtx) {
+        return false;
+    }
+    testCtx->makeCurrent();
+    sk_sp<SkSurface> surf = SkSurface::MakeRenderTarget(
+            testCtx->makeGrContext(context_options(gm)).get(), SkBudgeted::kNo, info, 0, &props);
+    if (!surf) {
+        return false;
+    }
+    gm->draw(surf->getCanvas());
+
+    storage->resize(w * h);
+    uint32_t* pix = storage->data();
+    size_t rb = w * sizeof(uint32_t);
+    SkASSERT(SkColorTypeBytesPerPixel(ct) == sizeof(uint32_t));
+    if (!surf->readPixels(SkImageInfo::Make(w, h, ct, kUnpremul_SkAlphaType), pix, rb, 0, 0)) {
+        storage->resize(0);
+        return false;
+    }
+    return true;
+}
+
+std::tuple<float, Error> EvaluateGM(SkiaBackend backend,
+                                    GMFactory gmFact,
+                                    skqp::AssetManager* assetManager,
+                                    const char* reportDirectoryPath) {
+    std::vector<uint32_t> pixels;
+    std::unique_ptr<skiagm::GM> gm(gmFact(nullptr));
+    int width = 0, height = 0;
+    if (!evaluate_gm(backend, gm.get(), &width, &height, &pixels)) {
+        return std::make_tuple(FLT_MAX, Error::SkiaFailure);
+    }
+    gmkb::Error e;
+    float value = gmkb::Check(pixels.data(), width, height,
+                              gm->getName(), GetBackendName(backend), assetManager,
+                              reportDirectoryPath, &e);
+    Error error = gmkb::Error::kBadInput == e ? Error::BadSkiaOutput
+                : gmkb::Error::kBadData  == e ? Error::BadGMKBData
+                                              : Error::None;
+    return std::make_tuple(value, error);
+}
+
+void InitSkia() {
+    SkGraphics::Init();
+    gSkFontMgr_DefaultFactory = &DM::MakeFontMgr;
+}
+
+std::vector<GMFactory> GetGMFactories(skqp::AssetManager* assetManager) {
+    std::vector<GMFactory> result;
+    for (const skiagm::GMRegistry* r = skiagm::GMRegistry::Head(); r; r = r->next()) {
+        GMFactory f = r->factory();
+
+        if (gmkb::IsGoodGM(GetGMName(f).c_str(), assetManager)) {
+            result.push_back(r->factory());
+            SkASSERT(result.back());
+        }
+    }
+    struct {
+        bool operator()(GMFactory u, GMFactory v) const { return GetGMName(u) < GetGMName(v); }
+    } less;
+    std::sort(result.begin(), result.end(), less);
+    return result;
+}
+
+std::string GetGMName(GMFactory gmFactory) {
+    SkASSERT(gmFactory);
+    std::unique_ptr<skiagm::GM> gm(gmFactory(nullptr));
+    SkASSERT(gm);
+    return std::string(gm->getName());
+}
+}  // namespace gm_runner
diff --git a/tools/skqp/gm_runner.h b/tools/skqp/gm_runner.h
new file mode 100644
index 0000000..690b371
--- /dev/null
+++ b/tools/skqp/gm_runner.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef gm_runner_DEFINED
+#define gm_runner_DEFINED
+
+#include <memory>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "skqp_asset_manager.h"
+
+/**
+A Skia GM is a single rendering test that can be executed on any Skia backend Canvas.
+*/
+namespace skiagm {
+class GM;
+}
+
+namespace skiatest {
+struct Test;
+}
+
+namespace gm_runner {
+
+using GMFactory = skiagm::GM* (*)(void*);
+
+using UnitTest = const skiatest::Test*;
+
+enum class SkiaBackend {
+    kGL,
+    kGLES,
+    kVulkan,
+};
+
+/**
+Initialize Skia
+*/
+void InitSkia();
+
+std::vector<SkiaBackend> GetSupportedBackends();
+
+/**
+@return a list of all Skia GMs in lexicographic order.
+*/
+std::vector<GMFactory> GetGMFactories(skqp::AssetManager*);
+
+/**
+@return a list of all Skia GPU unit tests in lexicographic order.
+*/
+std::vector<UnitTest> GetUnitTests();
+
+/**
+@return a descriptive name for the GM.
+*/
+std::string GetGMName(GMFactory);
+
+/**
+@return a descriptive name for the unit test.
+*/
+const char* GetUnitTestName(UnitTest);
+
+/**
+@return a descriptive name for the backend.
+*/
+const char* GetBackendName(SkiaBackend);
+
+enum class Error {
+    None = 0,
+    BadSkiaOutput = 1,
+    BadGMKBData = 2,
+    SkiaFailure = 3,
+};
+
+const char* GetErrorString(Error);
+
+/**
+@return A non-negative float representing how badly the GM failed (or zero for
+        success).  Any error running or evaluating the GM will result in a non-zero
+        error code.
+*/
+std::tuple<float, Error> EvaluateGM(SkiaBackend backend,
+                                    GMFactory gmFact,
+                                    skqp::AssetManager* assetManager,
+                                    const char* reportDirectoryPath);
+
+/**
+@return a (hopefully empty) list of errors produced by this unit test.
+*/
+std::vector<std::string> ExecuteTest(UnitTest);
+
+}  // namespace gm_runner
+
+#endif  // gm_runner_DEFINED
diff --git a/tools/skqp/make_gmkb.go b/tools/skqp/make_gmkb.go
new file mode 100644
index 0000000..b8b67a3
--- /dev/null
+++ b/tools/skqp/make_gmkb.go
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"image"
+	"image/draw"
+	"image/png"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"sort"
+	"strings"
+	"sync"
+
+	"go.skia.org/infra/golden/go/search"
+)
+
+const (
+	min_png = "min.png"
+	max_png = "max.png"
+)
+
+type ExportTestRecordArray []search.ExportTestRecord
+
+func (a ExportTestRecordArray) Len() int           { return len(a) }
+func (a ExportTestRecordArray) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ExportTestRecordArray) Less(i, j int) bool { return a[i].TestName < a[j].TestName }
+
+func in(v string, a []string) bool {
+	for _, u := range a {
+		if u == v {
+			return true
+		}
+	}
+	return false
+}
+
+// TODO(halcanary): clean up this blacklist.
+var blacklist = []string{
+	"circular-clips",
+	"colorcomposefilter_wacky",
+	"coloremoji_blendmodes",
+	"colormatrix",
+	"complexclip_bw",
+	"complexclip_bw_invert",
+	"complexclip_bw_layer",
+	"complexclip_bw_layer_invert",
+	"convex-lineonly-paths-stroke-and-fill",
+	"dftext",
+	"downsamplebitmap_image_high_mandrill_512.png",
+	"downsamplebitmap_image_medium_mandrill_512.png",
+	"filterbitmap_image_mandrill_16.png",
+	"filterbitmap_image_mandrill_64.png",
+	"filterbitmap_image_mandrill_64.png_g8",
+	"gradients_degenerate_2pt",
+	"gradients_degenerate_2pt_nodither",
+	"gradients_local_perspective",
+	"gradients_local_perspective_nodither",
+	"imagefilterstransformed",
+	"image_scale_aligned",
+	"lattice",
+	"linear_gradient",
+	"mipmap_srgb",
+	"mixedtextblobs",
+	"OverStroke",
+	"simple-offsetimagefilter",
+	"strokerect",
+	"textblobmixedsizes",
+	"textblobmixedsizes_df"}
+
+func processTest(testName string, imgUrls []string, output string) error {
+	if strings.ContainsRune(testName, '/') {
+		return nil
+	}
+	output_directory := path.Join(output, testName)
+	var img_max image.NRGBA
+	var img_min image.NRGBA
+	for _, url := range imgUrls {
+		resp, err := http.Get(url)
+		if err != nil {
+			return err
+		}
+		img, err := png.Decode(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			return err
+		}
+		if img_max.Rect.Max.X == 0 {
+			// N.B. img_max.Pix may alias img.Pix (if they're already NRGBA).
+			img_max = toNrgba(img)
+			img_min = copyNrgba(img_max)
+			continue
+		}
+		w := img.Bounds().Max.X - img.Bounds().Min.X
+		h := img.Bounds().Max.Y - img.Bounds().Min.Y
+		if img_max.Rect.Max.X != w || img_max.Rect.Max.Y != h {
+			return errors.New("size mismatch")
+		}
+		img_nrgba := toNrgba(img)
+		for i, value := range img_nrgba.Pix {
+			if value > img_max.Pix[i] {
+				img_max.Pix[i] = value
+			} else if value < img_min.Pix[i] {
+				img_min.Pix[i] = value
+			}
+		}
+	}
+	if img_max.Rect.Max.X == 0 {
+		return nil
+	}
+	if err := os.Mkdir(output_directory, os.ModePerm); err != nil && !os.IsExist(err) {
+		return err
+	}
+	if err := writePngToFile(path.Join(output_directory, min_png), &img_min); err != nil {
+		return err
+	}
+	if err := writePngToFile(path.Join(output_directory, max_png), &img_max); err != nil {
+		return err
+	}
+	return nil
+
+}
+
+func readMetaJsonFile(filename string) ([]search.ExportTestRecord, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	dec := json.NewDecoder(file)
+	var records []search.ExportTestRecord
+	err = dec.Decode(&records)
+	return records, err
+}
+
+func writePngToFile(path string, img image.Image) error {
+	file, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	return png.Encode(file, img)
+}
+
+// to_nrgb() may return a shallow copy of img if it's already NRGBA.
+func toNrgba(img image.Image) image.NRGBA {
+	switch v := img.(type) {
+	case *image.NRGBA:
+		return *v
+	}
+	nimg := *image.NewNRGBA(img.Bounds())
+	draw.Draw(&nimg, img.Bounds(), img, image.Point{0, 0}, draw.Src)
+	return nimg
+}
+
+func copyNrgba(src image.NRGBA) image.NRGBA {
+	dst := image.NRGBA{make([]uint8, len(src.Pix)), src.Stride, src.Rect}
+	copy(dst.Pix, src.Pix)
+	return dst
+}
+
+func main() {
+	if len(os.Args) != 3 {
+		log.Printf("Usage:\n  %s INPUT.json OUTPUT_DIRECTORY\n\n", os.Args[0])
+		os.Exit(1)
+	}
+	input := os.Args[1]
+	output := os.Args[2]
+	err := os.MkdirAll(output, os.ModePerm)
+	if err != nil && !os.IsExist(err) {
+		log.Fatal(err)
+	}
+
+	records, err := readMetaJsonFile(input)
+	if err != nil {
+		log.Fatal(err)
+	}
+	sort.Sort(ExportTestRecordArray(records))
+
+	index, err := os.Create(path.Join(output, "index.txt"))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer index.Close()
+
+	var wg sync.WaitGroup
+	for _, record := range records {
+		if in(record.TestName, blacklist) {
+			continue
+		}
+		var goodUrls []string
+		for _, digest := range record.Digests {
+			if (in("vk", digest.ParamSet["config"]) ||
+				in("gles", digest.ParamSet["config"])) &&
+				digest.Status == "positive" {
+				goodUrls = append(goodUrls, digest.URL)
+			}
+		}
+		wg.Add(1)
+		go func(testName string, imgUrls []string, output string) {
+			defer wg.Done()
+			if err := processTest(testName, imgUrls, output); err != nil {
+				log.Fatal(err)
+			}
+			fmt.Printf("\r%-60s", testName)
+		}(record.TestName, goodUrls, output)
+
+		_, err = fmt.Fprintf(index, "%s\n", record.TestName)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	wg.Wait()
+	fmt.Printf("\r%60s\n", "")
+}
diff --git a/tools/skqp/make_report.py b/tools/skqp/make_report.py
new file mode 100755
index 0000000..4345575
--- /dev/null
+++ b/tools/skqp/make_report.py
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+
+# Copyright 2017 Google Inc.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import glob
+import os
+import re
+import sys
+
+import sysopen
+
+if len(sys.argv) == 2:
+  if not os.path.isdir(sys.argv[1]):
+    exit(1)
+  os.chdir(sys.argv[1])
+
+head = '''<!doctype html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>SkQP Report</title>
+<style>
+img { max-width:48%; border:1px green solid; }
+</style>
+</head>
+<body>
+<h1>SkQP Report</h1>
+<hr>
+'''
+
+reg = re.compile('="../../')
+with open('report.html', 'w') as o:
+  o.write(head)
+  for x in glob.iglob('*/*/report.html'):
+    with open(x, 'r') as f:
+      o.write(reg.sub('="', f.read()))
+  o.write('</body>\n</html>\n')
+
+sysopen.sysopen('report.html')
diff --git a/tools/skqp/skqp.cpp b/tools/skqp/skqp.cpp
new file mode 100644
index 0000000..b9864cd
--- /dev/null
+++ b/tools/skqp/skqp.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm_runner.h"
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wused-but-marked-unused"
+#endif
+
+#include "gtest/gtest.h"
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+#include "SkStream.h"
+#include "SkString.h"
+
+////////////////////////////////////////////////////////////////////////////////
+
+static std::string gReportDirectoryPath;
+static std::unique_ptr<skqp::AssetManager> gAssetMgr;
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct GMTestCase {
+    gm_runner::GMFactory fGMFactory;
+    gm_runner::SkiaBackend fBackend;
+};
+
+struct GMTest : public testing::Test {
+    GMTestCase fTest;
+    GMTest(GMTestCase t) : fTest(t) {}
+    void TestBody() override {
+        float result;
+        gm_runner::Error error;
+        std::tie(result, error) =
+                gm_runner::EvaluateGM(fTest.fBackend, fTest.fGMFactory,
+                                      gAssetMgr.get(), gReportDirectoryPath.c_str());
+        EXPECT_EQ(error, gm_runner::Error::None);
+        if (gm_runner::Error::None == error) {
+            EXPECT_EQ(result, 0);
+        }
+    }
+};
+
+struct GMTestFactory : public testing::internal::TestFactoryBase {
+    GMTestCase fTest;
+    GMTestFactory(GMTestCase t) : fTest(t) {}
+    testing::Test* CreateTest() override { return new GMTest(fTest); }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+struct UnitTestTest : public testing::Test {
+    gm_runner::UnitTest fTest;
+    UnitTestTest(gm_runner::UnitTest test) : fTest(test) {}
+    void TestBody() override {
+        std::vector<std::string> errors = gm_runner::ExecuteTest(fTest);
+        for (const std::string& error : errors) {
+            GTEST_NONFATAL_FAILURE_(error.c_str());
+        }
+    }
+};
+
+struct UnitTestFactory : public testing::internal::TestFactoryBase {
+    gm_runner::UnitTest fTest;
+    UnitTestFactory(gm_runner::UnitTest test) : fTest(test) {}
+    testing::Test* CreateTest() override { return new UnitTestTest(fTest); }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+static void reg_test(const char* test, const char* testCase,
+                     testing::internal::TestFactoryBase* fact) {
+    testing::internal::MakeAndRegisterTestInfo(test,
+                                               testCase,
+                                               nullptr,
+                                               nullptr,
+                                               testing::internal::CodeLocation(__FILE__, __LINE__),
+                                               testing::internal::GetTestTypeId(),
+                                               testing::Test::SetUpTestCase,
+                                               testing::Test::TearDownTestCase,
+                                               fact);
+}
+
+
+void register_skia_tests() {
+    gm_runner::InitSkia();
+
+    // Rendering Tests
+    std::vector<gm_runner::SkiaBackend> backends = gm_runner::GetSupportedBackends();
+    std::vector<gm_runner::GMFactory> gms = gm_runner::GetGMFactories(gAssetMgr.get());
+    for (auto backend : backends) {
+        const char* backendName = GetBackendName(backend);
+        std::string test = std::string("SkiaGM_") + backendName;
+        for (auto gmFactory : gms) {
+            std::string gmName = gm_runner::GetGMName(gmFactory);
+            reg_test(test.c_str(), gmName.c_str(),
+                     new GMTestFactory(GMTestCase{gmFactory, backend}));
+        }
+    }
+
+    for (gm_runner::UnitTest test : gm_runner::GetUnitTests()) {
+        reg_test("Skia_Unit_Tests", gm_runner::GetUnitTestName(test), new UnitTestFactory{test});
+    }
+}
+
+namespace {
+struct StdAssetManager : public skqp::AssetManager {
+    SkString fPrefix;
+    StdAssetManager(const char* p) : fPrefix(p) {}
+    std::unique_ptr<SkStreamAsset> open(const char* path) override {
+        SkString fullPath = fPrefix.isEmpty()
+                          ? SkString(path)
+                          : SkStringPrintf("%s/%s", fPrefix.c_str(), path);
+        return SkStream::MakeFromFile(fullPath.c_str());
+    }
+};
+}
+
+int main(int argc, char** argv) {
+    testing::InitGoogleTest(&argc, argv);
+    if (argc < 2) {
+        std::cerr << "Usage:\n  " << argv[0]
+                  << " [GTEST_ARGUMENTS] GMKB_DIRECTORY_PATH GMKB_REPORT_PATH\n\n";
+        return 1;
+    }
+    gAssetMgr.reset(new StdAssetManager(argv[1]));
+    if (argc > 2) {
+        gReportDirectoryPath = argv[2];
+    }
+    register_skia_tests();
+    return RUN_ALL_TESTS();
+}
diff --git a/tools/skqp/skqp_asset_manager.h b/tools/skqp/skqp_asset_manager.h
new file mode 100644
index 0000000..b9bd4d6
--- /dev/null
+++ b/tools/skqp/skqp_asset_manager.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#ifndef skqp_asset_manager_DEFINED
+#define skqp_asset_manager_DEFINED
+
+#include <memory>
+
+class SkStreamAsset;
+
+namespace skqp {
+class AssetManager {
+public:
+    virtual ~AssetManager() {}
+    virtual std::unique_ptr<SkStreamAsset> open(const char* path) = 0;
+};
+}  // namespace skqp
+#endif  // skqp_asset_manager_DEFINED
diff --git a/tools/skqp/sysopen.py b/tools/skqp/sysopen.py
new file mode 100755
index 0000000..f104ab9
--- /dev/null
+++ b/tools/skqp/sysopen.py
@@ -0,0 +1,21 @@
+#! /usr/bin/env python2
+# Copyright 2017 Google Inc.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import subprocess
+import sys
+
+def sysopen(arg):
+  plat = sys.platform
+  if plat.startswith('darwin'):
+    subprocess.call(["open", arg])
+  elif plat.startswith('win'):
+    os.startfile(arg)
+  else:
+    subprocess.call(["xdg-open", arg])
+
+if __name__ == '__main__':
+  for a in sys.argv[1:]:
+    sysopen(a)