From 22647d0e84ec63b76b9d26153c59d9338b761107 Mon Sep 17 00:00:00 2001 From: Kevin Lubick Date: Fri, 6 Jul 2018 14:31:23 -0400 Subject: Adventures with Skia, WASM and a JS API for Pathkit See shell.html::entrypoint() for the JS side of things. See wasm_main.cpp for the C++ side of things (EMSCRIPTEN_BINDINGS at the bottom is what glues the two parts together - in general the strings are for JS and the not strings are the C++) To build this yourself, follow the getting started instructions: https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html and download this patch. Then, update compile.sh to point at your sdk and run it (e.g. $SKIA_ROOT/experimental/wasm/compile.sh) Then navigate a browser (e.g. Chrome) to http://localhost:8000/out/wasm/pathkit.html So far, can compile with compile.sh, but not really with GN/ninja (the compilation into many object files and a link at the end seems to mess emscripten up) Bug: skia: Change-Id: If6b300e2b102469e17841265c7866f1a81094d70 Reviewed-on: https://skia-review.googlesource.com/137422 Reviewed-by: Florin Malita Reviewed-by: Mike Reed Commit-Queue: Florin Malita --- experimental/wasm/compile.sh | 72 ++++++ experimental/wasm/shell.html | 511 ++++++++++++++++++++++++++++++++++++++++ experimental/wasm/wasm_main.cpp | 373 +++++++++++++++++++++++++++++ 3 files changed, 956 insertions(+) create mode 100755 experimental/wasm/compile.sh create mode 100644 experimental/wasm/shell.html create mode 100644 experimental/wasm/wasm_main.cpp (limited to 'experimental') diff --git a/experimental/wasm/compile.sh b/experimental/wasm/compile.sh new file mode 100755 index 0000000000..d2e7098a0b --- /dev/null +++ b/experimental/wasm/compile.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Copyright 2018 Google LLC +# +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +# Run this from $SKIA_HOME, not from the directory this file is in. +# This expects the environment variable EMSDK to be set +HTML_SHELL="./experimental/wasm/shell.html" + +if [[ ! -d $EMSDK ]]; then + echo "Be sure to set the EMSDK environment variable." + exit 1 +fi + +source $EMSDK/emsdk_env.sh + +echo "Compiling" + +set -e + +mkdir -p out/wasm + +# Use -O0 for larger builds (but generally quicker) +# Use -Oz for (much slower, but smaller/faster) production builds +em++ -Oz -std=c++14 \ +-Iinclude/config \ +-Iinclude/core \ +-Iinclude/private \ +-Iinclude/pathops \ +-Iinclude/utils \ +-Isrc/core \ +--bind \ +-s WASM=1 \ +-s NO_EXIT_RUNTIME=1 \ +-s ERROR_ON_UNDEFINED_SYMBOLS=1 \ +-s ERROR_ON_MISSING_LIBRARIES=1 \ +--shell-file $HTML_SHELL \ +-o out/wasm/pathkit.html \ +experimental/wasm/wasm_main.cpp \ +src/core/SkArenaAlloc.cpp \ +src/core/SkGeometry.cpp \ +src/core/SkMallocPixelRef.cpp \ +src/core/SkMath.cpp \ +src/core/SkMatrix.cpp \ +src/core/SkPath.cpp \ +src/core/SkPathRef.cpp \ +src/core/SkPoint.cpp \ +src/core/SkRect.cpp \ +src/core/SkStream.cpp \ +src/core/SkString.cpp \ +src/core/SkStringUtils.cpp \ +src/core/SkUtils.cpp \ +src/pathops/*.cpp \ +src/ports/SkDebug_stdio.cpp \ +src/ports/SkMemory_malloc.cpp \ +src/utils/SkParse.cpp \ +src/utils/SkParsePath.cpp + +# Add the following for debugging (bloats production code size otherwise) +# list of all (most?) settings: https://github.com/kripken/emscripten/blob/incoming/src/settings.js +#-s ASSERTIONS=1 \ +#-s DEMANGLE_SUPPORT=1 \ +#-g2 + +# To build with ASM.js (instead of WASM) +# This doesn't give the same results as native c++ or wasm.... +#-s WASM=0 \ +#-s ALLOW_MEMORY_GROWTH=1 \ + +python -m SimpleHTTPServer 8000 diff --git a/experimental/wasm/shell.html b/experimental/wasm/shell.html new file mode 100644 index 0000000000..8557241096 --- /dev/null +++ b/experimental/wasm/shell.html @@ -0,0 +1,511 @@ + + + + + + Skia and WASM + + + +
+
emscripten
+
Downloading...
+
+ +
+
+ +
+ + +
+
+ Resize canvas + Lock/hide mouse pointer +     + +
+ +
+ +
+ + + {{{ SCRIPT }}} + + diff --git a/experimental/wasm/wasm_main.cpp b/experimental/wasm/wasm_main.cpp new file mode 100644 index 0000000000..4486b06f40 --- /dev/null +++ b/experimental/wasm/wasm_main.cpp @@ -0,0 +1,373 @@ +/* + * Copyright 2018 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkFloatingPoint.h" +#include "SkParsePath.h" +#include "SkPath.h" +#include "SkPathOps.h" +#include "SkString.h" + +#include +#include + +using namespace emscripten; + +static const int MOVE = 0; +static const int LINE = 1; +static const int QUAD = 2; +static const int CUBIC = 4; +static const int CLOSE = 5; + +// ================================================================================= +// Creating/Exporting Paths +// ================================================================================= + +void EMSCRIPTEN_KEEPALIVE SkPathToVerbsArgsArray(SkPath path, emscripten::val /*Array*/ verbs, + emscripten::val /*Array*/ args) { + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + switch (verb) { + case SkPath::kMove_Verb: + verbs.call("push", MOVE); + args.call("push", pts[0].x()); + args.call("push", pts[0].y()); + break; + case SkPath::kLine_Verb: + verbs.call("push", LINE); + args.call("push", pts[1].x()); + args.call("push", pts[1].y()); + break; + case SkPath::kQuad_Verb: + verbs.call("push", QUAD); + args.call("push", pts[1].x()); + args.call("push", pts[1].y()); + args.call("push", pts[2].x()); + args.call("push", pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + verbs.call("push", CUBIC); + args.call("push", pts[1].x()); + args.call("push", pts[1].y()); + args.call("push", pts[2].x()); + args.call("push", pts[2].y()); + args.call("push", pts[3].x()); + args.call("push", pts[3].y()); + break; + case SkPath::kClose_Verb: + verbs.call("push", CLOSE); + break; + case SkPath::kDone_Verb: + break; + } + } +} + +emscripten::val JSArray = emscripten::val::global("Array"); + +emscripten::val EMSCRIPTEN_KEEPALIVE SkPathToCmdArray(SkPath path) { + val cmds = JSArray.new_(); + + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + val cmd = JSArray.new_(); + switch (verb) { + case SkPath::kMove_Verb: + cmd.call("push", MOVE); + cmd.call("push", pts[0].x()); + cmd.call("push", pts[0].y()); + break; + case SkPath::kLine_Verb: + cmd.call("push", LINE); + cmd.call("push", pts[1].x()); + cmd.call("push", pts[1].y()); + break; + case SkPath::kQuad_Verb: + cmd.call("push", QUAD); + cmd.call("push", pts[1].x()); + cmd.call("push", pts[1].y()); + cmd.call("push", pts[2].x()); + cmd.call("push", pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + cmd.call("push", CUBIC); + cmd.call("push", pts[1].x()); + cmd.call("push", pts[1].y()); + cmd.call("push", pts[2].x()); + cmd.call("push", pts[2].y()); + cmd.call("push", pts[3].x()); + cmd.call("push", pts[3].y()); + break; + case SkPath::kClose_Verb: + cmd.call("push", CLOSE); + break; + case SkPath::kDone_Verb: + break; + } + cmds.call("push", cmd); + } + return cmds; +} + +// This type signature is a mess, but it's necessary. See, we can't use "bind" (EMSCRIPTEN_BINDINGS) +// and pointers to primitive types (Only bound types like SkPoint). We could if we used +// cwrap (see https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97) +// but that requires us to stick to C code and, AFAIK, doesn't allow us to return nice things like +// SkPath or SkOpBuilder. +// +// So, basically, if we are using C++ and EMSCRIPTEN_BINDINGS, we can't have primative pointers +// in our function type signatures. (this gives an error message like "Cannot call foo due to unbound +// types Pi, Pf"). But, we can just pretend they are numbers and cast them to be pointers and +// the compiler is happy. +SkPath EMSCRIPTEN_KEEPALIVE SkPathFromVerbsArgsTyped(int /* uint8_t* */ vptr, int numVerbs, + int /* float* */aptr, int numArgs) { + auto verbs = reinterpret_cast(vptr); + auto args = reinterpret_cast(aptr); + SkPath path; + int argsIndex = 0; + float x1, y1, x2, y2, x3, y3; + + // if there are not enough arguments, bail with the path we've constructed so far. + #define CHECK_NUM_ARGS(n) \ + if ((argsIndex + n) > numArgs) { \ + SkDebugf("Not enough args to match the verbs. Saw %d args\n", numArgs); \ + return path; \ + } + + for(int i = 0; i < numVerbs; i++){ + switch (verbs[i]) { + case MOVE: + CHECK_NUM_ARGS(2); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + path.moveTo(x1, y1); + break; + case LINE: + CHECK_NUM_ARGS(2); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + path.lineTo(x1, y1); + break; + case QUAD: + CHECK_NUM_ARGS(4); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + x2 = args[argsIndex++], y2 = args[argsIndex++]; + path.quadTo(x1, y1, x2, y2); + break; + case CUBIC: + CHECK_NUM_ARGS(6); + x1 = args[argsIndex++], y1 = args[argsIndex++]; + x2 = args[argsIndex++], y2 = args[argsIndex++]; + x3 = args[argsIndex++], y3 = args[argsIndex++]; + path.cubicTo(x1, y1, x2, y2, x3, y3); + break; + case CLOSE: + path.close(); + break; + default: + SkDebugf(" path: UNKNOWN VERB %d, aborting dump...\n", verbs[i]); + return path; + } + } + + #undef CHECK_NUM_ARGS + + return path; +} + +// See above comment for rational of pointer mess +SkPath EMSCRIPTEN_KEEPALIVE SkPathFromCmdTyped(int /* float* */cptr, int numCmds) { + auto cmds = reinterpret_cast(cptr); + SkPath path; + float x1, y1, x2, y2, x3, y3; + + // if there are not enough arguments, bail with the path we've constructed so far. + #define CHECK_NUM_ARGS(n) \ + if ((i + n) > numCmds) { \ + SkDebugf("Not enough args to match the verbs. Saw %d commands\n", numCmds); \ + return path; \ + } + + for(int i = 0; i < numCmds;){ + switch (sk_float_floor2int(cmds[i++])) { + case MOVE: + CHECK_NUM_ARGS(2); + x1 = cmds[i++], y1 = cmds[i++]; + path.moveTo(x1, y1); + break; + case LINE: + CHECK_NUM_ARGS(2); + x1 = cmds[i++], y1 = cmds[i++]; + path.lineTo(x1, y1); + break; + case QUAD: + CHECK_NUM_ARGS(4); + x1 = cmds[i++], y1 = cmds[i++]; + x2 = cmds[i++], y2 = cmds[i++]; + path.quadTo(x1, y1, x2, y2); + break; + case CUBIC: + CHECK_NUM_ARGS(6); + x1 = cmds[i++], y1 = cmds[i++]; + x2 = cmds[i++], y2 = cmds[i++]; + x3 = cmds[i++], y3 = cmds[i++]; + path.cubicTo(x1, y1, x2, y2, x3, y3); + break; + case CLOSE: + path.close(); + break; + default: + SkDebugf(" path: UNKNOWN command %f, aborting dump...\n", cmds[i-1]); + return path; + } + } + + #undef CHECK_NUM_ARGS + + return path; +} + +//======================================================================================== +// SVG THINGS +//======================================================================================== + +val EMSCRIPTEN_KEEPALIVE ToSVGString(SkPath path) { + SkString s; + SkParsePath::ToSVGString(path, &s); + // Wrapping it in val automatically turns it into a JS string. + // Not too sure on performance implications, but is is simpler than + // returning a raw pointer to const char * and then using + // Pointer_stringify() on the calling side. + return val(s.c_str()); +} + + +SkPath EMSCRIPTEN_KEEPALIVE FromSVGString(std::string str) { + SkPath path; + SkParsePath::FromSVGString(str.c_str(), &path); + return path; +} + +//======================================================================================== +// PATHOP THINGS +//======================================================================================== + +SkPath EMSCRIPTEN_KEEPALIVE SimplifyPath(SkPath path) { + SkPath simple; + Simplify(path, &simple); + return simple; +} + +SkPath EMSCRIPTEN_KEEPALIVE ApplyPathOp(SkPath pathOne, SkPath pathTwo, SkPathOp op) { + SkPath path; + Op(pathOne, pathTwo, op, &path); + return path; +} + +SkPath EMSCRIPTEN_KEEPALIVE ResolveBuilder(SkOpBuilder builder) { + SkPath path; + builder.resolve(&path); + return path; +} + +//======================================================================================== +// Canvas THINGS +//======================================================================================== + +emscripten::val EMSCRIPTEN_KEEPALIVE ToPath2D(SkPath path, val/* Path2D&*/ retVal) { + SkPath::Iter iter(path, false); + SkPoint pts[4]; + SkPath::Verb verb; + while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) { + switch (verb) { + case SkPath::kMove_Verb: + retVal.call("moveTo", pts[0].x(), pts[0].y()); + break; + case SkPath::kLine_Verb: + retVal.call("lineTo", pts[1].x(), pts[1].y()); + break; + case SkPath::kQuad_Verb: + retVal.call("quadraticCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y()); + break; + case SkPath::kConic_Verb: + printf("unsupported conic verb\n"); + // TODO(kjlubick): Port in the logic from SkParsePath::ToSVGString? + break; + case SkPath::kCubic_Verb: + retVal.call("bezierCurveTo", pts[1].x(), pts[1].y(), pts[2].x(), pts[2].y(), + pts[3].x(), pts[3].y()); + break; + case SkPath::kClose_Verb: + retVal.call("closePath"); + break; + case SkPath::kDone_Verb: + break; + } + } + return retVal; +} + +// Binds the classes to the JS +EMSCRIPTEN_BINDINGS(skia) { + class_("SkPath") + .constructor<>() + + .function("moveTo", + select_overload(&SkPath::moveTo)) + .function("lineTo", + select_overload(&SkPath::lineTo)) + .function("quadTo", + select_overload(&SkPath::quadTo)) + .function("cubicTo", + select_overload(&SkPath::cubicTo)) + .function("close", &SkPath::close); + // Uncomment below for debugging. + //.function("dump", select_overload(&SkPath::dump)); + + class_("SkOpBuilder") + .constructor<>() + + .function("add", &SkOpBuilder::add); + + // Without this, module._ToPath2D (yes with an underscore) + // would be exposed, but be unable to correctly handle the SkPath type. + function("ToPath2D", &ToPath2D); + function("ToSVGString", &ToSVGString); + function("FromSVGString", &FromSVGString); + + function("SkPathToVerbsArgsArray", &SkPathToVerbsArgsArray); + function("SkPathFromVerbsArgsTyped", &SkPathFromVerbsArgsTyped); + + function("SkPathFromCmdTyped", &SkPathFromCmdTyped); + function("SkPathToCmdArray", &SkPathToCmdArray); + + function("SimplifyPath", &SimplifyPath); + function("ApplyPathOp", &ApplyPathOp); + function("ResolveBuilder", &ResolveBuilder); + + enum_("PathOp") + .value("DIFFERENCE", SkPathOp::kDifference_SkPathOp) + .value("INTERSECT", SkPathOp::kIntersect_SkPathOp) + .value("UNION", SkPathOp::kUnion_SkPathOp) + .value("XOR", SkPathOp::kXOR_SkPathOp) + .value("REVERSE_DIFFERENCE", SkPathOp::kReverseDifference_SkPathOp); + + constant("MOVE_VERB", MOVE); + constant("LINE_VERB", LINE); + constant("QUAD_VERB", QUAD); + constant("CUBIC_VERB", CUBIC); + constant("CLOSE_VERB", CLOSE); +} -- cgit v1.2.3