Reland "Deserialize MultiPictureDocument based SKP files (with image sharing proc) in wasm debugger."

This is a reland of 7635013ad1

Original change's description:
> Deserialize MultiPictureDocument based SKP files (with image sharing proc) in wasm debugger.
> 
> Change-Id: I73affae3cd05a2aa6ac1c75c8e049d352bbf3a85
> Bug: 9176
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/217135
> Commit-Queue: Nathaniel Nifong <nifong@google.com>
> Reviewed-by: Derek Sollenberger <djsollen@google.com>
> Reviewed-by: Kevin Lubick <kjlubick@google.com>

Bug: 9176
Change-Id: Ifef1ff45ac0013ba3015f88c7ecd75527b28b604
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/222505
Commit-Queue: Nathaniel Nifong <nifong@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
This commit is contained in:
Nathaniel Nifong 2019-06-21 11:09:19 -04:00 committed by Skia Commit-Bot
parent 87bda3dabe
commit 0426c38daa
8 changed files with 422 additions and 31 deletions

View File

@ -1057,6 +1057,7 @@ if (target_cpu == "wasm") {
public_configs = [ ":skia_public" ]
sources = [
"tools/SkSharingProc.cpp",
"tools/UrlDataManager.cpp",
"tools/debugger/DebugCanvas.cpp",
"tools/debugger/DrawCommand.cpp",
@ -1597,6 +1598,7 @@ if (skia_enable_tools) {
"tools/LsanSuppressions.cpp",
"tools/ProcStats.cpp",
"tools/Resources.cpp",
"tools/SkSharingProc.cpp",
"tools/ToolUtils.cpp",
"tools/UrlDataManager.cpp",
"tools/debugger/DebugCanvas.cpp",

View File

@ -8,6 +8,8 @@
#include "include/core/SkPicture.h"
#include "include/core/SkSurface.h"
#include "src/utils/SkJSONWriter.h"
#include "src/utils/SkMultiPictureDocument.h"
#include "tools/SkSharingProc.h"
#include "tools/UrlDataManager.h"
#include "tools/debugger/DebugCanvas.h"
@ -26,6 +28,10 @@
using JSColor = int32_t;
// 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;
@ -52,45 +58,61 @@ class SkpDebugPlayer {
* pointer we're expecting.
*/
void loadSkp(uintptr_t cptr, int length) {
const auto* data = reinterpret_cast<const uint8_t*>(cptr);
// note overloaded = operator that actually does a move
fPicture = SkPicture::MakeFromData(data, length);
if (!fPicture) {
SkDebugf("Unable to parse SKP file.\n");
return;
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));
}
SkDebugf("Parsed SKP file.\n");
// Make debug canvas using bounds from SkPicture
fBounds = fPicture->cullRect().roundOut();
fDebugCanvas.reset(new DebugCanvas(fBounds));
SkDebugf("DebugCanvas created.\n");
// Only draw picture to the debug canvas once.
fDebugCanvas->drawPicture(fPicture);
SkDebugf("Added picture with %d commands.\n", fDebugCanvas->getSize());
}
/* 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) {
fDebugCanvas->drawTo(surface->getCanvas(), index);
int cmdlen = frames[fp]->getSize();
if (cmdlen == 0) {
SkDebugf("Zero commands to execute");
return;
}
if (index >= cmdlen) {
SkDebugf("Constrained command index (%d) within this frame's length (%d)\n", index, cmdlen);
index = cmdlen-1;
}
frames[fp]->drawTo(surface->getCanvas(), index);
surface->getCanvas()->flush();
}
const SkIRect& getBounds() { return fBounds; }
void setOverdrawVis(bool on) { fDebugCanvas->setOverdrawViz(on); }
void setOverdrawVis(bool on) {
frames[fp]->setOverdrawViz(on);
}
void setGpuOpBounds(bool on) {
fDebugCanvas->setDrawGpuOpBounds(on);
frames[fp]->setDrawGpuOpBounds(on);
}
void setClipVizColor(JSColor color) {
fDebugCanvas->setClipVizColor(SkColor(color));
frames[fp]->setClipVizColor(SkColor(color));
}
void deleteCommand(int index) {
frames[fp]->deleteDrawCommandAt(index);
}
int getSize() const { return fDebugCanvas->getSize(); }
void deleteCommand(int index) { fDebugCanvas->deleteDrawCommandAt(index); }
void setCommandVisibility(int index, bool visible) {
fDebugCanvas->toggleCommand(index, visible);
frames[fp]->toggleCommand(index, visible);
}
int getSize() const {
return frames[fp]->getSize();
}
int getFrameCount() const {
return frames.size();
}
// Return the command list in JSON representation as a string
@ -101,7 +123,7 @@ class SkpDebugPlayer {
// this will be prepended to any links that are created in the json command list.
UrlDataManager udm(SkString("/"));
writer.beginObject(); // root
fDebugCanvas->toJSON(writer, udm, getSize(), surface->getCanvas());
frames[fp]->toJSON(writer, udm, getSize(), surface->getCanvas());
writer.endObject(); // root
writer.flush();
auto skdata = stream.detachAsData();
@ -113,8 +135,8 @@ class SkpDebugPlayer {
// Gets the clip and matrix of the last command drawn
std::string lastCommandInfo() {
SkMatrix vm = fDebugCanvas->getCurrentMatrix();
SkIRect clip = fDebugCanvas->getCurrentClip();
SkMatrix vm = frames[fp]->getCurrentMatrix();
SkIRect clip = frames[fp]->getCurrentClip();
SkDynamicMemoryWStream stream;
SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
@ -135,10 +157,81 @@ class SkpDebugPlayer {
return std::string(data_view);
}
void changeFrame(int index) {
fp = index;
}
private:
std::unique_ptr<DebugCanvas> fDebugCanvas;
sk_sp<SkPicture> fPicture;
SkIRect fBounds;
// 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> debugDanvas = std::make_unique<DebugCanvas>(fBounds);
SkDebugf("DebugCanvas created.\n");
// Only draw picture to the debug canvas once.
debugDanvas->drawPicture(picture);
SkDebugf("Added picture with %d commands.\n", debugDanvas->getSize());
return debugDanvas;
}
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;
}
for (const auto& page : pages) {
// Make debug canvas using bounds from SkPicture
fBounds = page.fPicture->cullRect().roundOut();
std::unique_ptr<DebugCanvas> debugDanvas = std::make_unique<DebugCanvas>(fBounds);
// Only draw picture to the debug canvas once.
debugDanvas->drawPicture(page.fPicture);
SkDebugf("Added picture with %d commands.\n", debugDanvas->getSize());
if (debugDanvas->getSize() <=0 ){
SkDebugf("Skipped corrupted frame, had %d commands \n", debugDanvas->getSize());
continue;
}
debugDanvas->setOverdrawViz(false);
debugDanvas->setDrawGpuOpBounds(false);
debugDanvas->setClipVizColor(SK_ColorTRANSPARENT);
frames.push_back(std::move(debugDanvas));
}
}
// 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;
};
#if SK_SUPPORT_GPU
@ -218,7 +311,9 @@ EMSCRIPTEN_BINDINGS(my_module) {
.function("setCommandVisibility", &SkpDebugPlayer::setCommandVisibility)
.function("setGpuOpBounds", &SkpDebugPlayer::setGpuOpBounds)
.function("jsonCommandList", &SkpDebugPlayer::jsonCommandList, allow_raw_pointers())
.function("lastCommandInfo", &SkpDebugPlayer::lastCommandInfo);
.function("lastCommandInfo", &SkpDebugPlayer::lastCommandInfo)
.function("changeFrame", &SkpDebugPlayer::changeFrame)
.function("getFrameCount", &SkpDebugPlayer::getFrameCount);
// Structs used as arguments or returns to the functions above
value_object<SkIRect>("SkIRect")

View File

@ -152,6 +152,7 @@ tests_sources = [
"$_tests/MessageBusTest.cpp",
"$_tests/MetaDataTest.cpp",
"$_tests/MipMapTest.cpp",
"$_tests/MultiSkpTest.cpp",
"$_tests/NonlinearBlendingTest.cpp",
"$_tests/OctoBoundsTest.cpp",
"$_tests/OSPathTest.cpp",

View File

@ -453,6 +453,7 @@ DM_SRCS_ALL = struct(
"experimental/svg/model/*.h",
"gm/*.cpp",
"gm/*.h",
"src/utils/SkMultiPictureDocument.cpp",
"src/xml/*.cpp",
"tests/*.cpp",
"tests/*.h",
@ -499,6 +500,7 @@ DM_SRCS_ALL = struct(
"tools/gpu/**/*.h",
"tools/random_parse_path.cpp",
"tools/random_parse_path.h",
"tools/SkSharingProc.cpp",
"tools/ToolUtils.cpp",
"tools/ToolUtils.h",
"tools/timer/*.cpp",

View File

@ -80,7 +80,8 @@ struct MultiPictureDocument final : public SkDocument {
SkCanvas* c = fPictureRecorder.beginRecording(SkRect::MakeSize(bigsize));
for (const sk_sp<SkPicture>& page : fPages) {
c->drawPicture(page);
c->drawAnnotation(SkRect::MakeEmpty(), kEndPage, nullptr);
// Annotations must include some data.
c->drawAnnotation(SkRect::MakeEmpty(), kEndPage, SkData::MakeWithCString("X"));
}
sk_sp<SkPicture> p = fPictureRecorder.finishRecordingAsPicture();
p->serialize(wStream, &fProcs);
@ -195,7 +196,8 @@ bool SkMultiPictureDocumentRead(SkStreamSeekable* stream,
// PagerCanvas::onDrawAnnotation().
picture->playback(&canvas);
if (canvas.fIndex != dstArrayCount) {
SkDEBUGF("Malformed SkMultiPictureDocument\n");
SkDEBUGF("Malformed SkMultiPictureDocument: canvas.fIndex=%d dstArrayCount=%d\n",
canvas.fIndex, dstArrayCount);
}
return true;
}

199
tests/MultiSkpTest.cpp Normal file
View File

@ -0,0 +1,199 @@
/*
* Copyright 2013 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* This test confirms that a multi skp can be serialize and deserailzied without error.
*/
#include "include/core/SkDocument.h"
#include "include/core/SkPicture.h"
#include "include/core/SkSurface.h"
#include "include/core/SkString.h"
#include "include/core/SkTextBlob.h"
#include "include/core/SkFont.h"
#include "src/core/SkRecord.h"
#include "src/core/SkRecorder.h"
#include "src/utils/SkMultiPictureDocument.h"
#include "tools/SkSharingProc.h"
#include "tests/Test.h"
namespace {
class RecordVisitor {
// An SkRecord visitor that remembers the name of the last visited command.
public:
SkString name;
explicit RecordVisitor() {}
template <typename T>
void operator()(const T& command) {
name = SkString(NameOf(command));
}
SkString lastCommandName() {
return name;
}
private:
template <typename T>
static const char* NameOf(const T&) {
#define CASE(U) case SkRecords::U##_Type: return #U;
switch (T::kType) { SK_RECORD_TYPES(CASE) }
#undef CASE
return "Unknown T";
}
};
} // namespace
// Compare record tested with record expected. Assert op sequence is the same (comparing types)
// frame_num is only used for error message.
static void compareRecords(const SkRecord& tested, const SkRecord& expected,
int frame_num, skiatest::Reporter* reporter) {
REPORTER_ASSERT(reporter, tested.count() == expected.count(),
"Found %d commands in frame %d, expected %d", tested.count(), frame_num, expected.count());
RecordVisitor rv;
for (int i = 0; i < tested.count(); i++) {
tested.visit(i, rv);
const SkString testCommandName = rv.lastCommandName();
expected.visit(i, rv);
const SkString expectedCommandName = rv.lastCommandName();
REPORTER_ASSERT(reporter, testCommandName == expectedCommandName,
"Unexpected command type '%s' in frame %d, op %d. Expected '%s'",
testCommandName.c_str(), frame_num, i, expectedCommandName.c_str());
}
}
static void draw_something(SkCanvas* canvas, int seed, sk_sp<SkImage> image) {
canvas->drawColor(SK_ColorWHITE);
SkPaint paint;
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(seed);
paint.setColor(SK_ColorRED);
SkRect rect = SkRect::MakeXYWH(50+seed, 50+seed, 4*seed, 60);
canvas->drawRect(rect, paint);
SkRRect oval;
oval.setOval(rect);
oval.offset(40, 60+seed);
paint.setColor(SK_ColorBLUE);
canvas->drawRRect(oval, paint);
paint.setColor(SK_ColorCYAN);
canvas->drawCircle(180, 50, 5*seed, paint);
rect.offset(80, 0);
paint.setColor(SK_ColorYELLOW);
canvas->drawRoundRect(rect, 10, 10, paint);
SkPath path;
path.cubicTo(768, 0, -512, 256, 256, 256);
paint.setColor(SK_ColorGREEN);
canvas->drawPath(path, paint);
canvas->drawImage(image, 128-seed, 128, &paint);
if (seed % 2 == 0) {
SkRect rect2 = SkRect::MakeXYWH(0, 0, 40, 60);
canvas->drawImageRect(image, rect2, &paint);
}
SkPaint paint2;
auto text = SkTextBlob::MakeFromString(
SkStringPrintf("Frame %d", seed).c_str(), SkFont(nullptr, 2+seed));
canvas->drawTextBlob(text.get(), 50, 25, paint2);
}
// Test serialization and deserialization of multi skp.
DEF_TEST(Serialize_and_deserialize_multi_skp, reporter) {
// Create the stream we will serialize into.
SkDynamicMemoryWStream stream;
// Create the image sharing proc.
SkSharingSerialContext ctx;
SkSerialProcs procs;
procs.fImageProc = SkSharingSerialContext::serializeImage;
procs.fImageCtx = &ctx;
// Create the mulit picture document used for recording frames.
sk_sp<SkDocument> multipic = SkMakeMultiPictureDocument(&stream, &procs);
static const int NUM_FRAMES = 12;
static const int WIDTH = 256;
static const int HEIGHT = 256;
// Make an image to be used in a later step.
auto surface(SkSurface::MakeRasterN32Premul(100, 100));
surface->getCanvas()->clear(SK_ColorGREEN);
sk_sp<SkImage> image(surface->makeImageSnapshot());
REPORTER_ASSERT(reporter, image);
// Create frames, recording them to multipic.
SkRecord expectedRecords[NUM_FRAMES];
for (int i=0; i<NUM_FRAMES; i++) {
SkCanvas* pictureCanvas = multipic->beginPage(WIDTH, HEIGHT);
draw_something(pictureCanvas, i, image);
multipic->endPage();
// Also record the same commands to separate SkRecords for later comparison
SkRecorder canvas(&expectedRecords[i], WIDTH, HEIGHT);
draw_something(&canvas, i, image);
}
// Finalize
multipic->close();
// Confirm written data is at least as large as the magic word
std::unique_ptr<SkStreamAsset> writtenStream = stream.detachAsStream();
REPORTER_ASSERT(reporter, writtenStream->getLength() > 24,
"Written data length too short (%d)", writtenStream->getLength());
SkDebugf("Multi Frame file size = %d\n", writtenStream->getLength());
// Set up deserialization
SkSharingDeserialContext deserialContext;
SkDeserialProcs dprocs;
dprocs.fImageProc = SkSharingDeserialContext::deserializeImage;
dprocs.fImageCtx = &deserialContext;
// Confirm data is a MultiPictureDocument
int frame_count = SkMultiPictureDocumentReadPageCount(writtenStream.get());
REPORTER_ASSERT(reporter, frame_count == NUM_FRAMES,
"Expected %d frames, got %d. \n 0 frames may indicate the written file was not a "
"MultiPictureDocument.", NUM_FRAMES, frame_count);
// Deserailize
std::vector<SkDocumentPage> frames(frame_count);
REPORTER_ASSERT(reporter,
SkMultiPictureDocumentRead(writtenStream.get(), frames.data(), frame_count, &dprocs),
"Failed while reading MultiPictureDocument");
// Examine each frame.
SkRecorder resultRecorder(nullptr, 1, 1);
int i=0;
for (const auto& frame : frames) {
SkRect bounds = frame.fPicture->cullRect();
REPORTER_ASSERT(reporter, bounds.width() == WIDTH,
"Page width: expected (%d) got (%d)", WIDTH, bounds.width());
REPORTER_ASSERT(reporter, bounds.height() == HEIGHT,
"Page height: expected (%d) got (%d)", HEIGHT, bounds.height());
// confirm contents of picture match what we drew.
// There are several ways of doing this, an ideal comparison would not break in the same
// way at the same time as the code under test (no serialization), and would involve only
// minimal transformation of frame.fPicture, minimizing the chance that a detected fault lies
// in the test itself. The comparions also would not be an overly sensitive change detector,
// so that it doesn't break every time someone submits code (no golden file)
// Extract the SkRecord from the deserialized picture using playback (instead of a mess of
// friend classes to grab the private record inside frame.fPicture
SkRecord record;
// This picture mode is necessary so that we record the command contents of frame.fPicture
// not just a 'DrawPicture' command.
resultRecorder.reset(&record, bounds, SkRecorder::Playback_DrawPictureMode, nullptr);
frame.fPicture->playback(&resultRecorder);
// Compare the record to the expected one
compareRecords(record, expectedRecords[i], i, reporter);
i++;
}
}

48
tools/SkSharingProc.cpp Normal file
View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "tools/SkSharingProc.h"
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkSerialProcs.h"
sk_sp<SkData> SkSharingSerialContext::serializeImage(SkImage* img, void* ctx) {
SkSharingSerialContext* context = reinterpret_cast<SkSharingSerialContext*>(ctx);
uint32_t id = img->uniqueID(); // get this process's id for the image. these are not hashes.
// find out if we have already serialized this, and if so, what its in-file id is.
auto iter = context->fImageMap.find(id);
if (iter == context->fImageMap.end()) {
// When not present, add its id to the map and return its usual serialized form.
context->fImageMap[id] = context->fImageMap.size();
return img->encodeToData();
}
uint32_t fid = context->fImageMap[id];
// if present, return only the in-file id we registered the first time we serialized it.
return SkData::MakeWithCopy(&fid, sizeof(fid));
}
sk_sp<SkImage> SkSharingDeserialContext::deserializeImage(
const void* data, size_t length, void* ctx) {
SkSharingDeserialContext* context = reinterpret_cast<SkSharingDeserialContext*>(ctx);
uint32_t fid;
// If the data is an image fid, look up an already deserialized image from our map
if (length == sizeof(fid)) {
memcpy(&fid, data, sizeof(fid));
if (fid >= context->fImages.size()) {
SkDebugf("We do not have the data for image %d.\n", fid);
return nullptr;
}
return context->fImages[fid];
}
// Otherwise, the data is an image, deserialise it, store it in our map at its fid.
// TODO(nifong): make DeserialProcs accept sk_sp<SkData> so we don't have to copy this.
sk_sp<SkData> dataView = SkData::MakeWithCopy(data, length);
const sk_sp<SkImage> image = SkImage::MakeFromEncoded(std::move(dataView));
context->fImages.push_back(image);
return image;
}

42
tools/SkSharingProc.h Normal file
View File

@ -0,0 +1,42 @@
/*
* Copyright 2019 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef SkSharingProc_DEFINED
#define SkSharingProc_DEFINED
#include <unordered_map>
#include <vector>
#include "include/core/SkData.h"
#include "include/core/SkImage.h"
#include "include/core/SkSerialProcs.h"
struct SkSharingSerialContext {
// A map from the ids from SkImage::uniqueID() to ids used within the file
std::unordered_map<uint32_t, uint32_t> fImageMap;
// A serial proc that shares images between subpictures
// To use this, create an instance of SkSerialProcs and populate it this way.
// The client must retain ownership of the context.
// auto ctx = std::make_unique<SkSharingSerialContext>()
// SkSerialProcs procs;
// procs.fImageProc = SkSharingSerialContext::serializeImage;
// procs.fImageCtx = ctx.get();
static sk_sp<SkData> serializeImage(SkImage* img, void* ctx);
};
struct SkSharingDeserialContext {
// a list of unique images in the order they were encountered in the file
// Subsequent occurrences of an image refer to it by it's index in this list.
std::vector<sk_sp<SkImage>> fImages;
// A deserial proc that can interpret id's in place of images as references to previous images.
// Can also deserialize a SKP where all images are inlined (it's backwards compatible)
static sk_sp<SkImage> deserializeImage(const void* data, size_t length, void* ctx);
};
#endif