/* * 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 #include #include #include #include #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 gm(fact()); SkDebugf("gm %s\n", gm->getName()); gms.call("push", std::string(gm->getName())); } return gms; } static std::unique_ptr getGMWithName(std::string name) { for (skiagm::GMFactory fact : skiagm::GMRegistry::Range()) { std::unique_ptr 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 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 dContext((GrDirectContext::MakeGL(interface))); return dContext; } static std::set gKnownDigests; static void LoadKnownDigest(std::string md5) { gKnownDigests.insert(md5); } static std::map> gResources; static sk_sp 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, WASMPointerU8 bPtr, size_t len) { const uint8_t* bytes = reinterpret_cast(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 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 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 = std::make_unique(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("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.fName); tests.call("push", std::string(test.fName)); } return tests; } static skiatest::Test getTestWithName(std::string name, bool* ok) { for (auto test : skiatest::TestRegistry::Range()) { if (name == test.fName) { *ok = true; return test; } } *ok = false; return skiatest::Test(nullptr, /*gpu*/ false, /*graphite*/ 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.fNeedsGpu) { result.set("result", "passed"); // default to passing - the reporter will mark failed. WasmReporter reporter(name, result); test.modifyGrContextOptions(&grOpts); 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 { 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 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_gpu_test::GrContextFactory factory(options); sk_gpu_test::ContextInfo ctxInfo = factory.getContextInfo(contextType); REPORTER_ASSERT(reporter, ctxInfo.directContext() != nullptr); if (!ctxInfo.directContext()) { return; } ctxInfo.testContext()->makeCurrent(); // From DMGpuTestProcs.cpp (*test)(reporter, ctxInfo); // Sync so any release/finished procs get called. ctxInfo.directContext()->flushAndSubmit(/*sync*/true); } } } // namespace skiatest namespace { // A GLtestContext that we can return from CreatePlatformGLTestContext below. // It doesn't have to do anything WebGL-specific that I know of but we can't return // a GLTestContext because it has pure virtual methods that need to be implemented. class WasmWebGlTestContext : public sk_gpu_test::GLTestContext { public: WasmWebGlTestContext() {} ~WasmWebGlTestContext() override { this->teardown(); } // We assume WebGL only has one context and that it is always current. // Therefore these context related functions return null intentionally. // It's possible that more tests will pass if these were correctly implemented. std::unique_ptr makeNew() const override { // This is supposed to create a new GL context in a new GLTestContext. // Specifically for tests that do not want to re-use the existing one. return nullptr; } void onPlatformMakeNotCurrent() const override { } void onPlatformMakeCurrent() const override { } std::function onPlatformGetAutoContextRestore() const override { return nullptr; } GrGLFuncPtr onPlatformGetProcAddress(const char* procName) const override { return nullptr; } }; } // namespace namespace sk_gpu_test { GLTestContext *CreatePlatformGLTestContext(GrGLStandard forcedGpuAPI, GLTestContext *shareContext) { return new WasmWebGlTestContext(); } } // 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") .smart_ptr>("sk_sp"); }