Merge tomhudson and mtklein SkPaint shrinking approaches.

I think this is cherry picking the best parts of both our CLs.  We've got dirty bit tracking from Tom's, picture format stability from Mike's, etc.  Paints are typically 1/3 their original size when flattened in the dictionary.

bench_record on my desktop looks promising.  Generally, looks faster.  (Best in monospace.)
a/b   skp                               before  after
0.83  desk_techcrunch.skp               0.29    0.24
0.83  tabl_gamedeksiam.skp              0.52    0.43
0.87  desk_carsvg.skp                   0.4     0.35
0.87  desk_googlehome.skp               0.038   0.033
0.87  desk_pokemonwiki.skp              3.9     3.4
0.88  desk_fontwipe.skp                 0.0089  0.0078
0.88  desk_googlespreadsheet.skp        0.16    0.14
0.89  desk_jsfiddlebigcar.skp           0.027   0.024
0.89  desk_tigersvg.skp                 0.038   0.034
0.89  desk_weather.skp                  0.19    0.17
0.89  tabl_engadget.skp                 0.37    0.33
0.89  tabl_googleblog.skp               0.28    0.25
0.9   desk_facebook.skp                 0.2     0.18
0.91  desk_mapsvg.skp                   0.45    0.41
0.91  desk_youtube.skp                  0.22    0.2
0.92  desk_forecastio.skp               0.12    0.11
0.92  desk_googlespreadsheetdashed.skp  0.49    0.45
0.92  desk_gws.skp                      0.13    0.12
0.92  desk_pinterest.skp                0.037   0.034
0.92  desk_twitter.skp                  0.25    0.23
0.92  tabl_culturalsolutions.skp        0.26    0.24
0.92  tabl_gspro.skp                    0.072   0.066
0.92  tabl_mercurynews.skp              0.26    0.24
0.93  desk_booking.skp                  0.46    0.43
0.93  desk_chalkboard.skp               0.28    0.26
0.93  desk_linkedin.skp                 0.14    0.13
0.93  desk_mobilenews.skp               0.28    0.26
0.93  tabl_cuteoverload.skp             0.46    0.43
0.93  tabl_deviantart.skp               0.15    0.14
0.93  tabl_gmail.skp                    0.029   0.027
0.93  tabl_googlecalendar.skp           0.15    0.14
0.93  tabl_mlb.skp                      0.15    0.14
0.94  desk_blogger.skp                  0.18    0.17
0.94  desk_jsfiddlehumperclip.skp       0.034   0.032
0.94  desk_wordpress.skp                0.33    0.31
0.94  desk_wowwiki.skp                  0.94    0.88
0.94  desk_yahooanswers.skp             0.17    0.16
0.94  desk_youtubetvvideo.skp           0.017   0.016
0.94  tabl_sahadan.skp                  0.093   0.087
0.94  tabl_worldjournal.skp             0.35    0.33
0.95  desk_css3gradients.skp            0.21    0.2
0.95  desk_gmailthread.skp              0.19    0.18
0.95  tabl_cnet.skp                     0.42    0.4
0.95  tabl_mozilla.skp                  1.9     1.8
0.95  tabl_pravda.skp                   0.19    0.18
0.96  mobi_wikipedia.skp                0.55    0.53
0.96  tabl_cnn.skp                      0.48    0.46
0.96  tabl_nofolo.skp                   0.05    0.048
0.97  desk_googleplus.skp               0.29    0.28
0.97  tabl_frantzen.skp                 0.059   0.057
0.97  tabl_onlinewsj.skp                0.38    0.37
0.97  tabl_slashdot.skp                 0.1     0.097
0.97  tabl_vnexpress.skp                0.29    0.28
0.99  desk_amazon.skp                   0.088   0.087
1     desk_baidu.skp                    0.097   0.099
1     desk_ebay.skp                     0.18    0.18
1     desk_espn.skp                     0.24    0.24
1     desk_oldinboxapp.skp              0.026   0.026
1     desk_rectangletransition.skp      0.014   0.014
1     desk_samoasvg.skp                 0.23    0.24
1     desk_yahoogames.skp               0.029   0.029
1     desk_yahoosports.skp              0.0033  0.0033
1     desk_youtubetvbrowse.skp          0.01    0.01
1     tabl_androidpolice.skp            0.65    0.65
1     tabl_digg.skp                     0.33    0.33
1     tabl_hsfi.skp                     0.32    0.32
1     tabl_nytimes.skp                  0.22    0.22
1     tabl_techmeme.skp                 0.069   0.072
1     tabl_ukwsj.skp                    0.35    0.35
1.1   desk_sfgate.skp                   0.25    0.28


