b62f50cf76
- Encoders and decoders always assume kIgnore. - They are less opinionated about F16 and color space, we just trust the color space that's passed in, and put that directly in the image (no sRGB encoding). - SkBitmap and SkPixmap read/write pixels functions were defaulting to kResepct, those are now always kIgnore. - Many other bits of plumbing are simplified, and I added a default of kIgnore to SkImage::makeColorSpace, so we can phase out that argument entirely. - Still need to add defaults to other public APIs that take SkTransferFunctionBehavior. - This makes gold think that we've dramatically changed the contents of all F16 images, but that's because it doesn't understand the (now linear) color space that's embedded. Once we triage them all once, they will work fine (and they'll look perfect in the browser). Bug: skia: Change-Id: I62fa090f96cae1b67d181ce14bd91f34ff2ed747 Reviewed-on: https://skia-review.googlesource.com/140570 Commit-Queue: Brian Osman <brianosman@google.com> Reviewed-by: Mike Klein <mtklein@google.com>
411 lines
14 KiB
C++
411 lines
14 KiB
C++
/*
|
|
* 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);
|
|
return wStream.isValid() && SkPngEncoder::Encode(&wStream, pixmap, SkPngEncoder::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
|