blob: 964351f57c5b5fe62c8b50a27fb7d1eac8eefd1e [file] [log] [blame]
* Copyright 2019 Google LLC
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
#include "include/core/SkPicture.h"
#include "include/core/SkString.h"
#include "include/core/SkSurface.h"
#include "include/utils/SkBase64.h"
#include "src/core/SkPicturePriv.h"
#include "src/utils/SkJSONWriter.h"
#include "src/utils/SkMultiPictureDocument.h"
#include "tools/SkSharingProc.h"
#include "tools/UrlDataManager.h"
#include "tools/debugger/DebugCanvas.h"
#include "tools/debugger/DebugLayerManager.h"
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <emscripten.h>
#include <emscripten/bind.h>
#ifdef SK_GL
#include "include/gpu/GrBackendSurface.h"
#include "include/gpu/GrContext.h"
#include "include/gpu/gl/GrGLInterface.h"
#include "include/gpu/gl/GrGLTypes.h"
#include <GL/gl.h>
#include <emscripten/html5.h>
using JSColor = int32_t;
using Uint8Array = emscripten::val;
// file signature for SkMultiPictureDocument
// TODO(nifong): make public and include from SkMultiPictureDocument.h
static constexpr char kMultiMagic[] = "Skia Multi-Picture Doc\n\n";
struct SimpleImageInfo {
int width;
int height;
SkColorType colorType;
SkAlphaType alphaType;
SkImageInfo toSkImageInfo(const SimpleImageInfo& sii) {
return SkImageInfo::Make(sii.width, sii.height, sii.colorType, sii.alphaType);
SimpleImageInfo toSimpleImageInfo(const SkImageInfo& ii) {
return (SimpleImageInfo){ii.width(), ii.height(), ii.colorType(), ii.alphaType()};
class SkpDebugPlayer {
SkpDebugPlayer() :
/* loadSkp deserializes a skp file that has been copied into the shared WASM memory.
* cptr - a pointer to the data to deserialize.
* length - length of the data in bytes.
* The caller must allocate the memory with M._malloc where M is the wasm module in javascript
* and copy the data into M.buffer at the pointer returned by malloc.
* uintptr_t is used here because emscripten will not allow binding of functions with pointers
* to primitive types. We can instead pass a number and cast it to whatever kind of
* pointer we're expecting.
* Returns an error string which is populated in the case that the file cannot be read.
std::string loadSkp(uintptr_t cptr, int length) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(cptr);
char magic[8];
// Both traditional and multi-frame skp files have a magic word
SkMemoryStream stream(data, length);
SkDebugf("make stream at %p, with %d bytes\n",data, length);
// Why -1? I think it's got to do with using a constexpr, just a guess.
const size_t magicsize = sizeof(kMultiMagic) - 1;
const bool isMulti = memcmp(data, kMultiMagic, magicsize) == 0;
if (isMulti) {
SkDebugf("Try reading as a multi-frame skp\n");
const auto& error = loadMultiFrame(&stream);
if (!error.empty()) { return error; }
} else {
SkDebugf("Try reading as single-frame skp\n");
// The unint32 after the magic string is the SKP version
memcpy(&fFileVersion, data + 8, 4);
// TODO(nifong): Rely on SkPicture's return errors once it provides some.
if (fFileVersion < SkPicturePriv::kMin_Version ||
fFileVersion > SkPicturePriv::kCurrent_Version) {
return std::string(SkStringPrintf("Skp version (%d) cannot be read by this build. Version range supported = (%d, %d)",
fFileVersion, SkPicturePriv::kMin_Version, SkPicturePriv::kCurrent_Version).c_str());
return "";
uint32_t fileVersion() { return fFileVersion; }
/* drawTo asks the debug canvas to draw from the beginning of the picture
* to the given command and flush the canvas.
void drawTo(SkSurface* surface, int32_t index) {
// Set the command within the frame or layer event being drawn.
if (fInspectedLayer >= 0) {
fLayerManager->setCommand(fInspectedLayer, fp, index);
} else {
index = constrainFrameCommand(index);
auto* canvas = surface->getCanvas();
if (fInspectedLayer >= 0) {
// when it's a layer event we're viewing, we use the layer manager to render it.
fLayerManager->drawLayerEventTo(canvas, fInspectedLayer, fp);
} else {
// otherwise, its a frame at the top level.
frames[fp]->drawTo(surface->getCanvas(), index);
// Draws to the end of the current frame.
void draw(SkSurface* surface) {
auto* canvas = surface->getCanvas();
const SkIRect getBounds() {
if (fInspectedLayer < 0) {
return fBounds;
auto summary = fLayerManager->event(fInspectedLayer, fp);
return SkIRect::MakeWH(summary.layerWidth, summary.layerHeight);
// returns the debugcanvas of the current frame, or the current draw event when inspecting
// a layer.
DebugCanvas* visibleCanvas() {
if (fInspectedLayer >=0) {
return fLayerManager->getEventDebugCanvas(fInspectedLayer, fp);
} else {
return frames[fp].get();
// The following three operations apply to every debugcanvas because they are overdraw features.
// There is only one toggle for them on the app, they are global settings.
// However, there's not a simple way to make the debugcanvases pull settings from a central
// location so we set it on all of them at once.
void setOverdrawVis(bool on) {
for (int i=0; i < frames.size(); i++) {
void setGpuOpBounds(bool on) {
for (int i=0; i < frames.size(); i++) {
void setClipVizColor(JSColor color) {
for (int i=0; i < frames.size(); i++) {
void setAndroidClipViz(bool on) {
for (int i=0; i < frames.size(); i++) {
// doesn't matter in layers
// The two operations below only apply to the current frame, because they concern the command
// list, which is unique to each frame.
void deleteCommand(int index) {
void setCommandVisibility(int index, bool visible) {
visibleCanvas()->toggleCommand(index, visible);
int getSize() const {
if (fInspectedLayer >=0) {
return fLayerManager->event(fInspectedLayer, fp).commandCount;
} else {
return frames[fp]->getSize();
int getFrameCount() const {
return frames.size();
// Return the command list in JSON representation as a string
std::string jsonCommandList(sk_sp<SkSurface> surface) {
SkDynamicMemoryWStream stream;
SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
writer.beginObject(); // root
visibleCanvas()->toJSON(writer, udm, surface->getCanvas());
writer.endObject(); // root
auto skdata = stream.detachAsData();
// Convert skdata to string_view, which accepts a length
std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
// and string_view to string, which emscripten understands.
return std::string(data_view);
// Gets the clip and matrix of the last command drawn
std::string lastCommandInfo() {
SkMatrix vm = visibleCanvas()->getCurrentMatrix();
SkIRect clip = visibleCanvas()->getCurrentClip();
SkDynamicMemoryWStream stream;
SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
writer.beginObject(); // root
DrawCommand::MakeJsonMatrix(writer, vm);
DrawCommand::MakeJsonIRect(writer, clip);
writer.endObject(); // root
auto skdata = stream.detachAsData();
// Convert skdata to string_view, which accepts a length
std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
// and string_view to string, which emscripten understands.
return std::string(data_view);
void changeFrame(int index) {
fp = index;
// Return the png file at the requested index in
// the skp file's vector of shared images. this is the set of images referred to by the
// filenames like "\\1" in DrawImage commands.
// Return type is the PNG data as a base64 encoded string with prepended URI.
std::string getImageResource(int index) {
sk_sp<SkData> pngData = fImages[index]->encodeToData();
size_t len = SkBase64::Encode(pngData->data(), pngData->size(), nullptr);
SkString dst;
SkBase64::Encode(pngData->data(), pngData->size(), dst.writable_str());
return std::string(dst.c_str());
int getImageCount() {
return fImages.size();
// Get the image info of one of the resource images.
SimpleImageInfo getImageInfo(int index) {
return toSimpleImageInfo(fImages[index]->imageInfo());
// return a list of layer draw events that happened at the beginning of this frame.
std::vector<DebugLayerManager::LayerSummary> getLayerSummaries() {
return fLayerManager->summarizeLayers(fp);
// When set to a valid layer index, causes this class to playback the layer draw event at nodeId
// on frame fp. No validation of nodeId or fp is performed, this must be valid values obtained
// from either fLayerManager.listNodesForFrame or fLayerManager.summarizeEvents
// Set to -1 to return to viewing the top level animation
void setInspectedLayer(int nodeId) {
fInspectedLayer = nodeId;
// Loads a single frame (traditional) skp file from the provided data stream and returns
// a newly allocated DebugCanvas initialized with the SkPicture that was in the file.
std::unique_ptr<DebugCanvas> loadSingleFrame(SkMemoryStream* stream) {
// note overloaded = operator that actually does a move
sk_sp<SkPicture> picture = SkPicture::MakeFromStream(stream);
if (!picture) {
SkDebugf("Unable to deserialze frame.\n");
return nullptr;
SkDebugf("Parsed SKP file.\n");
// Make debug canvas using bounds from SkPicture
fBounds = picture->cullRect().roundOut();
std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBounds);
// Only draw picture to the debug canvas once.
return debugCanvas;
std::string loadMultiFrame(SkMemoryStream* stream) {
// Attempt to deserialize with an image sharing serial proc.
auto deserialContext = std::make_unique<SkSharingDeserialContext>();
SkDeserialProcs procs;
procs.fImageProc = SkSharingDeserialContext::deserializeImage;
procs.fImageCtx = deserialContext.get();
int page_count = SkMultiPictureDocumentReadPageCount(stream);
if (!page_count) {
// MSKP's have a version separate from the SKP subpictures they contain.
return "Not a MultiPictureDocument, MultiPictureDocument file version too old, or MultiPictureDocument contained 0 frames.";
SkDebugf("Expecting %d frames\n", page_count);
std::vector<SkDocumentPage> pages(page_count);
if (!SkMultiPictureDocumentRead(stream,, page_count, &procs)) {
return "Reading frames from MultiPictureDocument failed";
fLayerManager = std::make_unique<DebugLayerManager>();
int i = 0;
for (const auto& page : pages) {
// Make debug canvas using bounds from SkPicture
fBounds = page.fPicture->cullRect().roundOut();
std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBounds);
debugCanvas->setLayerManagerAndFrame(fLayerManager.get(), i);
// Only draw picture to the debug canvas once.
if (debugCanvas->getSize() <=0 ){
SkDebugf("Skipped corrupted frame, had %d commands \n", debugCanvas->getSize());
// If you don't set these, they're undefined.
fImages = deserialContext->fImages;
return "";
// constrains the draw command index to the frame's command list length.
int constrainFrameCommand(int index) {
int cmdlen = frames[fp]->getSize();
if (index >= cmdlen) {
SkDebugf("Constrained command index (%d) within this frame's length (%d)\n",
index, cmdlen);
return cmdlen-1;
return index;
// A vector of DebugCanvas, each one initialized to a frame of the animation.
std::vector<std::unique_ptr<DebugCanvas>> frames;
// The index of the current frame (into the vector above)
int fp = 0;
// The width and height of the animation. (in practice the bounds of the last loaded frame)
SkIRect fBounds;
// SKP version of loaded file.
uint32_t fFileVersion;
// image resources from a loaded file
std::vector<sk_sp<SkImage>> fImages;
// The URLDataManager here is a cache that accepts encoded data (pngs) and puts
// numbers on them. We have our own collection of images (fImages) that was populated by the
// SkSharingDeserialContext when mskp files are loaded which it can use for IDing images
// without having to serialize them.
UrlDataManager udm;
// A structure holding the picture information needed to draw any layers used in an mskp file
// individual frames hold a pointer to it, store draw events, and request images from it.
// it is stateful and is set to the current frame at all times.
std::unique_ptr<DebugLayerManager> fLayerManager;
// The node id of a layer being inspected, if any.
// -1 means we are viewing the top level animation, not a layer.
// the exact draw event being inspected depends also on the selected frame `fp`.
int fInspectedLayer = -1;
#ifdef SK_GL
sk_sp<GrContext> MakeGrContext(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE context)
EMSCRIPTEN_RESULT r = emscripten_webgl_make_context_current(context);
if (r < 0) {
SkDebugf("failed to make webgl context current %d\n", r);
return nullptr;
// setup GrContext
auto interface = GrGLMakeNativeInterface();
// setup contexts
sk_sp<GrContext> grContext(GrContext::MakeGL(interface));
return grContext;
sk_sp<SkSurface> MakeOnScreenGLSurface(sk_sp<GrContext> grContext, int width, int height) {
glClearColor(0, 0, 0, 0);
// Wrap the frame buffer object attached to the screen in a Skia render
// target so Skia can render to it
GrGLint buffer;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &buffer);
GrGLFramebufferInfo info;
info.fFBOID = (GrGLuint) buffer;
SkColorType colorType;
info.fFormat = GL_RGBA8;
colorType = kRGBA_8888_SkColorType;
GrBackendRenderTarget target(width, height, 0, 8, info);
sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(grContext.get(), target,
colorType, nullptr, nullptr));
return surface;
sk_sp<SkSurface> MakeRenderTarget(sk_sp<GrContext> grContext, int width, int height) {
SkImageInfo info = SkImageInfo::MakeN32(width, height, SkAlphaType::kPremul_SkAlphaType);
sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(grContext.get(),
info, 0,
nullptr, true));
return surface;
sk_sp<SkSurface> MakeRenderTarget(sk_sp<GrContext> grContext, SimpleImageInfo sii) {
sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(grContext.get(),
toSkImageInfo(sii), 0,
nullptr, true));
return surface;
using namespace emscripten;
// The main class that the JavaScript in index.html uses
.function("changeFrame", &SkpDebugPlayer::changeFrame)
.function("deleteCommand", &SkpDebugPlayer::deleteCommand)
.function("draw", &SkpDebugPlayer::draw, allow_raw_pointers())
.function("drawTo", &SkpDebugPlayer::drawTo, allow_raw_pointers())
.function("fileVersion", &SkpDebugPlayer::fileVersion)
.function("getBounds", &SkpDebugPlayer::getBounds)
.function("getFrameCount", &SkpDebugPlayer::getFrameCount)
.function("getImageResource", &SkpDebugPlayer::getImageResource)
.function("getImageCount", &SkpDebugPlayer::getImageCount)
.function("getImageInfo", &SkpDebugPlayer::getImageInfo)
.function("getLayerSummaries", &SkpDebugPlayer::getLayerSummaries)
.function("getSize", &SkpDebugPlayer::getSize)
.function("jsonCommandList", &SkpDebugPlayer::jsonCommandList, allow_raw_pointers())
.function("lastCommandInfo", &SkpDebugPlayer::lastCommandInfo)
.function("loadSkp", &SkpDebugPlayer::loadSkp, allow_raw_pointers())
.function("setClipVizColor", &SkpDebugPlayer::setClipVizColor)
.function("setCommandVisibility", &SkpDebugPlayer::setCommandVisibility)
.function("setGpuOpBounds", &SkpDebugPlayer::setGpuOpBounds)
.function("setInspectedLayer", &SkpDebugPlayer::setInspectedLayer)
.function("setOverdrawVis", &SkpDebugPlayer::setOverdrawVis)
.function("setAndroidClipViz", &SkpDebugPlayer::setAndroidClipViz);
// Structs used as arguments or returns to the functions above
.field("fLeft", &SkIRect::fLeft)
.field("fTop", &SkIRect::fTop)
.field("fRight", &SkIRect::fRight)
.field("fBottom", &SkIRect::fBottom);
// emscripten provided the following convenience function for binding vector<T>
.field("nodeId", &DebugLayerManager::LayerSummary::nodeId)
.field("frameOfLastUpdate", &DebugLayerManager::LayerSummary::frameOfLastUpdate)
.field("fullRedraw", &DebugLayerManager::LayerSummary::fullRedraw)
.field("layerWidth", &DebugLayerManager::LayerSummary::layerWidth)
.field("layerHeight", &DebugLayerManager::LayerSummary::layerHeight);
// Symbols needed by cpu.js to perform surface creation and flushing.
.value("RGBA_8888", SkColorType::kRGBA_8888_SkColorType);
.value("Opaque", SkAlphaType::kOpaque_SkAlphaType)
.value("Premul", SkAlphaType::kPremul_SkAlphaType)
.value("Unpremul", SkAlphaType::kUnpremul_SkAlphaType);
.field("width", &SimpleImageInfo::width)
.field("height", &SimpleImageInfo::height)
.field("colorType", &SimpleImageInfo::colorType)
.field("alphaType", &SimpleImageInfo::alphaType);
function("_getRasterDirectSurface", optional_override([](const SimpleImageInfo ii,
uintptr_t /* uint8_t* */ pPtr,
size_t rowBytes)->sk_sp<SkSurface> {
uint8_t* pixels = reinterpret_cast<uint8_t*>(pPtr);
SkImageInfo imageInfo = toSkImageInfo(ii);
SkDebugf("Made raster direct surface.\n");
return SkSurface::MakeRasterDirect(imageInfo, pixels, rowBytes, nullptr);
}), allow_raw_pointers());
.function("width", &SkSurface::width)
.function("height", &SkSurface::height)
.function("_flush", select_overload<void()>(&SkSurface::flushAndSubmit))
.function("getCanvas", &SkSurface::getCanvas, allow_raw_pointers());
.function("clear", optional_override([](SkCanvas& self, JSColor color)->void {
// JS side gives us a signed int instead of an unsigned int for color
// Add a optional_override to change it out.
#ifdef SK_GL
function("currentContext", &emscripten_webgl_get_current_context);
function("setCurrentContext", &emscripten_webgl_make_context_current);
function("MakeGrContext", &MakeGrContext);
function("MakeOnScreenGLSurface", &MakeOnScreenGLSurface);
function("MakeRenderTarget", select_overload<sk_sp<SkSurface>(
sk_sp<GrContext>, int, int)>(&MakeRenderTarget));
function("MakeRenderTarget", select_overload<sk_sp<SkSurface>(
sk_sp<GrContext>, SimpleImageInfo)>(&MakeRenderTarget));
constant("gpu", true);