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
43static 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
58static 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
112static 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
120static 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
127constexpr SkColorType kColorType = kRGBA_8888_SkColorType;
128constexpr SkAlphaType kAlphaType = kUnpremul_SkAlphaType;
129
130static 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
135static 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
145static 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
160namespace {
161struct Run {
162    SkString fBackend;
163    SkString fGM;
164    int fMaxerror;
165    int fBadpixels;
166};
167}  // namespace
168
169static std::vector<Run> gErrors;
170static std::mutex gMutex;
171
172static 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
181namespace gmkb {
182float 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
276static 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
335static constexpr char kDocMiddle[] =
336    "}\n"
337    "</script>\n"
338    "</head>\n"
339    "<body onload=\"main()\">\n"
340    "<h1>SkQP Report</h1>\n";
341
342static 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
351static void write(SkWStream* wStream, const SkString& text) {
352    wStream->write(text.c_str(), text.size());
353}
354
355enum class Backend {
356    kUnknown,
357    kGLES,
358    kVulkan,
359};
360
361static 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
371bool 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