|  | /* | 
|  | * 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 <cstdlib> | 
|  | #include <fstream> | 
|  | #include <mutex> | 
|  | #include <sstream> | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | #include "../../src/core/SkStreamPriv.h" | 
|  | #include "../../src/core/SkTSort.h" | 
|  | #include "SkBitmap.h" | 
|  | #include "SkCodec.h" | 
|  | #include "SkOSFile.h" | 
|  | #include "SkOSPath.h" | 
|  | #include "SkPngEncoder.h" | 
|  | #include "SkStream.h" | 
|  |  | 
|  | #include "skqp_asset_manager.h" | 
|  |  | 
|  | #define IMAGES_DIRECTORY_PATH "images" | 
|  | #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" | 
|  | #define PATH_CSV     "out.csv" | 
|  |  | 
|  | #ifndef SK_SKQP_GLOBAL_ERROR_TOLERANCE | 
|  | #define SK_SKQP_GLOBAL_ERROR_TOLERANCE 0 | 
|  | #endif | 
|  |  | 
|  | //////////////////////////////////////////////////////////////////////////////// | 
|  |  | 
|  | 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 std::max(0, error - SK_SKQP_GLOBAL_ERROR_TOLERANCE); | 
|  | } | 
|  |  | 
|  | static int get_error_with_nearby(int x, int y, const SkPixmap& pm, | 
|  | const SkPixmap& pm_max, const SkPixmap& pm_min) { | 
|  | struct NearbyPixels { | 
|  | const int x, y, w, h; | 
|  | struct Iter { | 
|  | const int x, y, w, h; | 
|  | int8_t curr; | 
|  | SkIPoint operator*() const { return this->get(); } | 
|  | SkIPoint get() const { | 
|  | switch (curr) { | 
|  | case 0: return {x - 1, y - 1}; | 
|  | case 1: return {x    , y - 1}; | 
|  | case 2: return {x + 1, y - 1}; | 
|  | case 3: return {x - 1, y    }; | 
|  | case 4: return {x + 1, y    }; | 
|  | case 5: return {x - 1, y + 1}; | 
|  | case 6: return {x    , y + 1}; | 
|  | case 7: return {x + 1, y + 1}; | 
|  | default: SkASSERT(false); return {0, 0}; | 
|  | } | 
|  | } | 
|  | void skipBad() { | 
|  | while (curr < 8) { | 
|  | SkIPoint p = this->get(); | 
|  | if (p.x() >= 0 && p.y() >= 0 && p.x() < w && p.y() < h) { | 
|  | return; | 
|  | } | 
|  | ++curr; | 
|  | } | 
|  | curr = -1; | 
|  | } | 
|  | void operator++() { | 
|  | if (-1 == curr) { return; } | 
|  | ++curr; | 
|  | this->skipBad(); | 
|  | } | 
|  | bool operator!=(const Iter& other) const { return curr != other.curr; } | 
|  | }; | 
|  | Iter begin() const { Iter i{x, y, w, h, 0}; i.skipBad(); return i; } | 
|  | Iter end() const { return Iter{x, y, w, h, -1}; } | 
|  | }; | 
|  |  | 
|  | uint32_t c = *pm.addr32(x, y); | 
|  | int error = get_error(c, *pm_max.addr32(x, y), *pm_min.addr32(x, y)); | 
|  | for (SkIPoint p : NearbyPixels{x, y, pm.width(), pm.height()}) { | 
|  | if (error == 0) { | 
|  | return 0; | 
|  | } | 
|  | error = SkTMin(error, get_error( | 
|  | c, *pm_max.addr32(p.x(), p.y()), *pm_min.addr32(p.x(), p.y()))); | 
|  | } | 
|  | 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 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 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(bitmap.pixmap())) { | 
|  | bitmap.reset(); | 
|  | } | 
|  | } | 
|  | return bitmap; | 
|  | } | 
|  |  | 
|  | namespace { | 
|  | struct Run { | 
|  | SkString fBackend; | 
|  | SkString fGM; | 
|  | int fMaxerror; | 
|  | int fBadpixels; | 
|  | }; | 
|  | }  // namespace | 
|  |  | 
|  | static std::vector<Run> gErrors; | 
|  | static std::mutex gMutex; | 
|  |  | 
|  | static SkString make_path(const SkString& images_directory, | 
|  | const char* backend, | 
|  | const char* gm_name, | 
|  | const char* thing) { | 
|  | auto path = SkStringPrintf("%s_%s_%s", backend, gm_name, thing); | 
|  | return SkOSPath::Join(images_directory.c_str(), path.c_str()); | 
|  | } | 
|  |  | 
|  |  | 
|  | namespace gmkb { | 
|  | 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) { | 
|  | if (report_directory_path && report_directory_path[0]) { | 
|  | SkASSERT_RELEASE(sk_isdir(report_directory_path)); | 
|  | } | 
|  | if (width <= 0 || height <= 0) { | 
|  | return set_error_code(error_out, Error::kBadInput); | 
|  | } | 
|  | constexpr char PATH_ROOT[] = "gmkb"; | 
|  | SkString img_path = SkOSPath::Join(PATH_ROOT, name); | 
|  | SkString max_path = SkOSPath::Join(img_path.c_str(), PATH_MAX_PNG); | 
|  | SkString min_path = SkOSPath::Join(img_path.c_str(), PATH_MIN_PNG); | 
|  | SkBitmap max_image = ReadPngRgba8888FromFile(assetManager, max_path.c_str()); | 
|  | SkBitmap min_image = ReadPngRgba8888FromFile(assetManager, min_path.c_str()); | 
|  | if (max_image.isNull() || min_image.isNull()) { | 
|  | // No data. | 
|  | if (error_out) { | 
|  | *error_out = Error::kNone; | 
|  | } | 
|  | return 0; | 
|  | } | 
|  | 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; | 
|  | SkPixmap pm(SkImageInfo::Make(width, height, kColorType, kAlphaType), | 
|  | pixels, width * sizeof(uint32_t)); | 
|  | SkPixmap pm_max = max_image.pixmap(); | 
|  | SkPixmap pm_min = min_image.pixmap(); | 
|  | for (int y = 0; y < pm.height(); ++y) { | 
|  | for (int x = 0; x < pm.width(); ++x) { | 
|  | int error = get_error_with_nearby(x, y, pm, pm_max, pm_min) ; | 
|  | if (error > 0) { | 
|  | badness = SkTMax(error, badness); | 
|  | ++badPixelCount; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (badness == 0) { | 
|  | std::lock_guard<std::mutex> lock(gMutex); | 
|  | gErrors.push_back(Run{SkString(backend), SkString(name), 0, 0}); | 
|  | } | 
|  | if (report_directory_path && badness > 0 && report_directory_path[0] != '\0') { | 
|  | if (!backend) { | 
|  | backend = "skia"; | 
|  | } | 
|  | SkString images_directory = SkOSPath::Join(report_directory_path, IMAGES_DIRECTORY_PATH); | 
|  | sk_mkdir(images_directory.c_str()); | 
|  |  | 
|  | SkString image_path   = make_path(images_directory, backend, name, PATH_IMG_PNG); | 
|  | SkString error_path   = make_path(images_directory, backend, name, PATH_ERR_PNG); | 
|  | SkString max_path_out = make_path(images_directory, backend, name, PATH_MAX_PNG); | 
|  | SkString min_path_out = make_path(images_directory, backend, name, PATH_MIN_PNG); | 
|  |  | 
|  | SkAssertResult(WritePixmapToFile(rgba8888_to_pixmap(pixels, width, height), | 
|  | image_path.c_str())); | 
|  |  | 
|  | SkBitmap errorBitmap; | 
|  | errorBitmap.allocPixels(SkImageInfo::Make(width, height, kColorType, kAlphaType)); | 
|  | for (int y = 0; y < pm.height(); ++y) { | 
|  | for (int x = 0; x < pm.width(); ++x) { | 
|  | int error = get_error_with_nearby(x, y, pm, pm_max, pm_min); | 
|  | *errorBitmap.getAddr32(x, y) = | 
|  | error > 0 ? 0xFF000000 + (unsigned)error : 0xFFFFFFFF; | 
|  | } | 
|  | } | 
|  | SkAssertResult(WritePixmapToFile(errorBitmap.pixmap(), error_path.c_str())); | 
|  |  | 
|  | (void)copy(assetManager, max_path.c_str(), max_path_out.c_str()); | 
|  | (void)copy(assetManager, min_path.c_str(), min_path_out.c_str()); | 
|  |  | 
|  | std::lock_guard<std::mutex> lock(gMutex); | 
|  | gErrors.push_back(Run{SkString(backend), SkString(name), badness, badPixelCount}); | 
|  | } | 
|  | if (error_out) { | 
|  | *error_out = Error::kNone; | 
|  | } | 
|  | return (float)badness; | 
|  | } | 
|  |  | 
|  | static constexpr char kDocHead[] = | 
|  | "<!doctype html>\n" | 
|  | "<html lang=\"en\">\n" | 
|  | "<head>\n" | 
|  | "<meta charset=\"UTF-8\">\n" | 
|  | "<title>SkQP Report</title>\n" | 
|  | "<style>\n" | 
|  | "img { max-width:48%; border:1px green solid;\n" | 
|  | "      image-rendering: pixelated;\n" | 
|  | "      background-image:url('data:image/png;base64,iVBORw0KGgoA" | 
|  | "AAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAAXNSR0IArs4c6QAAAAJiS0dEAP+H" | 
|  | "j8y/AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH3gUBEi4DGRAQYgAAAB1J" | 
|  | "REFUGNNjfMoAAVJQmokBDdBHgPE/lPFsYN0BABdaAwN6tehMAAAAAElFTkSuQmCC" | 
|  | "'); }\n" | 
|  | "</style>\n" | 
|  | "<script>\n" | 
|  | "function ce(t) { return document.createElement(t); }\n" | 
|  | "function ct(n) { return document.createTextNode(n); }\n" | 
|  | "function ac(u,v) { return u.appendChild(v); }\n" | 
|  | "function br(u) { ac(u, ce(\"br\")); }\n" | 
|  | "function ma(s, c) { var a = ce(\"a\"); a.href = s; ac(a, c); return a; }\n" | 
|  | "function f(backend, gm, e1, e2) {\n" | 
|  | "  var b = ce(\"div\");\n" | 
|  | "  var x = ce(\"h2\");\n" | 
|  | "  var t = backend + \"/\" + gm;\n" | 
|  | "  ac(x, ct(t));\n" | 
|  | "  ac(b, x);\n" | 
|  | "  ac(b, ct(\"backend: \" + backend));\n" | 
|  | "  br(b);\n" | 
|  | "  ac(b, ct(\"gm name: \" + gm));\n" | 
|  | "  br(b);\n" | 
|  | "  ac(b, ct(\"maximum error: \" + e1));\n" | 
|  | "  br(b);\n" | 
|  | "  ac(b, ct(\"bad pixel counts: \" + e2));\n" | 
|  | "  br(b);\n" | 
|  | "  var q = \"" IMAGES_DIRECTORY_PATH "/\" + backend + \"_\" + gm + \"_\";\n" | 
|  | "  var i = ce(\"img\");\n" | 
|  | "  i.src = q + \"" PATH_IMG_PNG "\";\n" | 
|  | "  i.alt = \"img\";\n" | 
|  | "  ac(b, ma(i.src, i));\n" | 
|  | "  i = ce(\"img\");\n" | 
|  | "  i.src = q + \"" PATH_ERR_PNG "\";\n" | 
|  | "  i.alt = \"err\";\n" | 
|  | "  ac(b, ma(i.src, i));\n" | 
|  | "  br(b);\n" | 
|  | "  ac(b, ct(\"Expectation: \"));\n" | 
|  | "  ac(b, ma(q + \"" PATH_MAX_PNG "\", ct(\"max\")));\n" | 
|  | "  ac(b, ct(\" | \"));\n" | 
|  | "  ac(b, ma(q + \"" PATH_MIN_PNG "\", ct(\"min\")));\n" | 
|  | "  ac(b, ce(\"hr\"));\n" | 
|  | "  b.id = backend + \":\" + gm;\n" | 
|  | "  ac(document.body, b);\n" | 
|  | "  l = ce(\"li\");\n" | 
|  | "  ac(l, ct(\"[\" + e1 + \"] \"));\n" | 
|  | "  ac(l, ma(\"#\" + backend +\":\"+ gm , ct(t)));\n" | 
|  | "  ac(document.getElementById(\"toc\"), l);\n" | 
|  | "}\n" | 
|  | "function main() {\n"; | 
|  |  | 
|  | static constexpr char kDocMiddle[] = | 
|  | "}\n" | 
|  | "</script>\n" | 
|  | "</head>\n" | 
|  | "<body onload=\"main()\">\n" | 
|  | "<h1>SkQP Report</h1>\n"; | 
|  |  | 
|  | static constexpr char kDocTail[] = | 
|  | "<ul id=\"toc\"></ul>\n" | 
|  | "<hr>\n" | 
|  | "<p>Left image: test result<br>\n" | 
|  | "Right image: errors (white = no error, black = smallest error, red = biggest error)</p>\n" | 
|  | "<hr>\n" | 
|  | "</body>\n" | 
|  | "</html>\n"; | 
|  |  | 
|  | static void write(SkWStream* wStream, const SkString& text) { | 
|  | wStream->write(text.c_str(), text.size()); | 
|  | } | 
|  |  | 
|  | enum class Backend { | 
|  | kUnknown, | 
|  | kGLES, | 
|  | kVulkan, | 
|  | }; | 
|  |  | 
|  | static Backend get_backend(const SkString& s) { | 
|  | if (s.equals("gles")) { | 
|  | return Backend::kGLES; | 
|  | } else if (s.equals("vk")) { | 
|  | return Backend::kVulkan; | 
|  | } | 
|  | return Backend::kUnknown; | 
|  | } | 
|  |  | 
|  |  | 
|  | bool MakeReport(const char* report_directory_path) { | 
|  | int glesErrorCount = 0, vkErrorCount = 0, gles = 0, vk = 0; | 
|  |  | 
|  | SkASSERT_RELEASE(sk_isdir(report_directory_path)); | 
|  | std::lock_guard<std::mutex> lock(gMutex); | 
|  | SkFILEWStream csvOut(SkOSPath::Join(report_directory_path, PATH_CSV).c_str()); | 
|  | SkFILEWStream htmOut(SkOSPath::Join(report_directory_path, PATH_REPORT).c_str()); | 
|  | SkASSERT_RELEASE(csvOut.isValid()); | 
|  | if (!csvOut.isValid() || !htmOut.isValid()) { | 
|  | return false; | 
|  | } | 
|  | htmOut.writeText(kDocHead); | 
|  | for (const Run& run : gErrors) { | 
|  | auto backend = get_backend(run.fBackend); | 
|  | switch (backend) { | 
|  | case Backend::kGLES: ++gles; break; | 
|  | case Backend::kVulkan: ++vk; break; | 
|  | default: break; | 
|  | } | 
|  | write(&csvOut, SkStringPrintf("\"%s\",\"%s\",%d,%d\n", | 
|  | run.fBackend.c_str(), run.fGM.c_str(), | 
|  | run.fMaxerror, run.fBadpixels)); | 
|  | if (run.fMaxerror == 0 && run.fBadpixels == 0) { | 
|  | continue; | 
|  | } | 
|  | write(&htmOut, SkStringPrintf("  f(\"%s\", \"%s\", %d, %d);\n", | 
|  | run.fBackend.c_str(), run.fGM.c_str(), | 
|  | run.fMaxerror, run.fBadpixels)); | 
|  | switch (backend) { | 
|  | case Backend::kGLES: ++glesErrorCount; break; | 
|  | case Backend::kVulkan: ++vkErrorCount; break; | 
|  | default: break; | 
|  | } | 
|  | } | 
|  | htmOut.writeText(kDocMiddle); | 
|  | write(&htmOut, SkStringPrintf("<p>gles errors: %d (of %d)</br>\n" | 
|  | "vk errors: %d (of %d)</p>\n", | 
|  | glesErrorCount, gles, vkErrorCount, vk)); | 
|  | htmOut.writeText(kDocTail); | 
|  | return true; | 
|  | } | 
|  | }  // namespace gmkb |