skia2/modules/canvaskit/gm_bindings.cpp
Kevin Lubick dffd20efe9 [canvaskit] Add unit tests to wasm_gm_tests
There are currently many tests skipped, but many more pass.
This changes the built binary to have a lot of debugging logic
in it so we should be able to get backtraces on those crashes
more easily when debugging.

gmtests.html was removed as it was superceded by run-wasm-gm-tests
and make run_local.

Bug: skia:10812, skia:10869
Change-Id: I72ab34d3db83a654dc8829831b3ecb795fe23d43
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/329170
Reviewed-by: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Nathaniel Nifong <nifong@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
2020-11-02 16:51:23 +00:00

353 lines
12 KiB
C++

/*
* Copyright 2020 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <set>
#include <string>
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/html5.h>
#include "gm/gm.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkData.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkStream.h"
#include "include/core/SkSurface.h"
#include "include/gpu/GrContextOptions.h"
#include "include/gpu/GrDirectContext.h"
#include "include/gpu/gl/GrGLInterface.h"
#include "include/gpu/gl/GrGLTypes.h"
#include "modules/canvaskit/WasmCommon.h"
#include "src/core/SkFontMgrPriv.h"
#include "src/core/SkMD5.h"
#include "tests/Test.h"
#include "tools/HashAndEncode.h"
#include "tools/ResourceFactory.h"
#include "tools/flags/CommandLineFlags.h"
#include "tools/fonts/TestFontMgr.h"
using namespace emscripten;
/**
* Returns a JS array of strings containing the names of the registered GMs. GMs are only registered
* when their source is included in the "link" step, not if they are in something like libgm.a.
* The names are also logged to the console.
*/
static JSArray ListGMs() {
SkDebugf("Listing GMs\n");
JSArray gms = emscripten::val::array();
for (skiagm::GMFactory fact : skiagm::GMRegistry::Range()) {
std::unique_ptr<skiagm::GM> gm(fact());
SkDebugf("gm %s\n", gm->getName());
gms.call<void>("push", std::string(gm->getName()));
}
return gms;
}
static std::unique_ptr<skiagm::GM> getGMWithName(std::string name) {
for (skiagm::GMFactory fact : skiagm::GMRegistry::Range()) {
std::unique_ptr<skiagm::GM> gm(fact());
if (gm->getName() == name) {
return gm;
}
}
return nullptr;
}
/**
* Sets the given WebGL context to be "current" and then creates a GrDirectContext from that
* context.
*/
static sk_sp<GrDirectContext> MakeGrContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context)
{
EMSCRIPTEN_RESULT r = emscripten_webgl_make_context_current(context);
if (r < 0) {
printf("failed to make webgl context current %d\n", r);
return nullptr;
}
// setup GrDirectContext
auto interface = GrGLMakeNativeInterface();
// setup contexts
sk_sp<GrDirectContext> dContext(GrDirectContext::MakeGL(interface));
return dContext;
}
static std::set<std::string> gKnownDigests;
static void LoadKnownDigest(std::string md5) {
gKnownDigests.insert(md5);
}
static std::map<std::string, sk_sp<SkData>> gResources;
static sk_sp<SkData> getResource(const char* name) {
auto it = gResources.find(name);
if (it == gResources.end()) {
SkDebugf("Resource %s not found\n", name);
return nullptr;
}
return it->second;
}
static void LoadResource(std::string name, uintptr_t /* byte* */ bPtr, size_t len) {
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(bPtr);
auto data = SkData::MakeFromMalloc(bytes, len);
gResources[name] = std::move(data);
if (!gResourceFactory) {
gResourceFactory = getResource;
}
}
/**
* Runs the given GM and returns a JS object. If the GM was successful, the object will have the
* following properties:
* "png" - a Uint8Array of the PNG data extracted from the surface.
* "hash" - a string which is the md5 hash of the pixel contents and the metadata.
*/
static JSObject RunGM(sk_sp<GrDirectContext> ctx, std::string name) {
JSObject result = emscripten::val::object();
auto gm = getGMWithName(name);
if (!gm) {
SkDebugf("Could not find gm with name %s\n", name.c_str());
return result;
}
// TODO(kjlubick) make these configurable somehow. This probably makes sense to do as function
// parameters.
auto alphaType = SkAlphaType::kPremul_SkAlphaType;
auto colorType = SkColorType::kN32_SkColorType;
SkISize size = gm->getISize();
SkImageInfo info = SkImageInfo::Make(size, colorType, alphaType);
sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(ctx.get(),
SkBudgeted::kYes,
info, 0,
kBottomLeft_GrSurfaceOrigin,
nullptr, true));
if (!surface) {
SkDebugf("Could not make surface\n");
return result;
}
auto canvas = surface->getCanvas();
gm->onceBeforeDraw();
SkString msg;
// Based on GMSrc::draw from DM.
auto gpuSetupResult = gm->gpuSetup(ctx.get(), canvas, &msg);
if (gpuSetupResult == skiagm::DrawResult::kFail) {
SkDebugf("Error with gpu setup for gm %s: %s\n", name.c_str(), msg.c_str());
return result;
} else if (gpuSetupResult == skiagm::DrawResult::kSkip) {
return result;
}
auto drawResult = gm->draw(canvas, &msg);
if (drawResult == skiagm::DrawResult::kFail) {
SkDebugf("Error with gm %s: %s\n", name.c_str(), msg.c_str());
return result;
} else if (drawResult == skiagm::DrawResult::kSkip) {
return result;
}
surface->flushAndSubmit(true);
// Based on GPUSink::readBack
SkBitmap bitmap;
bitmap.allocPixels(info);
if (!canvas->readPixels(bitmap, 0, 0)) {
SkDebugf("Could not read pixels back\n");
return result;
}
// Now we need to encode to PNG and get the md5 hash of the pixels (and colorspace and stuff).
// This is based on Task::Run from DM.cpp
std::unique_ptr<HashAndEncode> hashAndEncode = std::make_unique<HashAndEncode>(bitmap);
SkString md5;
SkMD5 hash;
hashAndEncode->feedHash(&hash);
SkMD5::Digest digest = hash.finish();
for (int i = 0; i < 16; i++) {
md5.appendf("%02x", digest.data[i]);
}
auto ok = gKnownDigests.find(md5.c_str());
if (ok == gKnownDigests.end()) {
// We only need to decode the image if it is "interesting", that is, we have not written it
// before to disk and uploaded it to gold.
SkDynamicMemoryWStream stream;
// We do not need to include the keys because they are optional - they are not read by Gold.
CommandLineFlags::StringArray empty;
hashAndEncode->encodePNG(&stream, md5.c_str(), empty, empty);
auto data = stream.detachAsData();
// This is the cleanest way to create a new Uint8Array with a copy of the data that is not
// in the WASM heap. kjlubick tried returning a pointer inside an SkData, but that lead to
// some use after free issues. By making the copy using the JS transliteration, we don't
// risk the SkData object being cleaned up before we make the copy.
Uint8Array pngData = emscripten::val(
// https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#memory-views
typed_memory_view(data->size(), data->bytes())
).call<Uint8Array>("slice"); // slice with no args makes a copy of the memory view.
result.set("png", pngData);
gKnownDigests.emplace(md5.c_str());
}
result.set("hash", md5.c_str());
return result;
}
static JSArray ListTests() {
SkDebugf("Listing Tests\n");
JSArray tests = emscripten::val::array();
for (auto test : skiatest::TestRegistry::Range()) {
SkDebugf("test %s\n", test.name);
tests.call<void>("push", std::string(test.name));
}
return tests;
}
static skiatest::Test getTestWithName(std::string name, bool* ok) {
for (auto test : skiatest::TestRegistry::Range()) {
if (name == test.name) {
*ok = true;
return test;
}
}
*ok = false;
return skiatest::Test(nullptr, false, nullptr);
}
// Based on DM.cpp:run_test
struct WasmReporter : public skiatest::Reporter {
WasmReporter(std::string name, JSObject result): fName(name), fResult(result){}
void reportFailed(const skiatest::Failure& failure) override {
SkDebugf("Test %s failed: %s\n", fName.c_str(), failure.toString().c_str());
fResult.set("result", "failed");
fResult.set("msg", failure.toString().c_str());
}
std::string fName;
JSObject fResult;
};
/**
* Runs the given Test and returns a JS object. If the Test was located, the object will have the
* following properties:
* "result" : One of "passed", "failed", "skipped".
* "msg": May be non-empty on failure
*/
static JSObject RunTest(std::string name) {
JSObject result = emscripten::val::object();
bool ok = false;
auto test = getTestWithName(name, &ok);
if (!ok) {
SkDebugf("Could not find test with name %s\n", name.c_str());
return result;
}
GrContextOptions grOpts;
if (test.needsGpu) {
result.set("result", "passed"); // default to passing - the reporter will mark failed.
WasmReporter reporter(name, result);
test.run(&reporter, grOpts);
return result;
}
result.set("result", "passed"); // default to passing - the reporter will mark failed.
WasmReporter reporter(name, result);
test.run(&reporter, grOpts);
return result;
}
namespace skiatest {
class WasmContextInfo : public sk_gpu_test::ContextInfo {
public:
WasmContextInfo(GrDirectContext* context,
const GrContextOptions& options)
: fContext(context), fOptions(options) {}
GrDirectContext* directContext() const { return fContext; }
sk_gpu_test::TestContext* testContext() const { return nullptr; }
sk_gpu_test::GLTestContext* glContext() const { return nullptr; }
const GrContextOptions& options() const { return fOptions; }
private:
GrDirectContext* fContext = nullptr;
GrContextOptions fOptions;
};
using ContextType = sk_gpu_test::GrContextFactory::ContextType;
// These are the supported GrContextTypeFilterFn
bool IsGLContextType(ContextType ct) {
return GrBackendApi::kOpenGL == sk_gpu_test::GrContextFactory::ContextTypeBackend(ct);
}
bool IsRenderingGLContextType(ContextType ct) {
return IsGLContextType(ct) && sk_gpu_test::GrContextFactory::IsRenderingContext(ct);
}
bool IsRenderingGLOrMetalContextType(ContextType ct) {
return IsRenderingGLContextType(ct);
}
bool IsMockContextType(ContextType ct) {
return ct == ContextType::kMock_ContextType;
}
// These are not supported
bool IsVulkanContextType(ContextType) {return false;}
bool IsMetalContextType(ContextType) {return false;}
bool IsDirect3DContextType(ContextType) {return false;}
bool IsDawnContextType(ContextType) {return false;}
void RunWithGPUTestContexts(GrContextTestFn* test, GrContextTypeFilterFn* contextTypeFilter,
Reporter* reporter, const GrContextOptions& options) {
for (auto contextType : {ContextType::kGLES_ContextType, ContextType::kMock_ContextType}) {
if (contextTypeFilter && !(*contextTypeFilter)(contextType)) {
continue;
}
sk_sp<GrDirectContext> ctx = (contextType == ContextType::kGLES_ContextType) ?
GrDirectContext::MakeGL(options) :
GrDirectContext::MakeMock(nullptr, options);
if (!ctx) {
SkDebugf("Could not make context\n");
return;
}
WasmContextInfo ctxInfo(ctx.get(), options);
// From DMGpuTestProcs.cpp
(*test)(reporter, ctxInfo);
// Sync so any release/finished procs get called.
ctxInfo.directContext()->flushAndSubmit(/*sync*/true);
}
}
} // namespace skiatest
namespace sk_gpu_test {
GLTestContext *CreatePlatformGLTestContext(GrGLStandard forcedGpuAPI,
GLTestContext *shareContext) {
return nullptr;
}
} // namespace sk_gpu_test
void Init() {
// Use the portable fonts.
gSkFontMgr_DefaultFactory = &ToolUtils::MakePortableFontMgr;
}
EMSCRIPTEN_BINDINGS(GMs) {
function("Init", &Init);
function("ListGMs", &ListGMs);
function("ListTests", &ListTests);
function("LoadKnownDigest", &LoadKnownDigest);
function("_LoadResource", &LoadResource);
function("MakeGrContext", &MakeGrContext);
function("RunGM", &RunGM);
function("RunTest", &RunTest);
class_<GrDirectContext>("GrDirectContext")
.smart_ptr<sk_sp<GrDirectContext>>("sk_sp<GrDirectContext>");
}