BUG=skia:2190

Committed: http://code.google.com/p/skia/source/detail?r=13487

R=tomhudson@google.com, reed@google.com, mtklein@google.com

Author: mtklein@chromium.org

Review URL: https://codereview.chromium.org/158913005

git-svn-id: http://skia.googlecode.com/svn/trunk@13496 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/include/core/SkPaint.h b/include/core/SkPaint.h
index abb8599..bf0f91c 100644
--- a/include/core/SkPaint.h
+++ b/include/core/SkPaint.h
@@ -980,6 +980,11 @@
 
     SkDEVCODE(void toString(SkString*) const;)
 
+    struct FlatteningTraits {
+        static void Flatten(SkWriteBuffer& buffer, const SkPaint& paint);
+        static void Unflatten(SkReadBuffer& buffer, SkPaint* paint);
+    };
+
 private:
     SkTypeface*     fTypeface;
     SkScalar        fTextSize;
@@ -999,15 +1004,25 @@
     SkColor         fColor;
     SkScalar        fWidth;
     SkScalar        fMiterLimit;
-    // all of these bitfields should add up to 32
-    unsigned        fFlags : 16;
-    unsigned        fTextAlign : 2;
-    unsigned        fCapType : 2;
-    unsigned        fJoinType : 2;
-    unsigned        fStyle : 2;
-    unsigned        fTextEncoding : 2;  // 3 values
-    unsigned        fHinting : 2;
-    //unsigned      fFreeBits : 4;
+
+    union {
+        struct {
+            // all of these bitfields should add up to 32
+            unsigned        fFlags : 16;
+            unsigned        fTextAlign : 2;
+            unsigned        fCapType : 2;
+            unsigned        fJoinType : 2;
+            unsigned        fStyle : 2;
+            unsigned        fTextEncoding : 2;  // 3 values
+            unsigned        fHinting : 2;
+            //unsigned      fFreeBits : 4;
+        };
+        uint32_t fBitfields;
+    };
+    uint32_t getBitfields() const { return fBitfields; }
+    void setBitfields(uint32_t bitfields);
+
+    uint32_t fDirtyBits;
 
 
     SkDrawCacheProc    getDrawCacheProc() const;
diff --git a/src/core/SkPaint.cpp b/src/core/SkPaint.cpp
index c3f217c..236bbbd 100644
--- a/src/core/SkPaint.cpp
+++ b/src/core/SkPaint.cpp
@@ -35,6 +35,26 @@
 #include "SkTypeface.h"
 #include "SkXfermode.h"
 
+enum {
+    kColor_DirtyBit               = 1 <<  0,
+    kBitfields_DirtyBit           = 1 <<  1,
+    kTextSize_DirtyBit            = 1 <<  2,
+    kTextScaleX_DirtyBit          = 1 <<  3,
+    kTextSkewX_DirtyBit           = 1 <<  4,
+    kStrokeWidth_DirtyBit         = 1 <<  5,
+    kStrokeMiter_DirtyBit         = 1 <<  6,
+    kPathEffect_DirtyBit          = 1 <<  7,
+    kShader_DirtyBit              = 1 <<  8,
+    kXfermode_DirtyBit            = 1 <<  9,
+    kMaskFilter_DirtyBit          = 1 << 10,
+    kColorFilter_DirtyBit         = 1 << 11,
+    kRasterizer_DirtyBit          = 1 << 12,
+    kLooper_DirtyBit              = 1 << 13,
+    kImageFilter_DirtyBit         = 1 << 14,
+    kTypeface_DirtyBit            = 1 << 15,
+    kAnnotation_DirtyBit          = 1 << 16,
+    kPaintOptionsAndroid_DirtyBit = 1 << 17,
+};
 
 // define this to get a printf for out-of-range parameter in setters
 // e.g. setTextSize(-1)
@@ -55,8 +75,8 @@
     sk_bzero(this, sizeof(*this));
 
 #if 0   // not needed with the bzero call above
-    fTypeface   = NULL;
-    fTextSkewX  = 0;
+    fTypeface    = NULL;
+    fTextSkewX   = 0;
     fPathEffect  = NULL;
     fShader      = NULL;
     fXfermode    = NULL;
