* 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/codec/SkCodec.h"
#include "include/core/SkImage.h"
#include "include/core/SkPicture.h"
#include "include/core/SkString.h"
#include "include/core/SkSurface.h"
#include "include/docs/SkMultiPictureDocument.h"
#include "include/encode/SkPngEncoder.h"
#include "src/base/SkBase64.h"
#include "src/core/SkPicturePriv.h"
#include "src/utils/SkJSONWriter.h"
#include "tools/SkSharingProc.h"
#include "tools/UrlDataManager.h"
#include "tools/debugger/DebugCanvas.h"
#include "tools/debugger/DebugLayerManager.h"
#include "tools/debugger/DrawCommand.h"
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <map>
#include <emscripten.h>
#include <emscripten/bind.h>
#ifdef CK_ENABLE_WEBGL
#include "include/gpu/ganesh/GrBackendSurface.h"
#include "include/gpu/ganesh/GrDirectContext.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
#include "include/gpu/ganesh/gl/GrGLInterface.h"
#include "include/gpu/ganesh/gl/GrGLTypes.h"
#include <GL/gl.h>
#include <emscripten/html5.h>
#endif
#include "modules/canvaskit/WasmCommon.h"
static constexpr char kMultiMagic[] = "Skia Multi-Picture Doc\n\n";
uint32_t MinVersion() { return SkPicturePriv::kMin_Version; }
struct ImageInfoNoColorspace {
int width;
int height;
SkColorType colorType;
SkAlphaType alphaType;
};
ImageInfoNoColorspace toImageInfoNoColorspace(const SkImageInfo& ii) {
return (ImageInfoNoColorspace){ii.width(), ii.height(), ii.colorType(), ii.alphaType()};
}
static sk_sp<SkImage> deserializeImage(sk_sp<SkData> data, std::optional<SkAlphaType>, void*) {
std::unique_ptr<SkCodec> codec = DecodeImageData(std::move(data));
if (!codec) {
SkDebugf("Could not decode an image\n");
return nullptr;
}
sk_sp<SkImage> img = std::get<0>(codec->getImage());
if (!img) {
SkDebugf("Could not make an image from a codec\n");
return nullptr;
}
return img;
}
class SkpDebugPlayer {
public:
SkpDebugPlayer() :
udm(UrlDataManager(SkString("/data"))){}
* 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);
SkMemoryStream stream(data, length);
SkDebugf("make stream at %p, with %d bytes\n",data, length);
const bool isMulti = memcmp(data, kMultiMagic, sizeof(kMultiMagic) - 1) == 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");
std::unique_ptr<DebugCanvas> canvas = loadSingleFrame(&stream);
if (!canvas) {
return "Error loading single frame";
}
frames.push_back(std::move(canvas));
}
return "";
}
* to the given command and flush the canvas.
*/
void drawTo(SkSurface* surface, int32_t index) {
if (fInspectedLayer >= 0) {
fLayerManager->setCommand(fInspectedLayer, fp, index);
} else {
index = constrainFrameCommand(index);
}
auto* canvas = surface->getCanvas();
canvas->clear(SK_ColorTRANSPARENT);
if (fInspectedLayer >= 0) {
fLayerManager->drawLayerEventTo(surface, fInspectedLayer, fp);
} else {
frames[fp]->drawTo(surface->getCanvas(), index);
}
#ifdef CK_ENABLE_WEBGL
skgpu::ganesh::Flush(surface);
#endif
}
void draw(SkSurface* surface) {
auto* canvas = surface->getCanvas();
canvas->clear(SK_ColorTRANSPARENT);
frames[fp]->draw(surface->getCanvas());
#ifdef CK_ENABLE_WEBGL
skgpu::ganesh::Flush(surface);
#endif
}
const SkIRect getBoundsForFrame(int32_t frame) {
if (fInspectedLayer < 0) {
return fBoundsArray[frame];
}
auto summary = fLayerManager->event(fInspectedLayer, fp);
return SkIRect::MakeWH(summary.layerWidth, summary.layerHeight);
}
const SkIRect getBounds() {
return getBoundsForFrame(fp);
}
DebugCanvas* visibleCanvas() {
if (fInspectedLayer >=0) {
return fLayerManager->getEventDebugCanvas(fInspectedLayer, fp);
} else {
return frames[fp].get();
}
}
void setOverdrawVis(bool on) {
for (size_t i=0; i < frames.size(); i++) {
frames[i]->setOverdrawViz(on);
}
fLayerManager->setOverdrawViz(on);
}
void setGpuOpBounds(bool on) {
for (size_t i=0; i < frames.size(); i++) {
frames[i]->setDrawGpuOpBounds(on);
}
fLayerManager->setDrawGpuOpBounds(on);
}
void setClipVizColor(JSColor color) {
for (size_t i=0; i < frames.size(); i++) {
frames[i]->setClipVizColor(SkColor(color));
}
fLayerManager->setClipVizColor(SkColor(color));
}
void setAndroidClipViz(bool on) {
for (size_t i=0; i < frames.size(); i++) {
frames[i]->setAndroidClipViz(on);
}
}
void setOriginVisible(bool on) {
for (size_t i=0; i < frames.size(); i++) {
frames[i]->setOriginVisible(on);
}
}
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();
}
std::string jsonCommandList(sk_sp<SkSurface> surface) {
SkDynamicMemoryWStream stream;
SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
writer.beginObject();
visibleCanvas()->toJSON(writer, udm, surface->getCanvas());
writer.endObject();
writer.flush();
auto skdata = stream.detachAsData();
std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
return std::string(data_view);
}
std::string lastCommandInfo() {
SkM44 vm = visibleCanvas()->getCurrentMatrix();
SkIRect clip = visibleCanvas()->getCurrentClip();
SkDynamicMemoryWStream stream;
SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
writer.beginObject();
writer.appendName("ViewMatrix");
DrawCommand::MakeJsonMatrix44(writer, vm);
writer.appendName("ClipRect");
DrawCommand::MakeJsonIRect(writer, clip);
writer.endObject();
writer.flush();
auto skdata = stream.detachAsData();
std::string_view data_view(reinterpret_cast<const char*>(skdata->data()), skdata->size());
return std::string(data_view);
}
void changeFrame(int index) {
fp = index;
}
std::string getImageResource(int index) {
sk_sp<SkData> pngData = SkPngEncoder::Encode(nullptr, fImages[index].get(), {});
size_t len = SkBase64::EncodedSize(pngData->size());
SkString dst;
dst.resize(len);
SkBase64::Encode(pngData->data(), pngData->size(), dst.data());
dst.prepend("data:image/png;base64,");
return std::string(dst.c_str());
}
int getImageCount() {
return fImages.size();
}
ImageInfoNoColorspace getImageInfo(int index) {
return toImageInfoNoColorspace(fImages[index]->imageInfo());
}
JSObject imageUseInfo(int framenumber, int nodeid) {
JSObject result = emscripten::val::object();
DebugCanvas* debugCanvas = frames[framenumber].get();
if (nodeid >= 0) {
debugCanvas = fLayerManager->getEventDebugCanvas(nodeid, framenumber);
}
const auto& map = debugCanvas->getImageIdToCommandMap(udm);
for (auto it = map.begin(); it != map.end(); ++it) {
JSArray list = emscripten::val::array();
for (const int commandId : it->second) {
list.call<void>("push", commandId);
}
result.set(std::to_string(it->first), list);
}
return result;
}
JSArray getLayerSummariesJs() {
JSArray result = emscripten::val::array();
for (auto summary : fLayerManager->summarizeLayers(fp)) {
result.call<void>("push", summary);
}
return result;
}
JSArray getLayerKeys() {
JSArray result = emscripten::val::array();
for (auto key : fLayerManager->getKeys()) {
JSObject item = emscripten::val::object();
item.set("frame", key.frame);
item.set("nodeId", key.nodeId);
result.call<void>("push", item);
}
return result;
}
void setInspectedLayer(int nodeId) {
fInspectedLayer = nodeId;
}
int findCommandByPixel(SkSurface* surface, int x, int y, int commandIndex) {
SkColor finalColor = evaluateCommandColor(surface, commandIndex, x, y);
int lowerBound = 0;
int upperBound = commandIndex;
while (upperBound - lowerBound > 1) {
int command = (upperBound - lowerBound) / 2 + lowerBound;
auto c = evaluateCommandColor(surface, command, x, y);
if (c == finalColor) {
upperBound = command;
} else {
lowerBound = command;
}
}
drawTo(surface, commandIndex);
return upperBound;
}
private:
SkColor evaluateCommandColor(SkSurface* surface, int command, int x, int y) {
drawTo(surface, command);
SkColor c;
SkImageInfo info = SkImageInfo::Make(1, 1, kRGBA_8888_SkColorType, kOpaque_SkAlphaType);
SkPixmap pixmap(info, &c, 4);
surface->readPixels(pixmap, x, y);
return c;
}
std::unique_ptr<DebugCanvas> loadSingleFrame(SkMemoryStream* stream) {
SkDeserialProcs procs;
procs.fImageDataProc = deserializeImage;
sk_sp<SkPicture> picture = SkPicture::MakeFromStream(stream, &procs);
if (!picture) {
SkDebugf("Unable to deserialze frame.\n");
return nullptr;
}
SkDebugf("Parsed SKP file.\n");
fBoundsArray.push_back(picture->cullRect().roundOut());
std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
debugCanvas->drawPicture(picture);
return debugCanvas;
}
std::string loadMultiFrame(SkMemoryStream* stream) {
auto deserialContext = std::make_unique<SkSharingDeserialContext>();
SkDeserialProcs procs;
procs.fImageProc = SkSharingDeserialContext::deserializeImage;
procs.fImageCtx = deserialContext.get();
int page_count = SkMultiPictureDocument::ReadPageCount(stream);
if (!page_count) {
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 (!SkMultiPictureDocument::Read(stream, pages.data(), page_count, &procs)) {
return "Reading frames from MultiPictureDocument failed";
}
fLayerManager = std::make_unique<DebugLayerManager>();
int i = 0;
for (const auto& page : pages) {
fBoundsArray.push_back(page.fPicture->cullRect().roundOut());
std::unique_ptr<DebugCanvas> debugCanvas = std::make_unique<DebugCanvas>(fBoundsArray.back());
debugCanvas->setLayerManagerAndFrame(fLayerManager.get(), i);
debugCanvas->drawPicture(page.fPicture);
if (debugCanvas->getSize() <=0 ){
SkDebugf("Skipped corrupted frame, had %d commands \n", debugCanvas->getSize());
continue;
}
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 "";
}
int constrainFrameCommand(int index) {
int cmdlen = frames[fp]->getSize();
if (index >= cmdlen) {
return cmdlen-1;
}
return index;
}
std::vector<std::unique_ptr<DebugCanvas>> frames;
int fp = 0;
std::vector<SkIRect> fBoundsArray;
std::vector<sk_sp<SkImage>> fImages;
UrlDataManager udm;
std::unique_ptr<DebugLayerManager> fLayerManager;
int fInspectedLayer = -1;
};
using namespace emscripten;
EMSCRIPTEN_BINDINGS(my_module) {
function("MinVersion", &MinVersion);
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("findCommandByPixel", &SkpDebugPlayer::findCommandByPixel, allow_raw_pointers())
.function("getBounds", &SkpDebugPlayer::getBounds)
.function("getBoundsForFrame", &SkpDebugPlayer::getBoundsForFrame)
.function("getFrameCount", &SkpDebugPlayer::getFrameCount)
.function("getImageResource", &SkpDebugPlayer::getImageResource)
.function("getImageCount", &SkpDebugPlayer::getImageCount)
.function("getImageInfo", &SkpDebugPlayer::getImageInfo)
.function("getLayerKeys", &SkpDebugPlayer::getLayerKeys)
.function("getLayerSummariesJs", &SkpDebugPlayer::getLayerSummariesJs)
.function("getSize", &SkpDebugPlayer::getSize)
.function("imageUseInfo", &SkpDebugPlayer::imageUseInfo)
.function("imageUseInfoForFrameJs", optional_override([](SkpDebugPlayer& self, const int frame)->JSObject {
return self.imageUseInfo(frame, -1);
}))
.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);
value_object<SkIRect>("SkIRect")
.field("fLeft", &SkIRect::fLeft)
.field("fTop", &SkIRect::fTop)
.field("fRight", &SkIRect::fRight)
.field("fBottom", &SkIRect::fBottom);
register_vector<DebugLayerManager::LayerSummary>("VectorLayerSummary");
value_object<DebugLayerManager::LayerSummary>("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);
value_object<ImageInfoNoColorspace>("ImageInfoNoColorspace")
.field("width", &ImageInfoNoColorspace::width)
.field("height", &ImageInfoNoColorspace::height)
.field("colorType", &ImageInfoNoColorspace::colorType)
.field("alphaType", &ImageInfoNoColorspace::alphaType);
}