| /* |
| * 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/SkSurface.h" |
| #include "include/utils/SkBase64.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> |
| #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. |
| */ |
| void 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; |
| if (memcmp(data, kMultiMagic, magicsize) == 0) { |
| SkDebugf("Try reading as a multi-frame skp\n"); |
| loadMultiFrame(&stream); |
| } else { |
| SkDebugf("Try reading as single-frame skp\n"); |
| frames.push_back(loadSingleFrame(&stream)); |
| } |
| } |
| |
| /* 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 |
| } |
| // 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<SkSurface> 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<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 |
| |
| 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<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; |
| 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()); |
| } |
| |
| // 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; |
| } |
| |
| 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<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. |
| debugCanvas->drawPicture(picture); |
| return debugCanvas; |
| } |
| |
| void 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) { |
| SkDebugf("Not a MultiPictureDocument"); |
| return; |
| } |
| SkDebugf("Expecting %d frames\n", page_count); |
| |
| std::vector<SkDocumentPage> pages(page_count); |
| if (!SkMultiPictureDocumentRead(stream, pages.data(), page_count, &procs)) { |
| SkDebugf("Reading frames from MultiPictureDocument failed"); |
| return; |
| } |
| |
| 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. |
| 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); |
| } |
| |
| // 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); |
| 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<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(grContext.get(), target, |
| kBottomLeft_GrSurfaceOrigin, |
| 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(), |
| SkBudgeted::kYes, |
| info, 0, |
| kBottomLeft_GrSurfaceOrigin, |
| nullptr, true)); |
| return surface; |
| } |
| |
| sk_sp<SkSurface> MakeRenderTarget(sk_sp<GrContext> grContext, SimpleImageInfo sii) { |
| sk_sp<SkSurface> surface(SkSurface::MakeRenderTarget(grContext.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>("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("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 |
| value_object<SkIRect>("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<T> |
| // https://emscripten.org/docs/api_reference/bind.h.html#_CPPv415register_vectorPKc |
| register_vector<DebugLayerManager::LayerSummary>("VectorLayerSummary"); |
| value_object<DebugLayerManager::LayerSummary>("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_<SkColorType>("ColorType") |
| .value("RGBA_8888", SkColorType::kRGBA_8888_SkColorType); |
| enum_<SkAlphaType>("AlphaType") |
| .value("Opaque", SkAlphaType::kOpaque_SkAlphaType) |
| .value("Premul", SkAlphaType::kPremul_SkAlphaType) |
| .value("Unpremul", SkAlphaType::kUnpremul_SkAlphaType); |
| value_object<SimpleImageInfo>("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<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()); |
| class_<SkSurface>("SkSurface") |
| .smart_ptr<sk_sp<SkSurface>>("sk_sp<SkSurface>") |
| .function("width", &SkSurface::width) |
| .function("height", &SkSurface::height) |
| .function("_flush", select_overload<void()>(&SkSurface::flushAndSubmit)) |
| .function("getCanvas", &SkSurface::getCanvas, allow_raw_pointers()); |
| class_<SkCanvas>("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_<GrContext>("GrContext") |
| .smart_ptr<sk_sp<GrContext>>("sk_sp<GrContext>"); |
| 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); |
| #endif |
| } |