Home | History | Annotate | Download | only in skqp
      1 /*
      2  * Copyright 2017 Google Inc.
      3  *
      4  * Use of this source code is governed by a BSD-style license that can be
      5  * found in the LICENSE file.
      6  */
      7 
      8 #include "gm_knowledge.h"
      9 
     10 #include <cfloat>
     11 #include <cstdlib>
     12 #include <fstream>
     13 #include <mutex>
     14 #include <sstream>
     15 #include <string>
     16 #include <vector>
     17 
     18 #include "../../src/core/SkStreamPriv.h"
     19 #include "../../src/core/SkTSort.h"
     20 #include "SkBitmap.h"
     21 #include "SkCodec.h"
     22 #include "SkOSFile.h"
     23 #include "SkOSPath.h"
     24 #include "SkPngEncoder.h"
     25 #include "SkStream.h"
     26 
     27 #include "skqp_asset_manager.h"
     28 
     29 #define IMAGES_DIRECTORY_PATH "images"
     30 #define PATH_MAX_PNG "max.png"
     31 #define PATH_MIN_PNG "min.png"
     32 #define PATH_IMG_PNG "image.png"
     33 #define PATH_ERR_PNG "errors.png"
     34 #define PATH_REPORT  "report.html"
     35 #define PATH_CSV     "out.csv"
     36 
     37 #ifndef SK_SKQP_GLOBAL_ERROR_TOLERANCE
     38 #define SK_SKQP_GLOBAL_ERROR_TOLERANCE 0
     39 #endif
     40 
     41 ////////////////////////////////////////////////////////////////////////////////
     42 
     43 static int get_error(uint32_t value, uint32_t value_max, uint32_t value_min) {
     44     int error = 0;
     45     for (int j : {0, 8, 16, 24}) {
     46         uint8_t    v = (value     >> j) & 0xFF,
     47                 vmin = (value_min >> j) & 0xFF,
     48                 vmax = (value_max >> j) & 0xFF;
     49         if (v > vmax) {
     50             error = std::max(v - vmax, error);
     51         } else if (v < vmin) {
     52             error = std::max(vmin - v, error);
     53         }
     54     }
     55     return std::max(0, error - SK_SKQP_GLOBAL_ERROR_TOLERANCE);
     56 }
     57 
     58 static int get_error_with_nearby(int x, int y, const SkPixmap& pm,
     59                                  const SkPixmap& pm_max, const SkPixmap& pm_min) {
     60     struct NearbyPixels {
     61         const int x, y, w, h;
     62         struct Iter {
     63             const int x, y, w, h;
     64             int8_t curr;
     65             SkIPoint operator*() const { return this->get(); }
     66             SkIPoint get() const {
     67                 switch (curr) {
     68                     case 0: return {x - 1, y - 1};
     69                     case 1: return {x    , y - 1};
     70                     case 2: return {x + 1, y - 1};
     71                     case 3: return {x - 1, y    };
     72                     case 4: return {x + 1, y    };
     73                     case 5: return {x - 1, y + 1};
     74                     case 6: return {x    , y + 1};
     75                     case 7: return {x + 1, y + 1};
     76                     default: SkASSERT(false); return {0, 0};
     77                 }
     78             }
     79             void skipBad() {
     80                 while (curr < 8) {
     81                     SkIPoint p = this->get();
     82                     if (p.x() >= 0 && p.y() >= 0 && p.x() < w && p.y() < h) {
     83                         return;
     84                     }
     85                     ++curr;
     86                 }
     87                 curr = -1;
     88             }
     89             void operator++() {
     90                 if (-1 == curr) { return; }
     91                 ++curr;
     92                 this->skipBad();
     93             }
     94             bool operator!=(const Iter& other) const { return curr != other.curr; }
     95         };
     96         Iter begin() const { Iter i{x, y, w, h, 0}; i.skipBad(); return i; }
     97         Iter end() const { return Iter{x, y, w, h, -1}; }
     98     };
     99 
    100     uint32_t c = *pm.addr32(x, y);
    101     int error = get_error(c, *pm_max.addr32(x, y), *pm_min.addr32(x, y));
    102     for (SkIPoint p : NearbyPixels{x, y, pm.width(), pm.height()}) {
    103         if (error == 0) {
    104             return 0;
    105         }
    106         error = SkTMin(error, get_error(
    107                     c, *pm_max.addr32(p.x(), p.y()), *pm_min.addr32(p.x(), p.y())));
    108     }
    109     return error;
    110 }
    111 
    112 static float set_error_code(gmkb::Error* error_out, gmkb::Error error) {
    113     SkASSERT(error != gmkb::Error::kNone);
    114     if (error_out) {
    115         *error_out = error;
    116     }
    117     return FLT_MAX;
    118 }
    119 
    120 static bool WritePixmapToFile(const SkPixmap& pixmap, const char* path) {
    121     SkFILEWStream wStream(path);
    122     SkPngEncoder::Options options;
    123     options.fUnpremulBehavior = SkTransferFunctionBehavior::kIgnore;
    124     return wStream.isValid() && SkPngEncoder::Encode(&wStream, pixmap, options);
    125 }
    126 
    127 constexpr SkColorType kColorType = kRGBA_8888_SkColorType;
    128 constexpr SkAlphaType kAlphaType = kUnpremul_SkAlphaType;
    129 
    130 static SkPixmap rgba8888_to_pixmap(const uint32_t* pixels, int width, int height) {
    131     SkImageInfo info = SkImageInfo::Make(width, height, kColorType, kAlphaType);
    132     return SkPixmap(info, pixels, width * sizeof(uint32_t));
    133 }
    134 
    135 static bool copy(skqp::AssetManager* mgr, const char* path, const char* dst) {
    136     if (mgr) {
    137         if (auto stream = mgr->open(path)) {
    138             SkFILEWStream wStream(dst);
    139             return wStream.isValid() && SkStreamCopy(&wStream, stream.get());
    140         }
    141     }
    142     return false;
    143 }
    144 
    145 static SkBitmap ReadPngRgba8888FromFile(skqp::AssetManager* assetManager, const char* path) {
    146     SkBitmap bitmap;
    147     if (auto codec = SkCodec::MakeFromStream(assetManager->open(path))) {
    148         SkISize size = codec->getInfo().dimensions();
    149         SkASSERT(!size.isEmpty());
    150         SkImageInfo info = SkImageInfo::Make(size.width(), size.height(), kColorType, kAlphaType);
    151         bitmap.allocPixels(info);
    152         SkASSERT(bitmap.rowBytes() == (unsigned)bitmap.width() * sizeof(uint32_t));
    153         if (SkCodec::kSuccess != codec->getPixels(bitmap.pixmap())) {
    154             bitmap.reset();
    155         }
    156     }
    157     return bitmap;
    158 }
    159 
    160 namespace {
    161 struct Run {
    162     SkString fBackend;
    163     SkString fGM;
    164     int fMaxerror;
    165     int fBadpixels;
    166 };
    167 }  // namespace
    168 
    169 static std::vector<Run> gErrors;
    170 static std::mutex gMutex;
    171 
    172 static SkString make_path(const SkString& images_directory,
    173                           const char* backend,
    174                           const char* gm_name,
    175                           const char* thing) {
    176     auto path = SkStringPrintf("%s_%s_%s", backend, gm_name, thing);
    177     return SkOSPath::Join(images_directory.c_str(), path.c_str());
    178 }
    179 
    180 
    181 namespace gmkb {
    182 float Check(const uint32_t* pixels,
    183             int width,
    184             int height,
    185             const char* name,
    186             const char* backend,
    187             skqp::AssetManager* assetManager,
    188             const char* report_directory_path,
    189             Error* error_out) {
    190     if (report_directory_path && report_directory_path[0]) {
    191         SkASSERT_RELEASE(sk_isdir(report_directory_path));
    192     }
    193     if (width <= 0 || height <= 0) {
    194         return set_error_code(error_out, Error::kBadInput);
    195     }
    196     constexpr char PATH_ROOT[] = "gmkb";
    197     SkString img_path = SkOSPath::Join(PATH_ROOT, name);
    198     SkString max_path = SkOSPath::Join(img_path.c_str(), PATH_MAX_PNG);
    199     SkString min_path = SkOSPath::Join(img_path.c_str(), PATH_MIN_PNG);
    200     SkBitmap max_image = ReadPngRgba8888FromFile(assetManager, max_path.c_str());
    201     SkBitmap min_image = ReadPngRgba8888FromFile(assetManager, min_path.c_str());
    202     if (max_image.isNull() || min_image.isNull()) {
    203         // No data.
    204         if (error_out) {
    205             *error_out = Error::kNone;
    206         }
    207         return 0;
    208     }
    209     if (max_image.width()  != min_image.width() ||
    210         max_image.height() != min_image.height())
    211     {
    212         return set_error_code(error_out, Error::kBadData);
    213     }
    214     if (max_image.width() != width || max_image.height() != height) {
    215         return set_error_code(error_out, Error::kBadInput);
    216     }
    217 
    218     int badness = 0;
    219     int badPixelCount = 0;
    220     SkPixmap pm(SkImageInfo::Make(width, height, kColorType, kAlphaType),
    221                 pixels, width * sizeof(uint32_t));
    222     SkPixmap pm_max = max_image.pixmap();
    223     SkPixmap pm_min = min_image.pixmap();
    224     for (int y = 0; y < pm.height(); ++y) {
    225         for (int x = 0; x < pm.width(); ++x) {
    226             int error = get_error_with_nearby(x, y, pm, pm_max, pm_min) ;
    227             if (error > 0) {
    228                 badness = SkTMax(error, badness);
    229                 ++badPixelCount;
    230             }
    231         }
    232     }
    233 
    234     if (badness == 0) {
    235         std::lock_guard<std::mutex> lock(gMutex);
    236         gErrors.push_back(Run{SkString(backend), SkString(name), 0, 0});
    237     }
    238     if (report_directory_path && badness > 0 && report_directory_path[0] != '\0') {
    239         if (!backend) {
    240             backend = "skia";
    241         }
    242         SkString images_directory = SkOSPath::Join(report_directory_path, IMAGES_DIRECTORY_PATH);
    243         sk_mkdir(images_directory.c_str());
    244 
    245         SkString image_path   = make_path(images_directory, backend, name, PATH_IMG_PNG);
    246         SkString error_path   = make_path(images_directory, backend, name, PATH_ERR_PNG);
    247         SkString max_path_out = make_path(images_directory, backend, name, PATH_MAX_PNG);
    248         SkString min_path_out = make_path(images_directory, backend, name, PATH_MIN_PNG);
    249 
    250         SkAssertResult(WritePixmapToFile(rgba8888_to_pixmap(pixels, width, height),
    251                                          image_path.c_str()));
    252 
    253         SkBitmap errorBitmap;
    254         errorBitmap.allocPixels(SkImageInfo::Make(width, height, kColorType, kAlphaType));
    255         for (int y = 0; y < pm.height(); ++y) {
    256             for (int x = 0; x < pm.width(); ++x) {
    257                 int error = get_error_with_nearby(x, y, pm, pm_max, pm_min);
    258                 *errorBitmap.getAddr32(x, y) =
    259                          error > 0 ? 0xFF000000 + (unsigned)error : 0xFFFFFFFF;
    260             }
    261         }
    262         SkAssertResult(WritePixmapToFile(errorBitmap.pixmap(), error_path.c_str()));
    263 
    264         (void)copy(assetManager, max_path.c_str(), max_path_out.c_str());
    265         (void)copy(assetManager, min_path.c_str(), min_path_out.c_str());
    266 
    267         std::lock_guard<std::mutex> lock(gMutex);
    268         gErrors.push_back(Run{SkString(backend), SkString(name), badness, badPixelCount});
    269     }
    270     if (error_out) {
    271         *error_out = Error::kNone;
    272     }
    273     return (float)badness;
    274 }
    275 
    276 static constexpr char kDocHead[] =
    277     "<!doctype html>\n"
    278     "<html lang=\"en\">\n"
    279     "<head>\n"
    280     "<meta charset=\"UTF-8\">\n"
    281     "<title>SkQP Report</title>\n"
    282     "<style>\n"
    283     "img { max-width:48%; border:1px green solid;\n"
    284     "      image-rendering: pixelated;\n"
    285     "      background-image:url('data:image/png;base64,iVBORw0KGgoA"
    286     "AAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAAXNSR0IArs4c6QAAAAJiS0dEAP+H"
    287     "j8y/AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAB3RJTUUH3gUBEi4DGRAQYgAAAB1J"
    288     "REFUGNNjfMoAAVJQmokBDdBHgPE/lPFsYN0BABdaAwN6tehMAAAAAElFTkSuQmCC"
    289     "'); }\n"
    290     "</style>\n"
    291     "<script>\n"
    292     "function ce(t) { return document.createElement(t); }\n"
    293     "function ct(n) { return document.createTextNode(n); }\n"
    294     "function ac(u,v) { return u.appendChild(v); }\n"
    295     "function br(u) { ac(u, ce(\"br\")); }\n"
    296     "function ma(s, c) { var a = ce(\"a\"); a.href = s; ac(a, c); return a; }\n"
    297     "function f(backend, gm, e1, e2) {\n"
    298     "  var b = ce(\"div\");\n"
    299     "  var x = ce(\"h2\");\n"
    300     "  var t = backend + \"/\" + gm;\n"
    301     "  ac(x, ct(t));\n"
    302     "  ac(b, x);\n"
    303     "  ac(b, ct(\"backend: \" + backend));\n"
    304     "  br(b);\n"
    305     "  ac(b, ct(\"gm name: \" + gm));\n"
    306     "  br(b);\n"
    307     "  ac(b, ct(\"maximum error: \" + e1));\n"
    308     "  br(b);\n"
    309     "  ac(b, ct(\"bad pixel counts: \" + e2));\n"
    310     "  br(b);\n"
    311     "  var q = \"" IMAGES_DIRECTORY_PATH "/\" + backend + \"_\" + gm + \"_\";\n"
    312     "  var i = ce(\"img\");\n"
    313     "  i.src = q + \"" PATH_IMG_PNG "\";\n"
    314     "  i.alt = \"img\";\n"
    315     "  ac(b, ma(i.src, i));\n"
    316     "  i = ce(\"img\");\n"
    317     "  i.src = q + \"" PATH_ERR_PNG "\";\n"
    318     "  i.alt = \"err\";\n"
    319     "  ac(b, ma(i.src, i));\n"
    320     "  br(b);\n"
    321     "  ac(b, ct(\"Expectation: \"));\n"
    322     "  ac(b, ma(q + \"" PATH_MAX_PNG "\", ct(\"max\")));\n"
    323     "  ac(b, ct(\" | \"));\n"
    324     "  ac(b, ma(q + \"" PATH_MIN_PNG "\", ct(\"min\")));\n"
    325     "  ac(b, ce(\"hr\"));\n"
    326     "  b.id = backend + \":\" + gm;\n"
    327     "  ac(document.body, b);\n"
    328     "  l = ce(\"li\");\n"
    329     "  ac(l, ct(\"[\" + e1 + \"] \"));\n"
    330     "  ac(l, ma(\"#\" + backend +\":\"+ gm , ct(t)));\n"
    331     "  ac(document.getElementById(\"toc\"), l);\n"
    332     "}\n"
    333     "function main() {\n";
    334 
    335 static constexpr char kDocMiddle[] =
    336     "}\n"
    337     "</script>\n"
    338     "</head>\n"
    339     "<body onload=\"main()\">\n"
    340     "<h1>SkQP Report</h1>\n";
    341 
    342 static constexpr char kDocTail[] =
    343     "<ul id=\"toc\"></ul>\n"
    344     "<hr>\n"
    345     "<p>Left image: test result<br>\n"
    346     "Right image: errors (white = no error, black = smallest error, red = biggest error)</p>\n"
    347     "<hr>\n"
    348     "</body>\n"
    349     "</html>\n";
    350 
    351 static void write(SkWStream* wStream, const SkString& text) {
    352     wStream->write(text.c_str(), text.size());
    353 }
    354 
    355 enum class Backend {
    356     kUnknown,
    357     kGLES,
    358     kVulkan,
    359 };
    360 
    361 static Backend get_backend(const SkString& s) {
    362     if (s.equals("gles")) {
    363         return Backend::kGLES;
    364     } else if (s.equals("vk")) {
    365         return Backend::kVulkan;
    366     }
    367     return Backend::kUnknown;
    368 }
    369 
    370 
    371 bool MakeReport(const char* report_directory_path) {
    372     int glesErrorCount = 0, vkErrorCount = 0, gles = 0, vk = 0;
    373 
    374     SkASSERT_RELEASE(sk_isdir(report_directory_path));
    375     std::lock_guard<std::mutex> lock(gMutex);
    376     SkFILEWStream csvOut(SkOSPath::Join(report_directory_path, PATH_CSV).c_str());
    377     SkFILEWStream htmOut(SkOSPath::Join(report_directory_path, PATH_REPORT).c_str());
    378     SkASSERT_RELEASE(csvOut.isValid());
    379     if (!csvOut.isValid() || !htmOut.isValid()) {
    380         return false;
    381     }
    382     htmOut.writeText(kDocHead);
    383     for (const Run& run : gErrors) {
    384         auto backend = get_backend(run.fBackend);
    385         switch (backend) {
    386             case Backend::kGLES: ++gles; break;
    387             case Backend::kVulkan: ++vk; break;
    388             default: break;
    389         }
    390         write(&csvOut, SkStringPrintf("\"%s\",\"%s\",%d,%d\n",
    391                                       run.fBackend.c_str(), run.fGM.c_str(),
    392                                       run.fMaxerror, run.fBadpixels));
    393         if (run.fMaxerror == 0 && run.fBadpixels == 0) {
    394             continue;
    395         }
    396         write(&htmOut, SkStringPrintf("  f(\"%s\", \"%s\", %d, %d);\n",
    397                                       run.fBackend.c_str(), run.fGM.c_str(),
    398                                       run.fMaxerror, run.fBadpixels));
    399         switch (backend) {
    400             case Backend::kGLES: ++glesErrorCount; break;
    401             case Backend::kVulkan: ++vkErrorCount; break;
    402             default: break;
    403         }
    404     }
    405     htmOut.writeText(kDocMiddle);
    406     write(&htmOut, SkStringPrintf("<p>gles errors: %d (of %d)</br>\n"
    407                                   "vk errors: %d (of %d)</p>\n",
    408                                   glesErrorCount, gles, vkErrorCount, vk));
    409     htmOut.writeText(kDocTail);
    410     return true;
    411 }
    412 }  // namespace gmkb
    413