blob: df1ded0660b6dd46604e6bc703f0d381d610a97a [file] [log] [blame]
/*
* 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