/* * This file defines SkpDebugPlayer, a class which loads a SKP or MSKP file and draws it * to an SkSurface with annotation, and detailed playback controls. It holds as many DebugCanvases * as there are frames in the file. * * It also defines emscripten bindings for SkpDebugPlayer and other classes necessary to us it. * * 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 #include #include #include #include #include #include #ifdef SK_GL #include "include/gpu/GrBackendSurface.h" #include "include/gpu/GrDirectContext.h" #include "include/gpu/gl/GrGLInterface.h" #include "include/gpu/gl/GrGLTypes.h" #include #include #endif 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 { public: SkpDebugPlayer() : udm(UrlDataManager(SkString("/data"))){} /* 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(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()); } frames.push_back(loadSingleFrame(&stream)); } 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(); canvas->clear(SK_ColorTRANSPARENT); 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); } surface->getCanvas()->flush(); } // Draws to the end of the current frame. void draw(SkSurface* surface) { auto* canvas = surface->getCanvas(); canvas->clear(SK_ColorTRANSPARENT); frames[fp]->draw(surface->getCanvas()); surface->getCanvas()->flush(); } 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++) { frames[i]->setOverdrawViz(on); } fLayerManager->setOverdrawViz(on); } void setGpuOpBounds(bool on) { for (int i=0; i < frames.size(); i++) { frames[i]->setDrawGpuOpBounds(on); } fLayerManager->setDrawGpuOpBounds(on); } void setClipVizColor(JSColor color) { for (int i=0; i < frames.size(); i++) { frames[i]->setClipVizColor(SkColor(color)); } fLayerManager->setClipVizColor(SkColor(color)); } void setAndroidClipViz(bool on) { for (int i=0; i < frames.size(); i++) { frames[i]->setAndroidClipViz(on); } // doesn't matter in layers } void setOriginVisible(bool on) { for (int i=0; i < frames.size(); i++) { frames[i]->setOriginVisible(on); } } // 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) { visibleCanvas()->deleteDrawCommandAt(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 surface) { SkDynamicMemoryWStream stream; SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast); writer.beginObject(); // root visibleCanvas()->toJSON(writer, udm, surface->getCanvas()); writer.endObject(); // root writer.flush(); auto skdata = stream.detachAsData(); // Convert skdata to string_view, which accepts a length std::string_view data_view(reinterpret_cast(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 writer.appendName("ViewMatrix"); DrawCommand::MakeJsonMatrix(writer, vm); writer.appendName("ClipRect"); DrawCommand::MakeJsonIRect(writer, clip); writer.endObject(); // root writer.flush(); auto skdata = stream.detachAsData(); // Convert skdata to string_view, which accepts a length std::string_view data_view(reinterpret_cast(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 pngData = fImages[index]->encodeToData(); size_t len = SkBase64::Encode(pngData->data(), pngData->size(), nullptr); SkString dst; dst.resize(len); SkBase64::Encode(pngData->data(), pngData->size(), dst.writable_str()); dst.prepend("data:image/png;base64,"); 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()); } // returns a JSON string representing commands where each image is referenced. std::string imageUseInfoForFrame(int framenumber) { std::map> m = frames[framenumber]->getImageIdToCommandMap(udm); SkDynamicMemoryWStream stream; SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast); writer.beginObject(); // root for (auto it = m.begin(); it != m.end(); ++it) { writer.beginArray(std::to_string(it->first).c_str()); for (const int commandId : it->second) { writer.appendU64((uint64_t)commandId); } writer.endArray(); } writer.endObject(); // root writer.flush(); auto skdata = stream.detachAsData(); std::string_view data_view(reinterpret_cast(skdata->data()), skdata->size()); return std::string(data_view); } // return a list of layer draw events that happened at the beginning of this frame. std::vector 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; } private: // 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 loadSingleFrame(SkMemoryStream* stream) { // note overloaded = operator that actually does a move sk_sp 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 = std::make_unique(fBounds); // Only draw picture to the debug canvas once. debugCanvas->drawPicture(picture); return debugCanvas; } std::string loadMultiFrame(SkMemoryStream* stream) { // Attempt to deserialize with an image sharing serial proc. auto deserialContext = std::make_unique(); 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 pages(page_count); if (!SkMultiPictureDocumentRead(stream, pages.data(), page_count, &procs)) { return "Reading frames from MultiPictureDocument failed"; } fLayerManager = std::make_unique(); int i = 0; for (const auto& page : pages) { // Make debug canvas using bounds from SkPicture fBounds = page.fPicture->cullRect().roundOut(); std::unique_ptr debugCanvas = std::make_unique(fBounds); debugCanvas->setLayerManagerAndFrame(fLayerManager.get(), i); // Only draw picture to the debug canvas once. debugCanvas->drawPicture(page.fPicture); if (debugCanvas->getSize() <=0 ){ SkDebugf("Skipped corrupted frame, had %d commands \n", debugCanvas->getSize()); continue; } // If you don't set these, they're undefined. debugCanvas->setOverdrawViz(false); debugCanvas->setDrawGpuOpBounds(false); debugCanvas->setClipVizColor(SK_ColorTRANSPARENT); debugCanvas->setAndroidClipViz(false); frames.push_back(std::move(debugCanvas)); i++; } fImages = deserialContext->fImages; udm.indexImages(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> 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> 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 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 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 interface auto interface = GrGLMakeNativeInterface(); if (!interface) { SkDebugf("failed to make GrGLMakeNativeInterface\n"); return nullptr; } // setup context return GrDirectContext::MakeGL(interface); } sk_sp MakeOnScreenGLSurface(sk_sp dContext, int width, int height) { glClearColor(0, 0, 0, 0); glClearStencil(0); glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // 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 surface(SkSurface::MakeFromBackendRenderTarget(dContext.get(), target, kBottomLeft_GrSurfaceOrigin, colorType, nullptr, nullptr)); return surface; } sk_sp MakeRenderTarget(sk_sp dContext, int width, int height) { SkImageInfo info = SkImageInfo::MakeN32(width, height, SkAlphaType::kPremul_SkAlphaType); sk_sp surface(SkSurface::MakeRenderTarget(dContext.get(), SkBudgeted::kYes, info, 0, kBottomLeft_GrSurfaceOrigin, nullptr, true)); return surface; } sk_sp MakeRenderTarget(sk_sp dContext, SimpleImageInfo sii) { sk_sp surface(SkSurface::MakeRenderTarget(dContext.get(), SkBudgeted::kYes, toSkImageInfo(sii), 0, kBottomLeft_GrSurfaceOrigin, nullptr, true)); return surface; } #endif using namespace emscripten; EMSCRIPTEN_BINDINGS(my_module) { // The main class that the JavaScript in index.html uses class_("SkpDebugPlayer") .constructor<>() .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("imageUseInfoForFrame", &SkpDebugPlayer::imageUseInfoForFrame) .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("setOriginVisible", &SkpDebugPlayer::setOriginVisible) .function("setOverdrawVis", &SkpDebugPlayer::setOverdrawVis) .function("setAndroidClipViz", &SkpDebugPlayer::setAndroidClipViz); // Structs used as arguments or returns to the functions above value_object("SkIRect") .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 // https://emscripten.org/docs/api_reference/bind.h.html#_CPPv415register_vectorPKc register_vector("VectorLayerSummary"); value_object("DebugLayerManager::LayerSummary") .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. enum_("ColorType") .value("RGBA_8888", SkColorType::kRGBA_8888_SkColorType); enum_("AlphaType") .value("Opaque", SkAlphaType::kOpaque_SkAlphaType) .value("Premul", SkAlphaType::kPremul_SkAlphaType) .value("Unpremul", SkAlphaType::kUnpremul_SkAlphaType); value_object("SkImageInfo") .field("width", &SimpleImageInfo::width) .field("height", &SimpleImageInfo::height) .field("colorType", &SimpleImageInfo::colorType) .field("alphaType", &SimpleImageInfo::alphaType); constant("TRANSPARENT", (JSColor) SK_ColorTRANSPARENT); function("_getRasterDirectSurface", optional_override([](const SimpleImageInfo ii, uintptr_t /* uint8_t* */ pPtr, size_t rowBytes)->sk_sp { uint8_t* pixels = reinterpret_cast(pPtr); SkImageInfo imageInfo = toSkImageInfo(ii); SkDebugf("Made raster direct surface.\n"); return SkSurface::MakeRasterDirect(imageInfo, pixels, rowBytes, nullptr); }), allow_raw_pointers()); class_("SkSurface") .smart_ptr>("sk_sp") .function("width", &SkSurface::width) .function("height", &SkSurface::height) .function("_flush", optional_override([](SkSurface& self) { self.flushAndSubmit(false); })) .function("getCanvas", &SkSurface::getCanvas, allow_raw_pointers()); class_("SkCanvas") .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. self.clear(SkColor(color)); })); #ifdef SK_GL class_("GrDirectContext") .smart_ptr>("sk_sp"); 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, int, int)>(&MakeRenderTarget)); function("MakeRenderTarget", select_overload( sk_sp, SimpleImageInfo)>(&MakeRenderTarget)); constant("gpu", true); #endif }