@@ -66,7 +86,8 @@
     fLooper      = NULL;
     fImageFilter = NULL;
     fAnnotation  = NULL;
-    fWidth      = 0;
+    fWidth       = 0;
+    fDirtyBits   = 0;
 #endif
 
     fTextSize   = SkPaintDefaults_TextSize;
@@ -198,6 +219,7 @@
     if (options != fPaintOptionsAndroid) {
         fPaintOptionsAndroid = options;
         GEN_ID_INC;
+        fDirtyBits |= kPaintOptionsAndroid_DirtyBit;
     }
 }
 #endif
@@ -228,11 +250,13 @@
 void SkPaint::setHinting(Hinting hintingLevel) {
     GEN_ID_INC_EVAL((unsigned) hintingLevel != fHinting);
     fHinting = hintingLevel;
+    fDirtyBits |= kBitfields_DirtyBit;
 }
 
 void SkPaint::setFlags(uint32_t flags) {
     GEN_ID_INC_EVAL(fFlags != flags);
     fFlags = flags;
+    fDirtyBits |= kBitfields_DirtyBit;
 }
 
 void SkPaint::setAntiAlias(bool doAA) {
@@ -287,6 +311,7 @@
     if ((unsigned)style < kStyleCount) {
         GEN_ID_INC_EVAL((unsigned)style != fStyle);
         fStyle = style;
+        fDirtyBits |= kBitfields_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setStyle(%d) out of range\n", style);
@@ -297,6 +322,7 @@
 void SkPaint::setColor(SkColor color) {
     GEN_ID_INC_EVAL(color != fColor);
     fColor = color;
+    fDirtyBits |= kColor_DirtyBit;
 }
 
 void SkPaint::setAlpha(U8CPU a) {
@@ -312,6 +338,7 @@
     if (width >= 0) {
         GEN_ID_INC_EVAL(width != fWidth);
         fWidth = width;
+        fDirtyBits |= kStrokeWidth_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setStrokeWidth() called with negative value\n");
@@ -323,6 +350,7 @@
     if (limit >= 0) {
         GEN_ID_INC_EVAL(limit != fMiterLimit);
         fMiterLimit = limit;
+        fDirtyBits |= kStrokeMiter_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setStrokeMiter() called with negative value\n");
@@ -334,6 +362,7 @@
     if ((unsigned)ct < kCapCount) {
         GEN_ID_INC_EVAL((unsigned)ct != fCapType);
         fCapType = SkToU8(ct);
+        fDirtyBits |= kBitfields_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setStrokeCap(%d) out of range\n", ct);
@@ -345,6 +374,7 @@
     if ((unsigned)jt < kJoinCount) {
         GEN_ID_INC_EVAL((unsigned)jt != fJoinType);
         fJoinType = SkToU8(jt);
+        fDirtyBits |= kBitfields_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setStrokeJoin(%d) out of range\n", jt);
@@ -358,6 +388,7 @@
     if ((unsigned)align < kAlignCount) {
         GEN_ID_INC_EVAL((unsigned)align != fTextAlign);
         fTextAlign = SkToU8(align);
+        fDirtyBits |= kBitfields_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setTextAlign(%d) out of range\n", align);
@@ -369,6 +400,7 @@
     if (ts >= 0) {
         GEN_ID_INC_EVAL(ts != fTextSize);
         fTextSize = ts;
+        fDirtyBits |= kTextSize_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setTextSize() called with negative value\n");
@@ -379,17 +411,20 @@
 void SkPaint::setTextScaleX(SkScalar scaleX) {
     GEN_ID_INC_EVAL(scaleX != fTextScaleX);
     fTextScaleX = scaleX;
+    fDirtyBits |= kTextScaleX_DirtyBit;
 }
 
 void SkPaint::setTextSkewX(SkScalar skewX) {
     GEN_ID_INC_EVAL(skewX != fTextSkewX);
     fTextSkewX = skewX;
+    fDirtyBits |= kTextSkewX_DirtyBit;
 }
 
 void SkPaint::setTextEncoding(TextEncoding encoding) {
     if ((unsigned)encoding <= kGlyphID_TextEncoding) {
         GEN_ID_INC_EVAL((unsigned)encoding != fTextEncoding);
         fTextEncoding = encoding;
+        fDirtyBits |= kBitfields_DirtyBit;
     } else {
 #ifdef SK_REPORT_API_RANGE_CHECK
         SkDebugf("SkPaint::setTextEncoding(%d) out of range\n", encoding);
@@ -399,33 +434,43 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
+// Returns dst with the given bitmask enabled or disabled, depending on value.
+inline static uint32_t set_mask(uint32_t dst, uint32_t bitmask, bool value) {
+    return value ? (dst | bitmask) : (dst & ~bitmask);
+}
+
 SkTypeface* SkPaint::setTypeface(SkTypeface* font) {
     SkRefCnt_SafeAssign(fTypeface, font);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kTypeface_DirtyBit, font != NULL);
     return font;
 }
 
 SkRasterizer* SkPaint::setRasterizer(SkRasterizer* r) {
     SkRefCnt_SafeAssign(fRasterizer, r);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kRasterizer_DirtyBit, r != NULL);
     return r;
 }
 
 SkDrawLooper* SkPaint::setLooper(SkDrawLooper* looper) {
     SkRefCnt_SafeAssign(fLooper, looper);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kLooper_DirtyBit, looper != NULL);
     return looper;
 }
 
 SkImageFilter* SkPaint::setImageFilter(SkImageFilter* imageFilter) {
     SkRefCnt_SafeAssign(fImageFilter, imageFilter);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kImageFilter_DirtyBit, imageFilter != NULL);
     return imageFilter;
 }
 
 SkAnnotation* SkPaint::setAnnotation(SkAnnotation* annotation) {
     SkRefCnt_SafeAssign(fAnnotation, annotation);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kAnnotation_DirtyBit, annotation != NULL);
     return annotation;
 }
 
@@ -2149,18 +2194,21 @@
 SkShader* SkPaint::setShader(SkShader* shader) {
     GEN_ID_INC_EVAL(shader != fShader);
     SkRefCnt_SafeAssign(fShader, shader);
+    fDirtyBits = set_mask(fDirtyBits, kShader_DirtyBit, shader != NULL);
     return shader;
 }
 
 SkColorFilter* SkPaint::setColorFilter(SkColorFilter* filter) {
     GEN_ID_INC_EVAL(filter != fColorFilter);
     SkRefCnt_SafeAssign(fColorFilter, filter);
+    fDirtyBits = set_mask(fDirtyBits, kColorFilter_DirtyBit, filter != NULL);
     return filter;
 }
 
 SkXfermode* SkPaint::setXfermode(SkXfermode* mode) {
     GEN_ID_INC_EVAL(mode != fXfermode);
     SkRefCnt_SafeAssign(fXfermode, mode);
+    fDirtyBits = set_mask(fDirtyBits, kXfermode_DirtyBit, mode != NULL);
     return mode;
 }
 
@@ -2168,18 +2216,21 @@
     SkSafeUnref(fXfermode);
     fXfermode = SkXfermode::Create(mode);
     GEN_ID_INC;
+    fDirtyBits = set_mask(fDirtyBits, kXfermode_DirtyBit, fXfermode != NULL);
     return fXfermode;
 }
 
 SkPathEffect* SkPaint::setPathEffect(SkPathEffect* effect) {
     GEN_ID_INC_EVAL(effect != fPathEffect);
     SkRefCnt_SafeAssign(fPathEffect, effect);
+    fDirtyBits = set_mask(fDirtyBits, kPathEffect_DirtyBit, effect != NULL);
     return effect;
 }
 
 SkMaskFilter* SkPaint::setMaskFilter(SkMaskFilter* filter) {
     GEN_ID_INC_EVAL(filter != fMaskFilter);
     SkRefCnt_SafeAssign(fMaskFilter, filter);
+    fDirtyBits = set_mask(fDirtyBits, kMaskFilter_DirtyBit, filter != NULL);
     return filter;
 }
 
@@ -2550,3 +2601,91 @@
     }
     return false;
 }
+
+void SkPaint::setBitfields(uint32_t bitfields) {
+    fBitfields = bitfields;
+    fDirtyBits |= kBitfields_DirtyBit;
+}
+
+inline static unsigned popcount(uint8_t x) {
+    // As in Hacker's delight, adapted for just 8 bits.
+    x = (x & 0x55) + ((x >> 1) & 0x55);  // a b c d w x y z -> a+b c+d w+x y+z
+    x = (x & 0x33) + ((x >> 2) & 0x33);  // a+b c+d w+x y+z -> a+b+c+d w+x+y+z
+    x = (x & 0x0F) + ((x >> 4) & 0x0F);  // a+b+c+d w+x+y+z -> a+b+c+d+w+x+y+z
+    return x;
+}
+
+void SkPaint::FlatteningTraits::Flatten(SkWriteBuffer& buffer, const SkPaint& paint) {
+    const uint32_t dirty = paint.fDirtyBits;
+
+    // Each of the low 7 dirty bits corresponds to a 4-byte flat value, plus one for the dirty bits.
+    const size_t flatBytes = 4 * (popcount(dirty & 127) + 1);
+    SkASSERT(flatBytes <= 32);
+    uint32_t* u32 = buffer.reserve(flatBytes);
+    *u32++ = dirty;
+    if (dirty == 0) {
+        return;
+    }
+
+#define F(dst, field) if (dirty & k##field##_DirtyBit) *dst++ = paint.get##field()
+    F(u32, Color);
+    F(u32, Bitfields);
+    SkScalar* f32 = reinterpret_cast<SkScalar*>(u32);
+    F(f32, TextSize);
+    F(f32, TextScaleX);
+    F(f32, TextSkewX);
+    F(f32, StrokeWidth);
+    F(f32, StrokeMiter);
+#undef F
+#define F(field) if (dirty & k##field##_DirtyBit) buffer.writeFlattenable(paint.get##field())
+    F(PathEffect);
+    F(Shader);
+    F(Xfermode);
+    F(MaskFilter);
+    F(ColorFilter);
+    F(Rasterizer);
+    F(Looper);
+    F(ImageFilter);
+#undef F
+    if (dirty & kTypeface_DirtyBit) buffer.writeTypeface(paint.getTypeface());
+    if (dirty & kAnnotation_DirtyBit) paint.getAnnotation()->writeToBuffer(buffer);
+#ifdef SK_BUILD_FOR_ANDROID
+    if (dirty & kPaintOptionsAndroid_DirtyBit) paint.getPaintOptionsAndroid().flatten(buffer);
+#endif
+}
+
+void SkPaint::FlatteningTraits::Unflatten(SkReadBuffer& buffer, SkPaint* paint) {
+    const uint32_t dirty = buffer.readUInt();
+    if (dirty == 0) {
+        return;
+    }
+#define F(field, reader) if (dirty & k##field##_DirtyBit) paint->set##field(buffer.reader())
+    F(Color,       readUInt);
+    F(Bitfields,   readUInt);
+    F(TextSize,    readScalar);
+    F(TextScaleX,  readScalar);
+    F(TextSkewX,   readScalar);
+    F(StrokeWidth, readScalar);
+    F(StrokeMiter, readScalar);
+    F(PathEffect,  readPathEffect);
+    F(Shader,      readShader);
+    F(Xfermode,    readXfermode);
+    F(MaskFilter,  readMaskFilter);
+    F(ColorFilter, readColorFilter);
+    F(Rasterizer,  readRasterizer);
+    F(Looper,      readDrawLooper);
+    F(ImageFilter, readImageFilter);
+    F(Typeface,    readTypeface);
+#undef F
+    if (dirty & kAnnotation_DirtyBit) {
+        paint->setAnnotation(SkNEW_ARGS(SkAnnotation, (buffer)))->unref();
+    }
+#ifdef SK_BUILD_FOR_ANDROID
+    if (dirty & kPaintOptionsAndroid_DirtyBit) {
+        SkPaintOptionsAndroid options;
+        options.unflatten(buffer);
+        paint->setPaintOptionsAndroid(options);
+    }
+#endif
+    SkASSERT(dirty == paint->fDirtyBits);
+}
diff --git a/src/core/SkPictureFlat.h b/src/core/SkPictureFlat.h
index 6b4af13..8e87e71 100644
--- a/src/core/SkPictureFlat.h
+++ b/src/core/SkPictureFlat.h
@@ -561,15 +561,7 @@
                    SkFlatData::Identity, SkFlatData::Hash, SkFlatData::Equal> fHash;
 };
 
-struct SkPaintTraits {
-    static void Flatten(SkWriteBuffer& buffer, const SkPaint& paint) {
-        paint.flatten(buffer);
-    }
-    static void Unflatten(SkReadBuffer& buffer, SkPaint* paint) {
-        paint->unflatten(buffer);
-    }
-};
-typedef SkFlatDictionary<SkPaint, SkPaintTraits> SkPaintDictionary;
+typedef SkFlatDictionary<SkPaint, SkPaint::FlatteningTraits> SkPaintDictionary;
 
 class SkChunkFlatController : public SkFlatController {
 public:
diff --git a/src/core/SkPicturePlayback.cpp b/src/core/SkPicturePlayback.cpp
index 8b8c6b0..46f1c05 100644
--- a/src/core/SkPicturePlayback.cpp
+++ b/src/core/SkPicturePlayback.cpp
@@ -209,7 +209,7 @@
             for (int i = 0; i < paintCount; i++) {
                 if (needs_deep_copy(src.fPaints->at(i))) {
                     deepCopyInfo->paintData[i] =
-                        SkFlatData::Create<SkPaintTraits>(&deepCopyInfo->controller,
+                        SkFlatData::Create<SkPaint::FlatteningTraits>(&deepCopyInfo->controller,
                                                           src.fPaints->at(i), 0);
 
                 } else {
@@ -230,8 +230,8 @@
         SkTypefacePlayback* tfPlayback = deepCopyInfo->controller.getTypefacePlayback();
         for (int i = 0; i < paintCount; i++) {
             if (deepCopyInfo->paintData[i]) {
-                deepCopyInfo->paintData[i]->unflatten<SkPaintTraits>(&fPaints->writableAt(i),
-                                                                     bmHeap, tfPlayback);
+                deepCopyInfo->paintData[i]->unflatten<SkPaint::FlatteningTraits>(
+                    &fPaints->writableAt(i), bmHeap, tfPlayback);
             } else {
                 // needs_deep_copy was false, so just need to assign
                 fPaints->writableAt(i) = src.fPaints->at(i);
diff --git a/tests/PaintTest.cpp b/tests/PaintTest.cpp
index 3210e77..216c2ea 100644
--- a/tests/PaintTest.cpp
+++ b/tests/PaintTest.cpp
@@ -11,8 +11,11 @@
 #include "SkPaint.h"
 #include "SkPath.h"
 #include "SkRandom.h"
+#include "SkReadBuffer.h"
 #include "SkTypeface.h"
 #include "SkUtils.h"
+#include "SkWriteBuffer.h"
+#include "SkXfermode.h"
 #include "Test.h"
 
 static size_t uni_to_utf8(const SkUnichar src[], void* dst, int count) {
@@ -251,3 +254,40 @@
        test_cmap(reporter);
     }
 }
+
+#define ASSERT(expr) REPORTER_ASSERT(r, expr)
+
+DEF_TEST(Paint_FlatteningTraits, r) {
+    SkPaint paint;
+    paint.setColor(0x00AABBCC);
+    paint.setTextScaleX(1.0f);  // Encoded despite being the default value.
+    paint.setTextSize(19);
+    paint.setXfermode(SkXfermode::Create(SkXfermode::kModulate_Mode));
+    paint.setLooper(NULL);  // Ignored.
+
+    SkWriteBuffer writer;
+    SkPaint::FlatteningTraits::Flatten(writer, paint);
+    const size_t expectedBytesWritten = sizeof(void*) == 8 ? 48 : 40;
+    ASSERT(expectedBytesWritten == writer.bytesWritten());
+
+    const uint32_t* written = writer.getWriter32()->contiguousArray();
+    SkASSERT(written != NULL);
+    ASSERT(*written == ((1<<0) | (1<<2) | (1<<3) | (1<<9)));  // Dirty bits for our 4.
+
+    SkReadBuffer reader(written, writer.bytesWritten());
+    SkPaint other;
+    SkPaint::FlatteningTraits::Unflatten(reader, &other);
+    ASSERT(reader.offset() == writer.bytesWritten());
+
+    // No matter the encoding, these must always hold.
+    ASSERT(other.getColor()      == paint.getColor());
+    ASSERT(other.getTextScaleX() == paint.getTextScaleX());
+    ASSERT(other.getTextSize()   == paint.getTextSize());
+    ASSERT(other.getLooper()     == paint.getLooper());
+
+    // We have to be a little looser and compare just the modes.  Pointers might not be the same.
+    SkXfermode::Mode otherMode, paintMode;
+    ASSERT(other.getXfermode()->asMode(&otherMode));
+    ASSERT(paint.getXfermode()->asMode(&paintMode));
+    ASSERT(otherMode == paintMode);
+}