110367dcfc
The basic problem was that WasmContextInfo shadowed non-virtual members of ContextInfo. the tests, which work with that type, end up calling the super class's methods which return null for directContext() The context created and stored in WasmContextInfo would have worked if it could be returned, but I've opted to go with what looks like a more canonical way to test a new backend, that is to create a platform- specific subclass of GLTestContext, and let GrContextFactory create the GL context. Bug:skia:10869 Change-Id: Ie9e1142cf2e7268242ff9aab70f76151714850a3 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/355116 Commit-Queue: Nathaniel Nifong <nifong@google.com> Reviewed-by: Brian Salomon <bsalomon@google.com> Reviewed-by: Kevin Lubick <kjlubick@google.com>
366 lines
13 KiB
C++
366 lines
13 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 {
|
|
|
|
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_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>");
|
|
}
|