You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
12 KiB
363 lines
12 KiB
4 months ago
|
/*
|
||
|
* 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 {
|
||
|
|
||
|
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<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;
|
||
|
}
|
||
|
|
||
|
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>");
|
||
|
}
|