[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:
parent
7868692b9d
commit
0039874105
22
BUILD.gn
22
BUILD.gn
@ -1971,6 +1971,17 @@ if (skia_enable_tools) {
|
|||||||
"modules/skshaper",
|
"modules/skshaper",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
test_lib("hash_and_encode") {
|
||||||
|
sources = [
|
||||||
|
"tools/HashAndEncode.cpp",
|
||||||
|
"tools/HashAndEncode.h",
|
||||||
|
]
|
||||||
|
deps = [
|
||||||
|
":flags",
|
||||||
|
":skia",
|
||||||
|
"//third_party/libpng",
|
||||||
|
]
|
||||||
|
}
|
||||||
if (target_cpu != "wasm") {
|
if (target_cpu != "wasm") {
|
||||||
test_app("convert-to-nia") {
|
test_app("convert-to-nia") {
|
||||||
sources = [ "tools/convert-to-nia.cpp" ]
|
sources = [ "tools/convert-to-nia.cpp" ]
|
||||||
@ -1983,17 +1994,6 @@ if (skia_enable_tools) {
|
|||||||
":skia",
|
":skia",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
test_lib("hash_and_encode") {
|
|
||||||
sources = [
|
|
||||||
"tools/HashAndEncode.cpp",
|
|
||||||
"tools/HashAndEncode.h",
|
|
||||||
]
|
|
||||||
deps = [
|
|
||||||
":flags",
|
|
||||||
":skia",
|
|
||||||
"//third_party/libpng",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
test_app("fm") {
|
test_app("fm") {
|
||||||
sources = [
|
sources = [
|
||||||
"dm/DMGpuTestProcs.cpp", # blech
|
"dm/DMGpuTestProcs.cpp", # blech
|
||||||
|
@ -13,3 +13,12 @@ component("viewer_wasm") {
|
|||||||
]
|
]
|
||||||
deps = [ "../..:samples" ]
|
deps = [ "../..:samples" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component("gm_wasm") {
|
||||||
|
testonly = true
|
||||||
|
include_dirs = [ "../.." ]
|
||||||
|
deps = [
|
||||||
|
"../..:hash_and_encode",
|
||||||
|
"../..:tool_utils",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -76,6 +76,12 @@ npm:
|
|||||||
cp ../../out/canvaskit_wasm/canvaskit.js ./canvaskit/bin/core
|
cp ../../out/canvaskit_wasm/canvaskit.js ./canvaskit/bin/core
|
||||||
cp ../../out/canvaskit_wasm/canvaskit.wasm ./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:
|
local-example:
|
||||||
rm -rf node_modules/canvaskit
|
rm -rf node_modules/canvaskit
|
||||||
mkdir -p node_modules
|
mkdir -p node_modules
|
||||||
|
188
modules/canvaskit/compile_gm.sh
Executable file
188
modules/canvaskit/compile_gm.sh
Executable 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
37
modules/canvaskit/gm.js
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
173
modules/canvaskit/gm_bindings.cpp
Normal file
173
modules/canvaskit/gm_bindings.cpp
Normal 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>");
|
||||||
|
}
|
113
modules/canvaskit/wasm_tools/gmtests.html
Normal file
113
modules/canvaskit/wasm_tools/gmtests.html
Normal 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>
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
// HashAndEncode transforms any SkBitmap into a standard format, currently
|
// HashAndEncode transforms any SkBitmap into a standard format, currently
|
||||||
// 16-bit unpremul RGBA in the Rec. 2020 color space. This lets us compare
|
// 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.
|
// direct content-based hashing, or encodePNG() for visual comparison.
|
||||||
class HashAndEncode {
|
class HashAndEncode {
|
||||||
public:
|
public:
|
||||||
|
Loading…
Reference in New Issue
Block a user