/* * Copyright 2018 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "skqp.h" #include "../../../src/core/SkStreamPriv.h" #include "../../tools/fonts/SkTestFontMgr.h" #include "GrContext.h" #include "GrContextOptions.h" #include "GrContextPriv.h" #include "SkFontMgrPriv.h" #include "SkFontStyle.h" #include "SkGraphics.h" #include "SkImageInfoPriv.h" #include "SkOSFile.h" #include "SkOSPath.h" #include "SkPngEncoder.h" #include "SkStream.h" #include "SkSurface.h" #include "Test.h" #include "gl/GLTestContext.h" #include "gm.h" #include "vk/VkTestContext.h" #include #include #include #include "skqp_model.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_MODEL "model" static constexpr char kRenderTestCSVReport[] = "out.csv"; static constexpr char kRenderTestReportPath[] = "report.html"; static constexpr char kRenderTestsPath[] = "skqp/rendertests.txt"; static constexpr char kUnitTestReportPath[] = "unit_tests.txt"; static constexpr char kUnitTestsPath[] = "skqp/unittests.txt"; // Kind of like Python's readlines(), but without any allocation. // Calls f() on each line. // F is [](const char*, size_t) -> void template static void readlines(const void* data, size_t size, F f) { const char* start = (const char*)data; const char* end = start + size; const char* ptr = start; while (ptr < end) { while (*ptr++ != '\n' && ptr < end) {} size_t len = ptr - start; f(start, len); start = ptr; } } static void get_unit_tests(SkQPAssetManager* mgr, std::vector* unitTests) { std::unordered_set testset; auto insert = [&testset](const char* s, size_t l) { SkASSERT(l > 1) ; if (l > 0 && s[l - 1] == '\n') { // strip line endings. --l; } if (l > 0) { // only add non-empty strings. testset.insert(std::string(s, l)); } }; if (sk_sp dat = mgr->open(kUnitTestsPath)) { readlines(dat->data(), dat->size(), insert); } for (const skiatest::Test& test : skiatest::TestRegistry::Range()) { if ((testset.empty() || testset.count(std::string(test.name)) > 0) && test.needsGpu) { unitTests->push_back(&test); } } auto lt = [](SkQP::UnitTest u, SkQP::UnitTest v) { return strcmp(u->name, v->name) < 0; }; std::sort(unitTests->begin(), unitTests->end(), lt); } static void get_render_tests(SkQPAssetManager* mgr, std::vector* gmlist, std::unordered_map* gmThresholds) { auto insert = [gmThresholds](const char* s, size_t l) { SkASSERT(l > 1) ; if (l > 0 && s[l - 1] == '\n') { // strip line endings. --l; } if (l == 0) { return; } const char* end = s + l; const char* ptr = s; constexpr char kDelimeter = ','; while (ptr < end && *ptr != kDelimeter) { ++ptr; } if (ptr + 1 >= end) { SkASSERT(false); // missing delimeter return; } std::string key(s, ptr - s); ++ptr; // skip delimeter std::string number(ptr, end - ptr); // null-terminated copy. int64_t value = 0; if (1 != sscanf(number.c_str(), "%" SCNd64 , &value)) { SkASSERT(false); // Not a number return; } gmThresholds->insert({std::move(key), value}); // (*gmThresholds)[s] = value; }; if (sk_sp dat = mgr->open(kRenderTestsPath)) { readlines(dat->data(), dat->size(), insert); } using GmAndName = std::pair; std::vector gmsWithNames; for (skiagm::GMFactory f : skiagm::GMRegistry::Range()) { std::string name = SkQP::GetGMName(f); if ((gmThresholds->empty() || gmThresholds->count(name) > 0)) { gmsWithNames.push_back(std::make_pair(f, std::move(name))); } } std::sort(gmsWithNames.begin(), gmsWithNames.end(), [](GmAndName u, GmAndName v) { return u.second < v.second; }); gmlist->reserve(gmsWithNames.size()); for (const GmAndName& gmn : gmsWithNames) { gmlist->push_back(gmn.first); } } static std::unique_ptr make_test_context(SkQP::SkiaBackend backend) { using U = std::unique_ptr; switch (backend) { case SkQP::SkiaBackend::kGL: return U(sk_gpu_test::CreatePlatformGLTestContext(kGL_GrGLStandard, nullptr)); case SkQP::SkiaBackend::kGLES: return U(sk_gpu_test::CreatePlatformGLTestContext(kGLES_GrGLStandard, nullptr)); #ifdef SK_VULKAN case SkQP::SkiaBackend::kVulkan: return U(sk_gpu_test::CreatePlatformVkTestContext(nullptr)); #endif default: return nullptr; } } static GrContextOptions context_options(skiagm::GM* gm = nullptr) { GrContextOptions grContextOptions; grContextOptions.fAllowPathMaskCaching = true; grContextOptions.fSuppressPathRendering = true; grContextOptions.fDisableDriverCorrectnessWorkarounds = true; if (gm) { gm->modifyGrContextOptions(&grContextOptions); } return grContextOptions; } static std::vector get_backends() { std::vector result; SkQP::SkiaBackend backends[] = { #ifndef SK_BUILD_FOR_ANDROID SkQP::SkiaBackend::kGL, // Used for testing on desktop machines. #endif SkQP::SkiaBackend::kGLES, #ifdef SK_VULKAN SkQP::SkiaBackend::kVulkan, #endif }; for (SkQP::SkiaBackend backend : backends) { std::unique_ptr testCtx = make_test_context(backend); if (testCtx) { testCtx->makeCurrent(); if (nullptr != testCtx->makeGrContext(context_options())) { result.push_back(backend); } } } SkASSERT_RELEASE(result.size() > 0); return result; } static void print_backend_info(const char* dstPath, const std::vector& backends) { #ifdef SK_ENABLE_DUMP_GPU SkFILEWStream out(dstPath); out.writeText("[\n"); for (SkQP::SkiaBackend backend : backends) { if (std::unique_ptr testCtx = make_test_context(backend)) { testCtx->makeCurrent(); if (sk_sp ctx = testCtx->makeGrContext(context_options())) { SkString info = ctx->priv().dump(); // remove null out.write(info.c_str(), info.size()); out.writeText(",\n"); } } } out.writeText("]\n"); #endif } static void encode_png(const SkBitmap& src, const std::string& dst) { SkFILEWStream wStream(dst.c_str()); SkPngEncoder::Options options; bool success = wStream.isValid() && SkPngEncoder::Encode(&wStream, src.pixmap(), options); SkASSERT_RELEASE(success); } static void write_to_file(const sk_sp& src, const std::string& dst) { SkFILEWStream wStream(dst.c_str()); bool success = wStream.isValid() && wStream.write(src->data(), src->size()); SkASSERT_RELEASE(success); } //////////////////////////////////////////////////////////////////////////////// const char* SkQP::GetBackendName(SkQP::SkiaBackend b) { switch (b) { case SkQP::SkiaBackend::kGL: return "gl"; case SkQP::SkiaBackend::kGLES: return "gles"; case SkQP::SkiaBackend::kVulkan: return "vk"; } return ""; } std::string SkQP::GetGMName(SkQP::GMFactory f) { std::unique_ptr gm(f ? f(nullptr) : nullptr); return std::string(gm ? gm->getName() : ""); } const char* SkQP::GetUnitTestName(SkQP::UnitTest t) { return t->name; } SkQP::SkQP() {} SkQP::~SkQP() {} void SkQP::init(SkQPAssetManager* am, const char* reportDirectory) { SkASSERT_RELEASE(!fAssetManager); SkASSERT_RELEASE(am); fAssetManager = am; fReportDirectory = reportDirectory; SkGraphics::Init(); gSkFontMgr_DefaultFactory = &sk_tool_utils::MakePortableFontMgr; /* If the file "skqp/rendertests.txt" does not exist or is empty, run all render tests. Otherwise only run tests mentioned in that file. */ get_render_tests(fAssetManager, &fGMs, &fGMThresholds); /* If the file "skqp/unittests.txt" does not exist or is empty, run all gpu unit tests. Otherwise only run tests mentioned in that file. */ get_unit_tests(fAssetManager, &fUnitTests); fSupportedBackends = get_backends(); print_backend_info((fReportDirectory + "/grdump.txt").c_str(), fSupportedBackends); } std::tuple SkQP::evaluateGM(SkQP::SkiaBackend backend, SkQP::GMFactory gmFact) { SkASSERT_RELEASE(fAssetManager); static constexpr SkQP::RenderOutcome kError = {INT_MAX, INT_MAX, INT64_MAX}; static constexpr SkQP::RenderOutcome kPass = {0, 0, 0}; std::unique_ptr testCtx = make_test_context(backend); if (!testCtx) { return std::make_tuple(kError, "Skia Failure: test context"); } testCtx->makeCurrent(); SkASSERT(gmFact); std::unique_ptr gm(gmFact(nullptr)); SkASSERT(gm); const char* const name = gm->getName(); const SkISize size = gm->getISize(); const int w = size.width(); const int h = size.height(); const SkImageInfo info = SkImageInfo::Make(w, h, skqp::kColorType, kPremul_SkAlphaType, nullptr); const SkSurfaceProps props(0, SkSurfaceProps::kLegacyFontHost_InitType); sk_sp surf = SkSurface::MakeRenderTarget( testCtx->makeGrContext(context_options(gm.get())).get(), SkBudgeted::kNo, info, 0, &props); if (!surf) { return std::make_tuple(kError, "Skia Failure: gr-context"); } gm->draw(surf->getCanvas()); SkBitmap image; image.allocPixels(SkImageInfo::Make(w, h, skqp::kColorType, skqp::kAlphaType)); // SkColorTypeBytesPerPixel should be constexpr, but is not. SkASSERT(SkColorTypeBytesPerPixel(skqp::kColorType) == sizeof(uint32_t)); // Call readPixels because we need to compare pixels. if (!surf->readPixels(image.pixmap(), 0, 0)) { return std::make_tuple(kError, "Skia Failure: read pixels"); } int64_t passingThreshold = fGMThresholds.empty() ? -1 : fGMThresholds[std::string(name)]; if (-1 == passingThreshold) { return std::make_tuple(kPass, ""); } skqp::ModelResult modelResult = skqp::CheckAgainstModel(name, image.pixmap(), fAssetManager); if (!modelResult.fErrorString.empty()) { return std::make_tuple(kError, std::move(modelResult.fErrorString)); } fRenderResults.push_back(SkQP::RenderResult{backend, gmFact, modelResult.fOutcome}); if (modelResult.fOutcome.fMaxError <= passingThreshold) { return std::make_tuple(kPass, ""); } std::string imagesDirectory = fReportDirectory + "/" IMAGES_DIRECTORY_PATH; if (!sk_mkdir(imagesDirectory.c_str())) { SkDebugf("ERROR: sk_mkdir('%s');\n", imagesDirectory.c_str()); return std::make_tuple(modelResult.fOutcome, ""); } std::ostringstream tmp; tmp << imagesDirectory << '/' << SkQP::GetBackendName(backend) << '_' << name << '_'; std::string imagesPathPrefix1 = tmp.str(); tmp = std::ostringstream(); tmp << imagesDirectory << '/' << PATH_MODEL << '_' << name << '_'; std::string imagesPathPrefix2 = tmp.str(); encode_png(image, imagesPathPrefix1 + PATH_IMG_PNG); encode_png(modelResult.fErrors, imagesPathPrefix1 + PATH_ERR_PNG); write_to_file(modelResult.fMaxPng, imagesPathPrefix2 + PATH_MAX_PNG); write_to_file(modelResult.fMinPng, imagesPathPrefix2 + PATH_MIN_PNG); return std::make_tuple(modelResult.fOutcome, ""); } std::vector SkQP::executeTest(SkQP::UnitTest test) { SkASSERT_RELEASE(fAssetManager); struct : public skiatest::Reporter { std::vector fErrors; void reportFailed(const skiatest::Failure& failure) override { SkString desc = failure.toString(); fErrors.push_back(std::string(desc.c_str(), desc.size())); } } r; GrContextOptions options; options.fDisableDriverCorrectnessWorkarounds = true; if (test->fContextOptionsProc) { test->fContextOptionsProc(&options); } test->proc(&r, options); fUnitTestResults.push_back(UnitTestResult{test, r.fErrors}); return r.fErrors; } //////////////////////////////////////////////////////////////////////////////// static constexpr char kDocHead[] = "\n" "\n" "\n" "\n" "SkQP Report\n" "\n" "\n" "\n" "\n" "

SkQP Report

\n"; static constexpr char kDocTail[] = "
    \n" "
    \n" "

    Left image: test result
    \n" "Right image: errors (white = no error, black = smallest error, red = biggest error; " "other errors are a color between black and red.)

    \n" "
    \n" "\n" "\n"; template inline void write(SkWStream* wStream, const T& text) { wStream->write(text.c_str(), text.size()); } void SkQP::makeReport() { SkASSERT_RELEASE(fAssetManager); int glesErrorCount = 0, vkErrorCount = 0, gles = 0, vk = 0; if (!sk_isdir(fReportDirectory.c_str())) { SkDebugf("Report destination does not exist: '%s'\n", fReportDirectory.c_str()); return; } SkFILEWStream csvOut(SkOSPath::Join(fReportDirectory.c_str(), kRenderTestCSVReport).c_str()); SkFILEWStream htmOut(SkOSPath::Join(fReportDirectory.c_str(), kRenderTestReportPath).c_str()); SkASSERT_RELEASE(csvOut.isValid() && htmOut.isValid()); htmOut.writeText(kDocHead); for (const SkQP::RenderResult& run : fRenderResults) { switch (run.fBackend) { case SkQP::SkiaBackend::kGLES: ++gles; break; case SkQP::SkiaBackend::kVulkan: ++vk; break; default: break; } const char* backendName = SkQP::GetBackendName(run.fBackend); std::string gmName = SkQP::GetGMName(run.fGM); const SkQP::RenderOutcome& outcome = run.fOutcome; auto str = SkStringPrintf("\"%s\",\"%s\",%d,%d,%" PRId64, backendName, gmName.c_str(), outcome.fMaxError, outcome.fBadPixelCount, outcome.fTotalError); write(&csvOut, SkStringPrintf("%s\n", str.c_str())); int64_t passingThreshold = fGMThresholds.empty() ? 0 : fGMThresholds[gmName]; if (passingThreshold == -1 || outcome.fMaxError <= passingThreshold) { continue; } write(&htmOut, SkStringPrintf(" f(%s);\n", str.c_str())); switch (run.fBackend) { case SkQP::SkiaBackend::kGLES: ++glesErrorCount; break; case SkQP::SkiaBackend::kVulkan: ++vkErrorCount; break; default: break; } } htmOut.writeText(kDocMiddle); write(&htmOut, SkStringPrintf("

    gles errors: %d (of %d)
    \n" "vk errors: %d (of %d)

    \n", glesErrorCount, gles, vkErrorCount, vk)); htmOut.writeText(kDocTail); SkFILEWStream unitOut(SkOSPath::Join(fReportDirectory.c_str(), kUnitTestReportPath).c_str()); SkASSERT_RELEASE(unitOut.isValid()); for (const SkQP::UnitTestResult& result : fUnitTestResults) { unitOut.writeText(GetUnitTestName(result.fUnitTest)); if (result.fErrors.empty()) { unitOut.writeText(" PASSED\n* * *\n"); } else { write(&unitOut, SkStringPrintf(" FAILED (%u errors)\n", result.fErrors.size())); for (const std::string& err : result.fErrors) { write(&unitOut, err); unitOut.newline(); } unitOut.writeText("* * *\n"); } } }