[canvaskit] POC bindings for testing gms

In this CL, I forked compile.sh and created a new gm_bindings.cpp.
I also moved viewer.html into wasm_tools and created a gmtests.html
for testing out the bindings locally.

Right now there is only one gm file compiled in. I plan in a followup
CL to have some way to generate the list of cpp files that need to
be compiled in from gms.gni. I was unable to get it to work with
simply linking the lib_gm.gni, probably due to the same issue with
Registry that csmartdalton@ ran into when adding viewer.html
and the associated bindings.

Suggested reviewing order:
 - gmtests.html to get a sense of how the test flow works.
 - gm_bindings.cpp to make sure I setup the contexts/GMs correctly.
 - compile_gm.sh to see how the gms are compiled in.
 - The remaining files in any order.

When I tested this locally, the bleed_downscale digest was
exactly the same (pixel for pixel, byte for byte) as a known
digest in Gold, so I'm fairly confident in how things work.

Change-Id: I2babef848ca60f7db74e4adf27b8952a66bdeee1
Bug: skia:10812
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/322956
Reviewed-by: Chris Dalton <csmartdalton@google.com>
Commit-Queue: Kevin Lubick <kjlubick@google.com>
This commit is contained in:
Kevin Lubick 2020-10-08 10:05:07 -04:00 committed by Skia Commit-Bot
parent 7868692b9d
commit 0039874105
9 changed files with 538 additions and 12 deletions

View File

