Commit 4a99e579 authored by Vladimir Levin's avatar Vladimir Levin Committed by Commit Bot

cc/paint: Serialize PaintShaders (mostly).

This patch adds serialization to PaintShaders, with the exception of
PaintImage and PaintRecord.

R=enne@chromium.org

Bug: 737629
Cq-Include-Trybots: master.tryserver.blink:linux_trusty_blink_rel
Change-Id: Idbaf512f2f0c12df8edd6bcd459af0ddaaff4126
Reviewed-on: https://chromium-review.googlesource.com/590675
Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
Reviewed-by: default avatarenne <enne@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491889}
parent 2c099c8f
......@@ -8,6 +8,8 @@
#include "cc/paint/decoded_draw_image.h"
#include "cc/paint/display_item_list.h"
#include "cc/paint/image_provider.h"
#include "cc/paint/paint_op_reader.h"
#include "cc/paint/paint_op_writer.h"
#include "cc/test/skia_common.h"
#include "cc/test/test_skcanvas.h"
#include "testing/gtest/include/gtest/gtest.h"
......@@ -25,49 +27,110 @@ using testing::Mock;
namespace cc {
namespace {
// An arbitrary size guaranteed to fit the size of any serialized op in this
// unit test. This can also be used for deserialized op size safely in this
// unit test suite as generally deserialized ops are smaller.
static constexpr size_t kBufferBytesPerOp = 1000 + sizeof(LargestPaintOp);
} // namespace
void ExpectFlattenableEqual(SkFlattenable* expected, SkFlattenable* actual) {
sk_sp<SkData> expected_data(SkValidatingSerializeFlattenable(expected));
sk_sp<SkData> actual_data(SkValidatingSerializeFlattenable(actual));
ASSERT_EQ(expected_data->size(), actual_data->size());
EXPECT_TRUE(expected_data->equals(actual_data.get()));
}
void ExpectPaintFlagsEqual(const PaintFlags& expected,
const PaintFlags& actual) {
// Can't just ToSkPaint and operator== here as SkPaint does pointer
// comparisons on all the ref'd skia objects on the SkPaint, which
// is not true after serialization.
EXPECT_EQ(expected.getTextSize(), actual.getTextSize());
EXPECT_EQ(expected.getColor(), actual.getColor());
EXPECT_EQ(expected.getStrokeWidth(), actual.getStrokeWidth());
EXPECT_EQ(expected.getStrokeMiter(), actual.getStrokeMiter());
EXPECT_EQ(expected.getBlendMode(), actual.getBlendMode());
EXPECT_EQ(expected.getStrokeCap(), actual.getStrokeCap());
EXPECT_EQ(expected.getStrokeJoin(), actual.getStrokeJoin());
EXPECT_EQ(expected.getStyle(), actual.getStyle());
EXPECT_EQ(expected.getTextEncoding(), actual.getTextEncoding());
EXPECT_EQ(expected.getHinting(), actual.getHinting());
EXPECT_EQ(expected.getFilterQuality(), actual.getFilterQuality());
// TODO(enne): compare typeface and shader too
ExpectFlattenableEqual(expected.getPathEffect().get(),
actual.getPathEffect().get());
ExpectFlattenableEqual(expected.getMaskFilter().get(),
actual.getMaskFilter().get());
ExpectFlattenableEqual(expected.getColorFilter().get(),
actual.getColorFilter().get());
ExpectFlattenableEqual(expected.getLooper().get(), actual.getLooper().get());
ExpectFlattenableEqual(expected.getImageFilter().get(),
actual.getImageFilter().get());
}
class PaintOpSerializationTestUtils {
public:
static void ExpectFlattenableEqual(SkFlattenable* expected,
SkFlattenable* actual) {
sk_sp<SkData> expected_data(SkValidatingSerializeFlattenable(expected));
sk_sp<SkData> actual_data(SkValidatingSerializeFlattenable(actual));
ASSERT_EQ(expected_data->size(), actual_data->size());
EXPECT_TRUE(expected_data->equals(actual_data.get()));
}
static void ExpectPaintShadersEqual(const PaintShader* one,
const PaintShader* two) {
if (!one) {
EXPECT_FALSE(two);
return;
}
} // namespace
ASSERT_TRUE(one);
ASSERT_TRUE(two);
EXPECT_EQ(one->shader_type_, two->shader_type_);
EXPECT_EQ(one->flags_, two->flags_);
EXPECT_EQ(one->end_radius_, two->end_radius_);
EXPECT_EQ(one->start_radius_, two->start_radius_);
EXPECT_EQ(one->tx_, two->tx_);
EXPECT_EQ(one->ty_, two->ty_);
EXPECT_EQ(one->fallback_color_, two->fallback_color_);
EXPECT_EQ(one->scaling_behavior_, two->scaling_behavior_);
if (one->local_matrix_) {
EXPECT_TRUE(two->local_matrix_.has_value());
EXPECT_TRUE(*one->local_matrix_ == *two->local_matrix_);
} else {
EXPECT_FALSE(two->local_matrix_.has_value());
}
EXPECT_EQ(one->center_, two->center_);
EXPECT_EQ(one->tile_, two->tile_);
EXPECT_EQ(one->start_point_, two->start_point_);
EXPECT_EQ(one->end_point_, two->end_point_);
EXPECT_THAT(one->colors_, testing::ElementsAreArray(two->colors_));
EXPECT_THAT(one->positions_, testing::ElementsAreArray(two->positions_));
}
static void ExpectPaintFlagsEqual(const PaintFlags& expected,
const PaintFlags& actual) {
// Can't just ToSkPaint and operator== here as SkPaint does pointer
// comparisons on all the ref'd skia objects on the SkPaint, which
// is not true after serialization.
EXPECT_EQ(expected.getTextSize(), actual.getTextSize());
EXPECT_EQ(expected.getColor(), actual.getColor());
EXPECT_EQ(expected.getStrokeWidth(), actual.getStrokeWidth());
EXPECT_EQ(expected.getStrokeMiter(), actual.getStrokeMiter());
EXPECT_EQ(expected.getBlendMode(), actual.getBlendMode());
EXPECT_EQ(expected.getStrokeCap(), actual.getStrokeCap());
EXPECT_EQ(expected.getStrokeJoin(), actual.getStrokeJoin());
EXPECT_EQ(expected.getStyle(), actual.getStyle());
EXPECT_EQ(expected.getTextEncoding(), actual.getTextEncoding());
EXPECT_EQ(expected.getHinting(), actual.getHinting());
EXPECT_EQ(expected.getFilterQuality(), actual.getFilterQuality());
// TODO(enne): compare typeface too
ExpectFlattenableEqual(expected.getPathEffect().get(),
actual.getPathEffect().get());
ExpectFlattenableEqual(expected.getMaskFilter().get(),
actual.getMaskFilter().get());
ExpectFlattenableEqual(expected.getColorFilter().get(),
actual.getColorFilter().get());
ExpectFlattenableEqual(expected.getLooper().get(),
actual.getLooper().get());
ExpectFlattenableEqual(expected.getImageFilter().get(),
actual.getImageFilter().get());
ExpectPaintShadersEqual(expected.getShader(), actual.getShader());
}
static void FillArbitraryShaderValues(PaintShader* shader, bool use_matrix) {
shader->shader_type_ = PaintShader::Type::kTwoPointConicalGradient;
shader->flags_ = 12345;
shader->end_radius_ = 12.3f;
shader->start_radius_ = 13.4f;
shader->tx_ = SkShader::kRepeat_TileMode;
shader->ty_ = SkShader::kMirror_TileMode;
shader->fallback_color_ = SkColorSetARGB(254, 252, 250, 248);
shader->scaling_behavior_ = PaintShader::ScalingBehavior::kRasterAtScale;
if (use_matrix) {
shader->local_matrix_.emplace(SkMatrix::I());
shader->local_matrix_->setSkewX(10);
shader->local_matrix_->setSkewY(20);
}
shader->center_ = SkPoint::Make(50, 40);
shader->tile_ = SkRect::MakeXYWH(7, 77, 777, 7777);
shader->start_point_ = SkPoint::Make(-1, -5);
shader->end_point_ = SkPoint::Make(13, -13);
// TODO(vmpstr): Add PaintImage/PaintRecord.
shader->colors_ = {SkColorSetARGB(1, 2, 3, 4), SkColorSetARGB(5, 6, 7, 8),
SkColorSetARGB(9, 0, 1, 2)};
shader->positions_ = {0.f, 0.4f, 1.f};
}
};
TEST(PaintOpBufferTest, Empty) {
PaintOpBuffer buffer;
......@@ -112,7 +175,8 @@ class PaintOpAppendTest : public ::testing::Test {
ASSERT_EQ(iter->GetType(), PaintOpType::SaveLayer);
SaveLayerOp* save_op = static_cast<SaveLayerOp*>(*iter);
EXPECT_EQ(save_op->bounds, rect_);
ExpectPaintFlagsEqual(save_op->flags, flags_);
PaintOpSerializationTestUtils::ExpectPaintFlagsEqual(save_op->flags,
flags_);
++iter;
ASSERT_EQ(iter->GetType(), PaintOpType::Save);
......@@ -1117,6 +1181,7 @@ std::vector<PaintFlags> test_flags = {
flags.setTextEncoding(PaintFlags::kGlyphID_TextEncoding);
flags.setHinting(PaintFlags::kNormal_Hinting);
flags.setFilterQuality(SkFilterQuality::kMedium_SkFilterQuality);
flags.setShader(PaintShader::MakeColor(SkColorSetARGB(1, 2, 3, 4)));
return flags;
}(),
[] {
......@@ -1154,6 +1219,38 @@ std::vector<PaintFlags> test_flags = {
flags.setImageFilter(SkOffsetImageFilter::Make(10, 11, nullptr));
sk_sp<PaintShader> shader = PaintShader::MakeColor(SK_ColorTRANSPARENT);
PaintOpSerializationTestUtils::FillArbitraryShaderValues(shader.get(),
true);
flags.setShader(std::move(shader));
return flags;
}(),
[] {
PaintFlags flags;
flags.setShader(PaintShader::MakeColor(SkColorSetARGB(12, 34, 56, 78)));
return flags;
}(),
[] {
PaintFlags flags;
sk_sp<PaintShader> shader = PaintShader::MakeColor(SK_ColorTRANSPARENT);
PaintOpSerializationTestUtils::FillArbitraryShaderValues(shader.get(),
false);
flags.setShader(std::move(shader));
return flags;
}(),
[] {
PaintFlags flags;
SkPoint points[2] = {SkPoint::Make(1, 2), SkPoint::Make(3, 4)};
SkColor colors[3] = {SkColorSetARGB(1, 2, 3, 4),
SkColorSetARGB(4, 3, 2, 1),
SkColorSetARGB(0, 10, 20, 30)};
SkScalar positions[3] = {0.f, 0.3f, 1.f};
flags.setShader(PaintShader::MakeLinearGradient(
points, colors, positions, 3, SkShader::kMirror_TileMode));
return flags;
}(),
PaintFlags(),
......@@ -1596,7 +1693,7 @@ void PushTranslateOps(PaintOpBuffer* buffer) {
}
void CompareFlags(const PaintFlags& original, const PaintFlags& written) {
ExpectPaintFlagsEqual(original, written);
PaintOpSerializationTestUtils::ExpectPaintFlagsEqual(original, written);
}
void CompareImages(const PaintImage& original, const PaintImage& written) {}
......
......@@ -7,15 +7,35 @@
#include <stddef.h>
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_shader.h"
#include "third_party/skia/include/core/SkFlattenableSerialization.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkTextBlob.h"
namespace cc {
namespace {
bool IsValidPaintShaderType(PaintShader::Type type) {
return static_cast<uint8_t>(type) <
static_cast<uint8_t>(PaintShader::Type::kShaderCount);
}
bool IsValidSkShaderTileMode(SkShader::TileMode mode) {
return mode < SkShader::kTileModeCount;
}
bool IsValidPaintShaderScalingBehavior(PaintShader::ScalingBehavior behavior) {
return behavior == PaintShader::ScalingBehavior::kRasterAtScale ||
behavior == PaintShader::ScalingBehavior::kFixedScale;
}
} // namespace
template <typename T>
void PaintOpReader::ReadSimple(T* val) {
if (!AlignMemory(alignof(T)))
valid_ = false;
if (remaining_bytes_ < sizeof(T))
valid_ = false;
if (!valid_)
......@@ -33,6 +53,8 @@ void PaintOpReader::ReadFlattenable(sk_sp<T>* val) {
ReadSimple(&bytes);
if (remaining_bytes_ < bytes)
valid_ = false;
if (!SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)))
valid_ = false;
if (!valid_)
return;
if (bytes == 0)
......@@ -123,12 +145,16 @@ void PaintOpReader::Read(PaintFlags* flags) {
ReadSimple(&flags->bitfields_uint_);
// TODO(enne): ReadTypeface, http://crbug.com/737629
// Flattenables must be read at 4-byte boundary, which should be the case
// here.
ReadFlattenable(&flags->path_effect_);
// TODO(enne): ReadPaintShader, http://crbug.com/737629
ReadFlattenable(&flags->mask_filter_);
ReadFlattenable(&flags->color_filter_);
ReadFlattenable(&flags->draw_looper_);
ReadFlattenable(&flags->image_filter_);
Read(&flags->shader_);
}
void PaintOpReader::Read(PaintImage* image) {
......@@ -161,4 +187,85 @@ void PaintOpReader::Read(sk_sp<SkTextBlob>* blob) {
// TODO(enne): implement SkTextBlob serialization: http://crbug.com/737629
}
void PaintOpReader::Read(sk_sp<PaintShader>* shader) {
bool has_shader = false;
ReadSimple(&has_shader);
if (!has_shader) {
*shader = nullptr;
return;
}
PaintShader::Type shader_type;
ReadSimple(&shader_type);
// Avoid creating a shader if something is invalid.
if (!valid_ || !IsValidPaintShaderType(shader_type)) {
valid_ = false;
return;
}
*shader = sk_sp<PaintShader>(new PaintShader(shader_type));
PaintShader& ref = **shader;
ReadSimple(&ref.flags_);
ReadSimple(&ref.end_radius_);
ReadSimple(&ref.start_radius_);
ReadSimple(&ref.tx_);
ReadSimple(&ref.ty_);
if (!IsValidSkShaderTileMode(ref.tx_) || !IsValidSkShaderTileMode(ref.ty_))
valid_ = false;
ReadSimple(&ref.fallback_color_);
ReadSimple(&ref.scaling_behavior_);
if (!IsValidPaintShaderScalingBehavior(ref.scaling_behavior_))
valid_ = false;
bool has_local_matrix = false;
ReadSimple(&has_local_matrix);
if (has_local_matrix) {
ref.local_matrix_.emplace();
ReadSimple(&*ref.local_matrix_);
}
ReadSimple(&ref.center_);
ReadSimple(&ref.tile_);
ReadSimple(&ref.start_point_);
ReadSimple(&ref.end_point_);
// TODO(vmpstr): Read PaintImage image_. http://crbug.com/737629
// TODO(vmpstr): Read sk_sp<PaintRecord> record_. http://crbug.com/737629
decltype(ref.colors_)::size_type colors_size = 0;
ReadSimple(&colors_size);
size_t colors_bytes = colors_size * sizeof(SkColor);
if (colors_bytes > remaining_bytes_) {
valid_ = false;
return;
}
ref.colors_.resize(colors_size);
ReadData(colors_bytes, ref.colors_.data());
decltype(ref.positions_)::size_type positions_size = 0;
ReadSimple(&positions_size);
size_t positions_bytes = positions_size * sizeof(SkScalar);
if (positions_bytes > remaining_bytes_) {
valid_ = false;
return;
}
ref.positions_.resize(positions_size);
ReadData(positions_size * sizeof(SkScalar), ref.positions_.data());
// We don't write the cached shader, so don't attempt to read it either.
}
bool PaintOpReader::AlignMemory(size_t alignment) {
// Due to the math below, alignment must be a power of two.
DCHECK_GT(alignment, 0u);
DCHECK_EQ(alignment & (alignment - 1), 0u);
uintptr_t memory = reinterpret_cast<uintptr_t>(memory_);
// The following is equivalent to:
// padding = (alignment - memory % alignment) % alignment;
// because alignment is a power of two. This doesn't use modulo operator
// however, since it can be slow.
size_t padding = ((memory + alignment - 1) & ~(alignment - 1)) - memory;
if (padding > remaining_bytes_)
return false;
memory_ += padding;
remaining_bytes_ -= padding;
return true;
}
} // namespace cc
......@@ -7,13 +7,16 @@
#include <vector>
#include "cc/paint/paint_export.h"
#include "cc/paint/paint_op_writer.h"
namespace cc {
class PaintShader;
// PaintOpReader takes garbage |memory| and clobbers it with successive
// read functions.
class PaintOpReader {
class CC_PAINT_EXPORT PaintOpReader {
public:
PaintOpReader(const void* memory, size_t size)
: memory_(static_cast<const char*>(memory) +
......@@ -40,6 +43,7 @@ class PaintOpReader {
void Read(PaintImage* image);
void Read(sk_sp<SkData>* data);
void Read(sk_sp<SkTextBlob>* blob);
void Read(sk_sp<PaintShader>* shader);
void Read(SkClipOp* op) {
uint8_t value = 0u;
......@@ -69,6 +73,10 @@ class PaintOpReader {
template <typename T>
void ReadFlattenable(sk_sp<T>* val);
// Attempts to align the memory to the given alignment. Returns false if there
// is unsufficient bytes remaining to do this padding.
bool AlignMemory(size_t alignment);
const char* memory_ = nullptr;
size_t remaining_bytes_ = 0u;
bool valid_ = true;
......
......@@ -5,6 +5,7 @@
#include "cc/paint/paint_op_writer.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_shader.h"
#include "third_party/skia/include/core/SkFlattenableSerialization.h"
#include "third_party/skia/include/core/SkTextBlob.h"
......@@ -13,7 +14,9 @@ namespace cc {
template <typename T>
void PaintOpWriter::WriteSimple(const T& val) {
static_assert(base::is_trivially_copyable<T>::value, "");
if (sizeof(T) > remaining_bytes_)
if (!AlignMemory(alignof(T)))
valid_ = false;
if (remaining_bytes_ < sizeof(T))
valid_ = false;
if (!valid_)
return;
......@@ -25,6 +28,8 @@ void PaintOpWriter::WriteSimple(const T& val) {
}
void PaintOpWriter::WriteFlattenable(const SkFlattenable* val) {
DCHECK(SkIsAlign4(reinterpret_cast<uintptr_t>(memory_)))
<< "Flattenable must start writing at 4 byte alignment.";
// TODO(enne): change skia API to make this a const parameter.
sk_sp<SkData> data(
SkValidatingSerializeFlattenable(const_cast<SkFlattenable*>(val)));
......@@ -79,12 +84,16 @@ void PaintOpWriter::Write(const PaintFlags& flags) {
WriteSimple(flags.bitfields_uint_);
// TODO(enne): WriteTypeface, http://crbug.com/737629
// Flattenables must be written starting at a 4 byte boundary, which should be
// the case here.
WriteFlattenable(flags.path_effect_.get());
// TODO(enne): WritePaintShader, http://crbug.com/737629
WriteFlattenable(flags.mask_filter_.get());
WriteFlattenable(flags.color_filter_.get());
WriteFlattenable(flags.draw_looper_.get());
WriteFlattenable(flags.image_filter_.get());
Write(flags.shader_.get());
}
void PaintOpWriter::Write(const PaintImage& image, ImageDecodeCache* cache) {
......@@ -107,6 +116,46 @@ void PaintOpWriter::Write(const sk_sp<SkTextBlob>& blob) {
// TODO(enne): implement SkTextBlob serialization: http://crbug.com/737629
}
void PaintOpWriter::Write(const PaintShader* shader) {
if (!shader) {
WriteSimple(false);
return;
}
// TODO(vmpstr): This could be optimized to only serialize fields relevant to
// the specific shader type. If done, then corresponding reading and tests
// would have to also be updated.
WriteSimple(true);
WriteSimple(shader->shader_type_);
WriteSimple(shader->flags_);
WriteSimple(shader->end_radius_);
WriteSimple(shader->start_radius_);
WriteSimple(shader->tx_);
WriteSimple(shader->ty_);
WriteSimple(shader->fallback_color_);
WriteSimple(shader->scaling_behavior_);
if (shader->local_matrix_) {
Write(true);
WriteSimple(*shader->local_matrix_);
} else {
Write(false);
}
WriteSimple(shader->center_);
WriteSimple(shader->tile_);
WriteSimple(shader->start_point_);
WriteSimple(shader->end_point_);
// TODO(vmpstr): Write PaintImage image_. http://crbug.com/737629
// TODO(vmpstr): Write sk_sp<PaintRecord> record_. http://crbug.com/737629
WriteSimple(shader->colors_.size());
WriteData(shader->colors_.size() * sizeof(SkColor), shader->colors_.data());
WriteSimple(shader->positions_.size());
WriteData(shader->positions_.size() * sizeof(SkScalar),
shader->positions_.data());
// Explicitly don't write the cached_shader_ because that can be regenerated
// using other fields.
}
void PaintOpWriter::WriteData(size_t bytes, const void* input) {
if (bytes > remaining_bytes_)
valid_ = false;
......@@ -125,4 +174,23 @@ void PaintOpWriter::WriteArray(size_t count, const SkPoint* input) {
WriteData(bytes, input);
}
bool PaintOpWriter::AlignMemory(size_t alignment) {
// Due to the math below, alignment must be a power of two.
DCHECK_GT(alignment, 0u);
DCHECK_EQ(alignment & (alignment - 1), 0u);
uintptr_t memory = reinterpret_cast<uintptr_t>(memory_);
// The following is equivalent to:
// padding = (alignment - memory % alignment) % alignment;
// because alignment is a power of two. This doesn't use modulo operator
// however, since it can be slow.
size_t padding = ((memory + alignment - 1) & ~(alignment - 1)) - memory;
if (padding > remaining_bytes_)
return false;
memory_ += padding;
remaining_bytes_ -= padding;
return true;
}
} // namespace cc
......@@ -6,6 +6,7 @@
#define CC_PAINT_PAINT_OP_WRITER_H_
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_export.h"
struct SkRect;
struct SkIRect;
......@@ -14,8 +15,9 @@ class SkRRect;
namespace cc {
class ImageDecodeCache;
class PaintShader;
class PaintOpWriter {
class CC_PAINT_EXPORT PaintOpWriter {
public:
PaintOpWriter(void* memory, size_t size)
: memory_(static_cast<char*>(memory) + HeaderBytes()),
......@@ -46,6 +48,7 @@ class PaintOpWriter {
void Write(const PaintImage& image, ImageDecodeCache* cache);
void Write(const sk_sp<SkData>& data);
void Write(const sk_sp<SkTextBlob>& blob);
void Write(const PaintShader* shader);
void Write(SkClipOp op) { Write(static_cast<uint8_t>(op)); }
void Write(PaintCanvas::AnnotationType type) {
......@@ -62,6 +65,10 @@ class PaintOpWriter {
void WriteFlattenable(const SkFlattenable* val);
// Attempts to align the memory to the given alignment. Returns false if there
// is unsufficient bytes remaining to do this padding.
bool AlignMemory(size_t alignment);
char* memory_ = nullptr;
size_t size_ = 0u;
size_t remaining_bytes_ = 0u;
......
......@@ -22,7 +22,7 @@ using PaintRecord = PaintOpBuffer;
class CC_PAINT_EXPORT PaintShader : public SkRefCnt {
public:
enum class Type {
enum class Type : uint8_t {
kColor,
kLinearGradient,
kRadialGradient,
......@@ -36,7 +36,7 @@ class CC_PAINT_EXPORT PaintShader : public SkRefCnt {
// Scaling behavior dictates how a PaintRecord shader will behave. Use
// RasterAtScale to create a picture shader. Use FixedScale to create an image
// shader that is backed by the paint record.
enum class ScalingBehavior { kRasterAtScale, kFixedScale };
enum class ScalingBehavior : uint8_t { kRasterAtScale, kFixedScale };
static sk_sp<PaintShader> MakeColor(SkColor color);
......@@ -115,6 +115,9 @@ class CC_PAINT_EXPORT PaintShader : public SkRefCnt {
private:
friend class PaintFlags;
friend class PaintOpReader;
friend class PaintOpSerializationTestUtils;
friend class PaintOpWriter;
explicit PaintShader(Type type);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment