blob: 233303e01c39c6929bc111be4b20ea7adc471e5e [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/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 "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;
}
void Init() {
// Use the portable fonts.
gSkFontMgr_DefaultFactory = &ToolUtils::MakePortableFontMgr;
}
EMSCRIPTEN_BINDINGS(GMs) {
function("Init", &Init);
function("ListGMs", &ListGMs);
function("LoadKnownDigest", &LoadKnownDigest);
function("_LoadResource", &LoadResource);
function("MakeGrContext", &MakeGrContext);
function("RunGM", &RunGM);
class_<GrDirectContext>("GrDirectContext")
.smart_ptr<sk_sp<GrDirectContext>>("sk_sp<GrDirectContext>");
}