@ -1971,6 +1971,17 @@ if (skia_enable_tools) {
"modules/skshaper",
]
}
test_lib("hash_and_encode") {
sources = [
"tools/HashAndEncode.cpp",
"tools/HashAndEncode.h",
]
deps = [
":flags",
":skia",
"//third_party/libpng",
]
}
if (target_cpu != "wasm") {
test_app("convert-to-nia") {
sources = [ "tools/convert-to-nia.cpp" ]
@ -1983,17 +1994,6 @@ if (skia_enable_tools) {
":skia",
]
}
test_lib("hash_and_encode") {
sources = [
"tools/HashAndEncode.cpp",
"tools/HashAndEncode.h",
]
deps = [
":flags",
":skia",
"//third_party/libpng",
]
}
test_app("fm") {
sources = [
"dm/DMGpuTestProcs.cpp", # blech

View File

@ -13,3 +13,12 @@ component("viewer_wasm") {
]
deps = [ "../..:samples" ]
}
component("gm_wasm") {
testonly = true
include_dirs = [ "../.." ]
deps = [
"../..:hash_and_encode",
"../..:tool_utils",
]
}

View File

@ -76,6 +76,12 @@ npm:
cp ../../out/canvaskit_wasm/canvaskit.js ./canvaskit/bin/core
cp ../../out/canvaskit_wasm/canvaskit.wasm ./canvaskit/bin/core
gm_tests:
./compile_gm.sh
mkdir -p ./out
cp ../../out/wasm_gm_tests/wasm_gm_tests.js ./out
cp ../../out/wasm_gm_tests/wasm_gm_tests.wasm ./out
local-example:
rm -rf node_modules/canvaskit
mkdir -p node_modules

188
modules/canvaskit/compile_gm.sh Executable file
View File

@ -0,0 +1,188 @@
#!/bin/bash
# Copyright 2020 Google LLC
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
set -ex
BASE_DIR=`cd $(dirname ${BASH_SOURCE[0]}) && pwd`
# This expects the environment variable EMSDK to be set
if [[ ! -d $EMSDK ]]; then
cat >&2 << "EOF"
Be sure to set the EMSDK environment variable to the location of Emscripten SDK:
https://emscripten.org/docs/getting_started/downloads.html
EOF
exit 1
fi
# Navigate to SKIA_HOME from where this file is located.
pushd $BASE_DIR/../..
source $EMSDK/emsdk_env.sh
EMCC=`which emcc`
EMCXX=`which em++`
EMAR=`which emar`
RELEASE_CONF="-O3 -DSK_RELEASE --pre-js $BASE_DIR/release.js \
-DGR_GL_CHECK_ALLOC_WITH_GET_ERROR=0 -DGR_TEST_UTILS"
EXTRA_CFLAGS="\"-DSK_RELEASE\", \"-DGR_GL_CHECK_ALLOC_WITH_GET_ERROR=0\", \"-DGR_TEST_UTILS\", "
IS_OFFICIAL_BUILD="false"
BUILD_DIR=${BUILD_DIR:="out/wasm_gm_tests"}
mkdir -p $BUILD_DIR
# sometimes the .a files keep old symbols around - cleaning them out makes sure
# we get a fresh build.
rm -f $BUILD_DIR/*.a
GN_GPU="skia_enable_gpu=true skia_gl_standard = \"webgl\""
GN_GPU_FLAGS="\"-DSK_DISABLE_LEGACY_SHADERCONTEXT\","
WASM_GPU="-lGL -DSK_SUPPORT_GPU=1 -DSK_GL \
-DSK_DISABLE_LEGACY_SHADERCONTEXT --pre-js $BASE_DIR/cpu.js --pre-js $BASE_DIR/gpu.js\
-s USE_WEBGL2=1"
GM_LIB="$BUILD_DIR/libgm_wasm.a"
GN_FONT="skia_enable_fontmgr_custom_directory=false "
BUILTIN_FONT="$BASE_DIR/fonts/NotoMono-Regular.ttf.cpp"
# Generate the font's binary file (which is covered by .gitignore)
python tools/embed_resources.py \
--name SK_EMBEDDED_FONTS \
--input $BASE_DIR/fonts/NotoMono-Regular.ttf \
--output $BASE_DIR/fonts/NotoMono-Regular.ttf.cpp \
--align 4
GN_FONT+="skia_enable_fontmgr_custom_embedded=true skia_enable_fontmgr_custom_empty=false"
GN_SHAPER="skia_use_icu=true skia_use_system_icu=false skia_use_harfbuzz=true skia_use_system_harfbuzz=false"
#SHAPER_LIB="$BUILD_DIR/libharfbuzz.a $BUILD_DIR/libicu.a"
SHAPER_LIB=""
DO_DECODE="true"
ENCODE_PNG="true"
ENCODE_JPEG="false"
ENCODE_WEBP="false"
# Turn off exiting while we check for ninja (which may not be on PATH)
set +e
NINJA=`which ninja`
if [[ -z $NINJA ]]; then
git clone "https://chromium.googlesource.com/chromium/tools/depot_tools.git" --depth 1 $BUILD_DIR/depot_tools
NINJA=$BUILD_DIR/depot_tools/ninja
fi
# Re-enable error checking
set -e
./bin/fetch-gn
echo "Compiling bitcode"
# With emsdk 2.0.0 we get a false positive on tautological-value-range-compare. This appears to be
# fixed in the emsdk 2.0.4 toolchain. Disable the warning while we maintain support for 2.0.0.
EXTRA_CFLAGS+="\"-Wno-tautological-value-range-compare\","
# Inspired by https://github.com/Zubnix/skia-wasm-port/blob/master/build_bindings.sh
./bin/gn gen ${BUILD_DIR} \
--args="cc=\"${EMCC}\" \
cxx=\"${EMCXX}\" \
ar=\"${EMAR}\" \
extra_cflags_cc=[\"-frtti\"] \
extra_cflags=[\"-s\", \"WARN_UNALIGNED=1\", \"-s\", \"MAIN_MODULE=1\",
\"-DSKNX_NO_SIMD\", \"-DSK_DISABLE_AAA\",
\"-DSK_FORCE_8_BYTE_ALIGNMENT\",
${GN_GPU_FLAGS}
${EXTRA_CFLAGS}
] \
is_debug=false \
is_official_build=${IS_OFFICIAL_BUILD} \
is_component_build=false \
werror=true \
target_cpu=\"wasm\" \
\
skia_use_angle=false \
skia_use_dng_sdk=false \
skia_use_webgl=true \
skia_use_fontconfig=false \
skia_use_freetype=true \
skia_use_libheif=false \
skia_use_libjpeg_turbo_decode=${DO_DECODE} \
skia_use_libjpeg_turbo_encode=${ENCODE_JPEG} \
skia_use_libpng_decode=${DO_DECODE} \
skia_use_libpng_encode=${ENCODE_PNG} \
skia_use_libwebp_decode=${DO_DECODE} \
skia_use_libwebp_encode=${ENCODE_WEBP} \
skia_use_lua=false \
skia_use_piex=false \
skia_use_system_freetype2=false \
skia_use_system_libjpeg_turbo=false \
skia_use_system_libpng=false \
skia_use_system_libwebp=false \
skia_use_system_zlib=false\
skia_use_vulkan=false \
skia_use_wuffs=true \
skia_use_zlib=true \
\
${GN_SHAPER} \
${GN_GPU} \
${GN_FONT} \
skia_use_expat=false \
skia_enable_ccpr=false \
\
skia_enable_skshaper=true \
skia_enable_nvpr=false \
skia_enable_skparagraph=true \
skia_enable_pdf=false"
# Build all the libs we will need below
parse_targets() {
for LIBPATH in $@; do
basename $LIBPATH
done
}
${NINJA} -C ${BUILD_DIR} libskia.a libskshaper.a \
$(parse_targets $SHAPER_LIB $GM_LIB)
echo "Generating final wasm"
# Disable '-s STRICT=1' outside of Linux until
# https://github.com/emscripten-core/emscripten/issues/12118 is resovled.
STRICTNESS="-s STRICT=1"
if [[ `uname` != "Linux" ]]; then
echo "Disabling '-s STRICT=1'. See: https://github.com/emscripten-core/emscripten/issues/12118"
STRICTNESS=""
fi
# Emscripten prefers that the .a files go last in order, otherwise, it
# may drop symbols that it incorrectly thinks aren't used. One day,
# Emscripten will use LLD, which may relax this requirement.
EMCC_DEBUG=1 ${EMCXX} \
$RELEASE_CONF \
-I. \
-DSK_DISABLE_AAA \
-DSK_FORCE_8_BYTE_ALIGNMENT \
$WASM_GPU \
-std=c++17 \
--bind \
--no-entry \
--pre-js $BASE_DIR/gm.js \
$BASE_DIR/gm_bindings.cpp \
gm/bleed.cpp \
gm/gm.cpp \
$GM_LIB \
$BUILD_DIR/libskshaper.a \
$SHAPER_LIB \
$BUILD_DIR/libskia.a \
$BUILTIN_FONT \
-s LLD_REPORT_UNDEFINED \
-s ALLOW_MEMORY_GROWTH=1 \
-s EXPORT_NAME="InitWasmGMTests" \
-s FORCE_FILESYSTEM=0 \
-s FILESYSTEM=0 \
-s MODULARIZE=1 \
-s NO_EXIT_RUNTIME=1 \
-s INITIAL_MEMORY=128MB \
-s WASM=1 \
$STRICTNESS \
-o $BUILD_DIR/wasm_gm_tests.js

37
modules/canvaskit/gm.js Normal file
View File

@ -0,0 +1,37 @@
// When this file is loaded in, the high level object is "Module";
var WasmGMTests = Module;
WasmGMTests.onRuntimeInitialized = function() {
WasmGMTests.GetWebGLContext = function(canvas, webGLVersion) {
if (!canvas) {
throw 'null canvas passed into makeWebGLContext';
}
if (webGLVersion !== 1 && webGLVersion !== 2 ) {
throw 'invalid webGLVersion';
}
var contextAttributes = {
'alpha': 1,
'depth': 0, // can be 0 because off-screen.
'stencil': 0, // can be 0 because off-screen.
'antialias': 0,
'premultipliedAlpha': 1,
'preserveDrawingBuffer': 0,
'preferLowPowerToHighPerformance': 0,
'failIfMajorPerformanceCaveat': 0,
'enableExtensionsByDefault': 1,
'explicitSwapControl': 0,
'renderViaOffscreenBackBuffer': 0,
'majorVersion': webGLVersion,
};
// Creates a WebGL context and sets it to be the current context.
// These functions are defined in emscripten's library_webgl.js
var handle = GL.createContext(canvas, contextAttributes);
if (!handle) {
return 0;
}
GL.makeContextCurrent(handle);
return handle;
};
}

View File

@ -0,0 +1,173 @@
/*
* 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 <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/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 "src/core/SkMD5.h"
#include "tools/HashAndEncode.h"
#include "tools/flags/CommandLineFlags.h"
#include "modules/canvaskit/WasmCommon.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;
}
/**
* 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]);
}
// We do not need to include the keys because they are optional - they are not read by Gold.
CommandLineFlags::StringArray empty;
SkDynamicMemoryWStream stream;
// TODO(kjlubick) make emission of PNGs optional and make it so we can check the hash against
// the list of known digests to not emit it. This will hopefully speed tests up.
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);
result.set("hash", md5.c_str());
return result;
}
EMSCRIPTEN_BINDINGS(GMs) {
function("ListGMs", &ListGMs);
function("MakeGrContext", &MakeGrContext);
function("RunGM", &RunGM);
class_<GrDirectContext>("GrDirectContext")
.smart_ptr<sk_sp<GrDirectContext>>("sk_sp<GrDirectContext>");
}

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<title>GMs and unit tests against WASM/WebGL</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html, body {
margin: 0;
padding: 0;
}
#debug_canvas {
/* Same checkboard pattern as is on debugger.skia.org, just a little darker. */
background-position: 0 0, 10px 10px;
background-size: 20px 20px;
background-image: linear-gradient(45deg, #CCC 25%, transparent 25%, transparent 75%, #CCC 75%, #CCC 100%),
linear-gradient(45deg, #CCC 25%, white 25%, white 75%, #CCC 75%, #CCC 100%);
}
</style>
<canvas id=debug_canvas height=1000 width=1000></canvas>
<canvas id=gm_canvas></canvas>
<script type="text/javascript" src="/out/wasm_gm_tests.js"></script>
<script type="text/javascript" charset="utf-8">
const loadTests = InitWasmGMTests({
locateFile: (file) => '/out/'+file,
});
Promise.all([loadTests]).then(([GM]) => {
RunGMs(GM);
});
function RunGMs(GM) {
const canvas = document.getElementById('gm_canvas');
const ctx = GM.GetWebGLContext(canvas, 2);
const grcontext = GM.MakeGrContext(ctx);
requestAnimationFrame(drawQueuedPNGs);
const names = GM.ListGMs();
names.sort();
for (const name of names) {
const pngAndHash = GM.RunGM(grcontext, name);
if (!pngAndHash) {
continue;
}
drawDebugPNG(pngAndHash.png);
// We need to know the digest of the image as well as which gm produced it.
// As such, we include both parts in the name.
outputPNG(pngAndHash.png, pngAndHash.hash + '_' + name + '.png');
}
grcontext.delete();
}
const msPerGM = 500;
let timeSinceLastPNGSwapped = 0;
const queuedDebugPNGs = [];
// This decodes the given PNG and queues it up to be drawn. Because decoding the image
// (createImageBitmap) is asynchronous, we queue this in a list and have a drawing loop that
// occasionally pulls the next image off the queue to be displayed to the human. That way we
// have a minimum amount of time an image is seen so the human can casually inspect the outputs
// as they are generated.
function drawDebugPNG(pngBytes) {
const blob = new Blob([pngBytes], {type: 'image/png'});
createImageBitmap(blob).then((bitmap) => {
queuedDebugPNGs.push(bitmap);
});
}
function drawQueuedPNGs() {
requestAnimationFrame(drawQueuedPNGs);
if (!queuedDebugPNGs.length) {
return; // no new image to show
}
if ((Date.now() - timeSinceLastPNGSwapped) < msPerGM) {
return; // not been displayed long enough.
}
// Draw the first image in the queue.
const bitmap = queuedDebugPNGs.shift();
const debugCanvas = document.getElementById('debug_canvas');
debugCanvas.width = bitmap.width;
debugCanvas.height = bitmap.height;
const ctx = debugCanvas.getContext('2d');
ctx.clearRect(0, 0, 1000, 1000);
ctx.drawImage(bitmap, 0, 0);
timeSinceLastPNGSwapped = Date.now();
}
// This triggers a download of the created PNG using the provided filename. For a production
// testing environment, it will probably be good to swap this out with a webserver because it
// might not be easy to determine where the download folder for a given browser is.
function outputPNG(pngBytes, fileName) {
// https://stackoverflow.com/a/32094834
const blob = new Blob([pngBytes], {type: 'image/png'});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.href = url;
a.download = fileName;
a.click();
// clean up after because FF might not download it synchronously
setTimeout(function() {
URL.revokeObjectURL(url);
a.remove();
}, 50);
}
</script>

View File

@ -9,7 +9,7 @@
// HashAndEncode transforms any SkBitmap into a standard format, currently
// 16-bit unpremul RGBA in the Rec. 2020 color space. This lets us compare
// images from different backends or configurations, using writeForHash() for
// images from different backends or configurations, using feedHash() for
// direct content-based hashing, or encodePNG() for visual comparison.
class HashAndEncode {
public: