aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Hal Canary <halcanary@google.com>2017-12-11 17:46:26 -0500
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2017-12-15 17:23:48 +0000
commitd7b3845f3d3f3498c2adc542b4b20003ac7d3ab0 (patch)
tree11ed4db86efb28f344255a6cc44f0c19d926c8ef
parent0215e39d7e415d0530231df6ad20d5f215c72152 (diff)
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>
-rw-r--r--BUILD.gn21
-rw-r--r--dm/DM.cpp6
-rw-r--r--platform_tools/android/.gitignore1
-rw-r--r--src/core/SkFontMgrPriv.h14
-rw-r--r--tools/gpucts/gm_knowledge.c12
-rw-r--r--tools/gpucts/gm_knowledge.h57
-rw-r--r--tools/gpucts/gm_runner.cpp109
-rw-r--r--tools/gpucts/gm_runner.h65
-rw-r--r--tools/gpucts/gpucts.cpp159
-rw-r--r--tools/ok_vias.cpp3
-rw-r--r--tools/skqp/README.md66
-rw-r--r--tools/skqp/gm_knowledge.cpp230
-rw-r--r--tools/skqp/gm_knowledge.h71
-rw-r--r--tools/skqp/gm_runner.cpp213
-rw-r--r--tools/skqp/gm_runner.h98
-rw-r--r--tools/skqp/make_gmkb.go223
-rwxr-xr-xtools/skqp/make_report.py41
-rw-r--r--tools/skqp/skqp.cpp140
-rw-r--r--tools/skqp/skqp_asset_manager.h21
-rwxr-xr-xtools/skqp/sysopen.py21
20 files changed, 1158 insertions, 413 deletions
diff --git a/BUILD.gn b/BUILD.gn
index 4d1850a09c..b03f1117d9 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1701,19 +1701,30 @@ if (skia_enable_tools) {
]
}
- 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 4deaa2fbb7..058fb9da03 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 @@ static sk_sp<SkTypeface> create_from_name(const char familyName[], SkFontStyle s
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 cb3f2d6237..26e14aa29f 100644
--- a/platform_tools/android/.gitignore
+++ b/platform_tools/android/.gitignore
@@ -9,3 +9,4 @@ app/.project
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 0000000000..ba370157f6
--- /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 8dbdd00f09..0000000000
--- 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 d9d71e05c5..0000000000
--- 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 a18c457d1b..0000000000
--- 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 cd0d7d32bb..0000000000
--- 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 c50519cc5b..0000000000
--- 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 7c246644ee..c1f8d9365e 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 @@ static Register trace{"trace",
"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 0000000000..beac911ba0
--- /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 0000000000..bc2ca8201a
--- /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 0000000000..4aca00ec30
--- /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 0000000000..3c3885ef5c
--- /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 0000000000..690b3714e6
--- /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 0000000000..b8b67a3634
--- /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 0000000000..434557572a
--- /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 0000000000..b9864cd1d1
--- /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 0000000000..b9bd4d6c81
--- /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 0000000000..f104ab9fa4
--- /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)