blob: 67ab366f9ac64f58582e29404a646bc88b9094d7 [file] [log] [blame]
/*
* 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 "tests/TestHarness.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, WASMPointerU8 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(), skgpu::Budgeted::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.fName);
tests.call<void>("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::MakeCPU(nullptr, 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.fTestType == skiatest::TestType::kGanesh) {
result.set("result", "passed"); // default to passing - the reporter will mark failed.
WasmReporter reporter(name, result);
test.modifyGrContextOptions(&grOpts);
test.ganesh(&reporter, grOpts);
return result;
} else if (test.fTestType == skiatest::TestType::kGraphite) {
SkDebugf("Graphite test %s not yet supported\n", name.c_str());
return result;
}
result.set("result", "passed"); // default to passing - the reporter will mark failed.
WasmReporter reporter(name, result);
test.cpu(&reporter);
return result;
}
namespace skiatest {
using ContextType = sk_gpu_test::GrContextFactory::ContextType;
// These are the supported GrContextTypeFilterFn. They are defined in Test.h and implemented here.
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 RunWithGaneshTestContexts(GrContextTestFn* testFn, GrContextTypeFilterFn* filter,
Reporter* reporter, const GrContextOptions& options) {
for (auto contextType : {ContextType::kGLES_ContextType, ContextType::kMock_ContextType}) {
if (filter && !(*filter)(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
(*testFn)(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<GLTestContext> 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<void()> 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;
}
TestHarness CurrentTestHarness() {
return TestHarness::kWasmGMTests;
}
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>");
}