added skeletal animation support to GPU backend

added caching of SkVertices

Docs-Preview: https://skia.org/?cl=138596
Bug: skia:
Change-Id: Ia750f55f5f6d0de250d9e9c5619f4d1ac856f9f5
Reviewed-on: https://skia-review.googlesource.com/138596
Reviewed-by: Brian Osman <brianosman@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Ruiqi Mao <ruiqimao@google.com>
diff --git a/src/core/SkBlurMF.cpp b/src/core/SkBlurMF.cpp
index 23cca83..f3d7123 100644
--- a/src/core/SkBlurMF.cpp
+++ b/src/core/SkBlurMF.cpp
@@ -874,7 +874,8 @@
         }
 
         paint.addCoverageFragmentProcessor(std::move(fp));
-        renderTargetContext->drawVertices(clip, std::move(paint), viewMatrix, std::move(vertices));
+        renderTargetContext->drawVertices(clip, std::move(paint), viewMatrix, std::move(vertices),
+                                          nullptr, 0);
     } else {
         SkMatrix inverse;
         if (!viewMatrix.invert(&inverse)) {
diff --git a/src/core/SkCanvas.cpp b/src/core/SkCanvas.cpp
index 8314f80..45bbc64 100644
--- a/src/core/SkCanvas.cpp
+++ b/src/core/SkCanvas.cpp
@@ -1715,6 +1715,7 @@
                             SkBlendMode mode, const SkPaint& paint) {
     TRACE_EVENT0("skia", TRACE_FUNC);
     RETURN_ON_NULL(vertices);
+    SkASSERT(boneCount <= 100);
     this->onDrawVerticesObject(vertices.get(), bones, boneCount, mode, paint);
 }
 
@@ -1722,6 +1723,7 @@
                             SkBlendMode mode, const SkPaint& paint) {
     TRACE_EVENT0("skia", TRACE_FUNC);
     RETURN_ON_NULL(vertices);
+    SkASSERT(boneCount <= 100);
     this->onDrawVerticesObject(vertices, bones, boneCount, mode, paint);
 }
 
diff --git a/src/core/SkVertices.cpp b/src/core/SkVertices.cpp
index 04db0d3..7cbf209 100644
--- a/src/core/SkVertices.cpp
+++ b/src/core/SkVertices.cpp
@@ -95,16 +95,17 @@
     bool hasTexs = SkToBool(builderFlags & SkVertices::kHasTexCoords_BuilderFlag);
     bool hasColors = SkToBool(builderFlags & SkVertices::kHasColors_BuilderFlag);
     bool hasBones = SkToBool(builderFlags & SkVertices::kHasBones_BuilderFlag);
-    this->init(mode, vertexCount, indexCount,
+    bool isVolatile = SkToBool(builderFlags & SkVertices::kIsVolatile_BuilderFlag);
+    this->init(mode, vertexCount, indexCount, isVolatile,
                SkVertices::Sizes(mode, vertexCount, indexCount, hasTexs, hasColors, hasBones));
 }
 
-SkVertices::Builder::Builder(VertexMode mode, int vertexCount, int indexCount,
+SkVertices::Builder::Builder(VertexMode mode, int vertexCount, int indexCount, bool isVolatile,
                              const SkVertices::Sizes& sizes) {
-    this->init(mode, vertexCount, indexCount, sizes);
+    this->init(mode, vertexCount, indexCount, isVolatile, sizes);
 }
 
-void SkVertices::Builder::init(VertexMode mode, int vertexCount, int indexCount,
+void SkVertices::Builder::init(VertexMode mode, int vertexCount, int indexCount, bool isVolatile,
                                const SkVertices::Sizes& sizes) {
     if (!sizes.isValid()) {
         return; // fVertices will already be null
@@ -128,6 +129,7 @@
     fVertices->fIndices = sizes.fISize ? (uint16_t*)ptr : nullptr;
     fVertices->fVertexCnt = vertexCount;
     fVertices->fIndexCnt = indexCount;
+    fVertices->fIsVolatile = isVolatile;
     fVertices->fMode = mode;
 
     // We defer assigning fBounds and fUniqueID until detach() is called
@@ -171,6 +173,10 @@
     return fVertices ? fVertices->indexCount() : 0;
 }
 
+bool SkVertices::Builder::isVolatile() const {
+    return fVertices ? fVertices->isVolatile() : false;
+}
+
 SkPoint* SkVertices::Builder::positions() {
     return fVertices ? const_cast<SkPoint*>(fVertices->positions()) : nullptr;
 }
@@ -208,7 +214,8 @@
                                        const SkColor colors[],
                                        const BoneIndices boneIndices[],
                                        const BoneWeights boneWeights[],
-                                       int indexCount, const uint16_t indices[]) {
+                                       int indexCount, const uint16_t indices[],
+                                       bool isVolatile) {
     SkASSERT((!boneIndices && !boneWeights) || (boneIndices && boneWeights));
     Sizes sizes(mode,
                 vertexCount,
@@ -220,7 +227,7 @@
         return nullptr;
     }
 
-    Builder builder(mode, vertexCount, indexCount, sizes);
+    Builder builder(mode, vertexCount, indexCount, isVolatile, sizes);
     SkASSERT(builder.isValid());
 
     sk_careful_memcpy(builder.positions(), pos, sizes.fVSize);
@@ -247,15 +254,15 @@
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-// storage = packed | vertex_count | index_count | pos[] | texs[] | colors[] | boneIndices[] |
-//           boneWeights[] | indices[]
+// storage = packed | vertex_count | index_count | is_volatile | pos[] | texs[] | colors[] |
+//           boneIndices[] | boneWeights[] | indices[]
 //         = header + arrays
 
 #define kMode_Mask          0x0FF
 #define kHasTexs_Mask       0x100
 #define kHasColors_Mask     0x200
 #define kHasBones_Mask      0x400
-#define kHeaderSize         (3 * sizeof(uint32_t))
+#define kHeaderSize         (4 * sizeof(uint32_t))
 
 sk_sp<SkData> SkVertices::encode() const {
     // packed has room for addtional flags in the future (e.g. versioning)
@@ -288,6 +295,7 @@
     writer.write32(packed);
     writer.write32(fVertexCnt);
     writer.write32(fIndexCnt);
+    writer.writeBool(fIsVolatile);
     writer.write(fPositions, sizes.fVSize);
     writer.write(fTexs, sizes.fTSize);
     writer.write(fColors, sizes.fCSize);
@@ -310,6 +318,7 @@
     const uint32_t packed = reader.readInt();
     const int vertexCount = safe.checkGE(reader.readInt(), 0);
     const int indexCount = safe.checkGE(reader.readInt(), 0);
+    const bool isVolatile = reader.readBool();
     const VertexMode mode = safe.checkLE<VertexMode>(packed & kMode_Mask,
                                                      SkVertices::kLast_VertexMode);
     if (!safe) {
@@ -327,7 +336,7 @@
         return nullptr;
     }
 
-    Builder builder(mode, vertexCount, indexCount, sizes);
+    Builder builder(mode, vertexCount, indexCount, isVolatile, sizes);
 
     reader.read(builder.positions(), sizes.fVSize);
     reader.read(builder.texCoords(), sizes.fTSize);
diff --git a/src/gpu/GrDefaultGeoProcFactory.cpp b/src/gpu/GrDefaultGeoProcFactory.cpp
index e2bf87b..c9108e3 100644
--- a/src/gpu/GrDefaultGeoProcFactory.cpp
+++ b/src/gpu/GrDefaultGeoProcFactory.cpp
@@ -27,8 +27,13 @@
     kColorAttributeIsSkColor_GPFlag = 0x2,
     kLocalCoordAttribute_GPFlag     = 0x4,
     kCoverageAttribute_GPFlag       = 0x8,
+    kBonesAttribute_GPFlag          = 0x10,
 };
 
+static constexpr int kNumFloatsPerSkMatrix = 9;
+static constexpr int kMaxBones = 100; // Due to GPU memory limitations, only up to 100 bone
+                                      // matrices are accepted.
+
 class DefaultGeoProc : public GrGeometryProcessor {
 public:
     static sk_sp<GrGeometryProcessor> Make(uint32_t gpTypeFlags,
@@ -37,10 +42,12 @@
                                            const SkMatrix& viewMatrix,
                                            const SkMatrix& localMatrix,
                                            bool localCoordsWillBeRead,
-                                           uint8_t coverage) {
+                                           uint8_t coverage,
+                                           const SkMatrix bones[],
+                                           int boneCount) {
         return sk_sp<GrGeometryProcessor>(new DefaultGeoProc(
                 gpTypeFlags, color, std::move(colorSpaceXform), viewMatrix, localMatrix, coverage,
-                localCoordsWillBeRead));
+                localCoordsWillBeRead, bones, boneCount));
     }
 
     const char* name() const override { return "DefaultGeometryProcessor"; }
@@ -52,6 +59,9 @@
     bool localCoordsWillBeRead() const { return fLocalCoordsWillBeRead; }
     uint8_t coverage() const { return fCoverage; }
     bool hasVertexCoverage() const { return fInCoverage.isInitialized(); }
+    const float* bones() const { return fBones.data(); }
+    int boneCount() const { return fBones.size() / kNumFloatsPerSkMatrix; }
+    bool hasBones() const { return fBones.size() > 0; }
 
     class GLSLProcessor : public GrGLSLGeometryProcessor {
     public:
@@ -99,11 +109,37 @@
                                         &fColorUniform);
             }
 
+            // Setup bone transforms
+            const char* transformedPositionName = gp.fInPosition.name();
+            if (gp.hasBones()) {
+                const char* vertBonesUniformName;
+                fBonesUniform = uniformHandler->addUniformArray(kVertex_GrShaderFlag,
+                                                                kFloat3x3_GrSLType,
+                                                                "Bones",
+                                                                kMaxBones,
+                                                                &vertBonesUniformName);
+                vertBuilder->codeAppendf(
+                        "float2 transformedPosition = (%s[0] * float3(%s, 1)).xy;"
+                        "float3x3 influence = float3x3(0);"
+                        "for (int i = 0; i < 4; i++) {"
+                        "   int index = %s[i];"
+                        "   float weight = %s[i];"
+                        "   influence += %s[index] * weight;"
+                        "}"
+                        "transformedPosition = (influence * float3(transformedPosition, 1)).xy;",
+                        vertBonesUniformName,
+                        gp.fInPosition.name(),
+                        gp.fInBoneIndices.name(),
+                        gp.fInBoneWeights.name(),
+                        vertBonesUniformName);
+                transformedPositionName = "transformedPosition";
+            }
+
             // Setup position
             this->writeOutputPosition(vertBuilder,
                                       uniformHandler,
                                       gpArgs,
-                                      gp.fInPosition.name(),
+                                      transformedPositionName,
                                       gp.viewMatrix(),
                                       &fViewMatrixUniform);
 
@@ -147,8 +183,8 @@
                                   GrProcessorKeyBuilder* b) {
             const DefaultGeoProc& def = gp.cast<DefaultGeoProc>();
             uint32_t key = def.fFlags;
-            key |= (def.coverage() == 0xff) ? 0x10 : 0;
-            key |= (def.localCoordsWillBeRead() && def.localMatrix().hasPerspective()) ? 0x20 : 0x0;
+            key |= (def.coverage() == 0xff) ? 0x20 : 0;
+            key |= (def.localCoordsWillBeRead() && def.localMatrix().hasPerspective()) ? 0x40 : 0x0;
             key |= ComputePosKey(def.viewMatrix()) << 20;
             b->add32(key);
             b->add32(GrColorSpaceXform::XformKey(def.fColorSpaceXform.get()));
@@ -180,6 +216,10 @@
             this->setTransformDataHelper(dgp.fLocalMatrix, pdman, &transformIter);
 
             fColorSpaceHelper.setData(pdman, dgp.fColorSpaceXform.get());
+
+            if (dgp.hasBones()) {
+                pdman.setMatrix3fv(fBonesUniform, dgp.boneCount(), dgp.bones());
+            }
         }
 
     private:
@@ -189,6 +229,7 @@
         UniformHandle fViewMatrixUniform;
         UniformHandle fColorUniform;
         UniformHandle fCoverageUniform;
+        UniformHandle fBonesUniform;
         GrGLSLColorSpaceXformHelper fColorSpaceHelper;
 
         typedef GrGLSLGeometryProcessor INHERITED;
@@ -209,7 +250,9 @@
                    const SkMatrix& viewMatrix,
                    const SkMatrix& localMatrix,
                    uint8_t coverage,
-                   bool localCoordsWillBeRead)
+                   bool localCoordsWillBeRead,
+                   const SkMatrix* bones,
+                   int boneCount)
             : INHERITED(kDefaultGeoProc_ClassID)
             , fColor(color)
             , fViewMatrix(viewMatrix)
@@ -217,7 +260,8 @@
             , fCoverage(coverage)
             , fFlags(gpTypeFlags)
             , fLocalCoordsWillBeRead(localCoordsWillBeRead)
-            , fColorSpaceXform(std::move(colorSpaceXform)) {
+            , fColorSpaceXform(std::move(colorSpaceXform))
+            , fBones() {
         fInPosition = {"inPosition", kFloat2_GrVertexAttribType};
         int cnt = 1;
         if (fFlags & kColorAttribute_GPFlag) {
@@ -232,17 +276,49 @@
             fInCoverage = {"inCoverage", kHalf_GrVertexAttribType};
             ++cnt;
         }
+        if (fFlags & kBonesAttribute_GPFlag) {
+            SkASSERT(bones && (boneCount > 0));
+            fInBoneIndices = {"inBoneIndices", kInt4_GrVertexAttribType};
+            ++cnt;
+            fInBoneWeights = {"inBoneWeights", kFloat4_GrVertexAttribType};
+            ++cnt;
+        }
         this->setVertexAttributeCnt(cnt);
+
+        // Set bone data
+        if (bones) {
+            fBones.reserve(boneCount * kNumFloatsPerSkMatrix);
+            for (int i = 0; i < boneCount; i ++) {
+                const SkMatrix& matrix = bones[i];
+                fBones.push_back(matrix.get(SkMatrix::kMScaleX));
+                fBones.push_back(matrix.get(SkMatrix::kMSkewY));
+                fBones.push_back(matrix.get(SkMatrix::kMPersp0));
+                fBones.push_back(matrix.get(SkMatrix::kMSkewX));
+                fBones.push_back(matrix.get(SkMatrix::kMScaleY));
+                fBones.push_back(matrix.get(SkMatrix::kMPersp1));
+                fBones.push_back(matrix.get(SkMatrix::kMTransX));
+                fBones.push_back(matrix.get(SkMatrix::kMTransY));
+                fBones.push_back(matrix.get(SkMatrix::kMPersp2));
+            }
+        }
     }
 
     const Attribute& onVertexAttribute(int i) const override {
-        return IthInitializedAttribute(i, fInPosition, fInColor, fInLocalCoords, fInCoverage);
+        return IthInitializedAttribute(i,
+                                       fInPosition,
+                                       fInColor,
+                                       fInLocalCoords,
+                                       fInCoverage,
+                                       fInBoneIndices,
+                                       fInBoneWeights);
     }
 
     Attribute fInPosition;
     Attribute fInColor;
     Attribute fInLocalCoords;
     Attribute fInCoverage;
+    Attribute fInBoneIndices;
+    Attribute fInBoneWeights;
     GrColor fColor;
     SkMatrix fViewMatrix;
     SkMatrix fLocalMatrix;
@@ -250,6 +326,7 @@
     uint32_t fFlags;
     bool fLocalCoordsWillBeRead;
     sk_sp<GrColorSpaceXform> fColorSpaceXform;
+    std::vector<float> fBones;
 
     GR_DECLARE_GEOMETRY_PROCESSOR_TEST
 
@@ -273,6 +350,17 @@
     if (d->fRandom->nextBool()) {
         flags |= kLocalCoordAttribute_GPFlag;
     }
+    if (d->fRandom->nextBool()) {
+        flags |= kBonesAttribute_GPFlag;
+    }
+
+    int numBones = d->fRandom->nextRangeU(0, kMaxBones);
+    std::vector<SkMatrix> bones(numBones);
+    for (int i = 0; i < numBones; i++) {
+        for (int j = 0; j < kNumFloatsPerSkMatrix; j++) {
+            bones[i][j] = d->fRandom->nextF();
+        }
+    }
 
     return DefaultGeoProc::Make(flags,
                                 GrRandomColor(d->fRandom),
@@ -280,7 +368,9 @@
                                 GrTest::TestMatrix(d->fRandom),
                                 GrTest::TestMatrix(d->fRandom),
                                 d->fRandom->nextBool(),
-                                GrRandomCoverage(d->fRandom));
+                                GrRandomCoverage(d->fRandom),
+                                bones.data(),
+                                numBones);
 }
 #endif
 
@@ -307,7 +397,9 @@
                                 viewMatrix,
                                 localCoords.fMatrix ? *localCoords.fMatrix : SkMatrix::I(),
                                 localCoordsWillBeRead,
-                                inCoverage);
+                                inCoverage,
+                                nullptr,
+                                0);
 }
 
 sk_sp<GrGeometryProcessor> GrDefaultGeoProcFactory::MakeForDeviceSpace(
@@ -330,3 +422,33 @@
     LocalCoords inverted(LocalCoords::kUsePosition_Type, &invert);
     return Make(color, coverage, inverted, SkMatrix::I());
 }
+
+sk_sp<GrGeometryProcessor> GrDefaultGeoProcFactory::MakeWithBones(const Color& color,
+                                                                  const Coverage& coverage,
+                                                                  const LocalCoords& localCoords,
+                                                                  const Bones& bones,
+                                                                  const SkMatrix& viewMatrix) {
+    uint32_t flags = 0;
+    if (Color::kPremulGrColorAttribute_Type == color.fType) {
+        flags |= kColorAttribute_GPFlag;
+    } else if (Color::kUnpremulSkColorAttribute_Type == color.fType) {
+        flags |= kColorAttribute_GPFlag | kColorAttributeIsSkColor_GPFlag;
+    }
+    flags |= coverage.fType == Coverage::kAttribute_Type ? kCoverageAttribute_GPFlag : 0;
+    flags |= localCoords.fType == LocalCoords::kHasExplicit_Type ? kLocalCoordAttribute_GPFlag : 0;
+    flags |= kBonesAttribute_GPFlag;
+
+    uint8_t inCoverage = coverage.fCoverage;
+    bool localCoordsWillBeRead = localCoords.fType != LocalCoords::kUnused_Type;
+
+    GrColor inColor = color.fColor;
+    return DefaultGeoProc::Make(flags,
+                                inColor,
+                                color.fColorSpaceXform,
+                                viewMatrix,
+                                localCoords.fMatrix ? *localCoords.fMatrix : SkMatrix::I(),
+                                localCoordsWillBeRead,
+                                inCoverage,
+                                bones.fBones,
+                                bones.fBoneCount);
+}
diff --git a/src/gpu/GrDefaultGeoProcFactory.h b/src/gpu/GrDefaultGeoProcFactory.h
index 314c1bb..c22cefb 100644
--- a/src/gpu/GrDefaultGeoProcFactory.h
+++ b/src/gpu/GrDefaultGeoProcFactory.h
@@ -54,7 +54,7 @@
         SkPoint fLocalCoord;
     };
 
-    struct PositionColorLocalCoordCoverage {
+    struct PositionColorLocalCoordCoverageAttr {
         SkPoint fPosition;
         GrColor fColor;
         SkPoint fLocalCoord;
@@ -118,6 +118,15 @@
         const SkMatrix* fMatrix;
     };
 
+    struct Bones {
+        Bones(const SkMatrix bones[], int boneCount)
+            : fBones(bones)
+            , fBoneCount(boneCount) {}
+
+        const SkMatrix* fBones;
+        int fBoneCount;
+    };
+
     sk_sp<GrGeometryProcessor> Make(const Color&,
                                     const Coverage&,
                                     const LocalCoords&,
@@ -132,6 +141,17 @@
                                                   const Coverage&,
                                                   const LocalCoords&,
                                                   const SkMatrix& viewMatrix);
+
+    /*
+     * Use this factory to create a GrGeometryProcessor that supports skeletal animation through
+     * deformation of vertices using matrices that are passed in. This should only be called from
+     * GrDrawVerticesOp.
+     */
+    sk_sp<GrGeometryProcessor> MakeWithBones(const Color&,
+                                             const Coverage&,
+                                             const LocalCoords&,
+                                             const Bones&,
+                                             const SkMatrix& viewMatrix);
 };
 
 #endif
diff --git a/src/gpu/GrRenderTargetContext.cpp b/src/gpu/GrRenderTargetContext.cpp
index 05b3c24..4030366 100644
--- a/src/gpu/GrRenderTargetContext.cpp
+++ b/src/gpu/GrRenderTargetContext.cpp
@@ -844,6 +844,8 @@
                                          GrPaint&& paint,
                                          const SkMatrix& viewMatrix,
                                          sk_sp<SkVertices> vertices,
+                                         const SkMatrix bones[],
+                                         int boneCount,
                                          GrPrimitiveType* overridePrimType) {
     ASSERT_SINGLE_OWNER
     RETURN_IF_ABANDONED
@@ -855,7 +857,7 @@
     SkASSERT(vertices);
     GrAAType aaType = this->chooseAAType(GrAA::kNo, GrAllowMixedSamples::kNo);
     std::unique_ptr<GrDrawOp> op = GrDrawVerticesOp::Make(
-            fContext, std::move(paint), std::move(vertices), viewMatrix, aaType,
+            fContext, std::move(paint), std::move(vertices), bones, boneCount, viewMatrix, aaType,
             this->colorSpaceInfo().refColorSpaceXformFromSRGB(), overridePrimType);
     this->addDrawOp(clip, std::move(op));
 }
diff --git a/src/gpu/GrRenderTargetContext.h b/src/gpu/GrRenderTargetContext.h
index 783b351..ee0d183 100644
--- a/src/gpu/GrRenderTargetContext.h
+++ b/src/gpu/GrRenderTargetContext.h
@@ -218,12 +218,16 @@
      * @param   paint            describes how to color pixels.
      * @param   viewMatrix       transformation matrix
      * @param   vertices         specifies the mesh to draw.
+     * @param   bones            bone deformation matrices.
+     * @param   boneCount        number of bone matrices.
      * @param   overridePrimType primitive type to draw. If NULL, derive prim type from vertices.
      */
     void drawVertices(const GrClip&,
                       GrPaint&& paint,
                       const SkMatrix& viewMatrix,
                       sk_sp<SkVertices> vertices,
+                      const SkMatrix bones[],
+                      int boneCount,
                       GrPrimitiveType* overridePrimType = nullptr);
 
     /**
diff --git a/src/gpu/SkGpuDevice.cpp b/src/gpu/SkGpuDevice.cpp
index a665517..f1e5270 100644
--- a/src/gpu/SkGpuDevice.cpp
+++ b/src/gpu/SkGpuDevice.cpp
@@ -365,7 +365,7 @@
                                                       nullptr);
 
     fRenderTargetContext->drawVertices(this->clip(), std::move(grPaint), *viewMatrix,
-                                       std::move(vertices), &primitiveType);
+                                       std::move(vertices), nullptr, 0, &primitiveType);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -1480,7 +1480,9 @@
 }
 
 void SkGpuDevice::wireframeVertices(SkVertices::VertexMode vmode, int vertexCount,
-                                    const SkPoint vertices[], SkBlendMode bmode,
+                                    const SkPoint vertices[],
+                                    const SkMatrix bones[], int boneCount,
+                                    SkBlendMode bmode,
                                     const uint16_t indices[], int indexCount,
                                     const SkPaint& paint) {
     ASSERT_SINGLE_OWNER
@@ -1538,12 +1540,13 @@
                                        std::move(grPaint),
                                        this->ctm(),
                                        builder.detach(),
+                                       bones,
+                                       boneCount,
                                        &primitiveType);
 }
 
-void SkGpuDevice::drawVertices(const SkVertices* vertices, const SkMatrix* bones, int boneCount,
+void SkGpuDevice::drawVertices(const SkVertices* vertices, const SkMatrix bones[], int boneCount,
                                SkBlendMode mode, const SkPaint& paint) {
-    // TODO: GPU ANIMATION
     ASSERT_SINGLE_OWNER
     GR_CREATE_TRACE_MARKER_CONTEXT("SkGpuDevice", "drawVertices", fContext.get());
 
@@ -1554,7 +1557,8 @@
     if ((!hasTexs || !paint.getShader()) && !hasColors) {
         // The dreaded wireframe mode. Fallback to drawVertices and go so slooooooow.
         this->wireframeVertices(vertices->mode(), vertices->vertexCount(), vertices->positions(),
-                                mode, vertices->indices(), vertices->indexCount(), paint);
+                                bones, boneCount, mode, vertices->indices(), vertices->indexCount(),
+                                paint);
         return;
     }
     if (!init_vertices_paint(fContext.get(), fRenderTargetContext->colorSpaceInfo(), paint,
@@ -1562,7 +1566,8 @@
         return;
     }
     fRenderTargetContext->drawVertices(this->clip(), std::move(grPaint), this->ctm(),
-                                       sk_ref_sp(const_cast<SkVertices*>(vertices)));
+                                       sk_ref_sp(const_cast<SkVertices*>(vertices)),
+                                       bones, boneCount);
 }
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/src/gpu/SkGpuDevice.h b/src/gpu/SkGpuDevice.h
index 48ba8ac..c31e624 100644
--- a/src/gpu/SkGpuDevice.h
+++ b/src/gpu/SkGpuDevice.h
@@ -91,7 +91,7 @@
                      int scalarsPerPos, const SkPoint& offset, const SkPaint&) override;
     void drawTextBlob(const SkTextBlob*, SkScalar x, SkScalar y,
                       const SkPaint& paint, SkDrawFilter* drawFilter) override;
-    void drawVertices(const SkVertices*, const SkMatrix* bones, int boneCount, SkBlendMode,
+    void drawVertices(const SkVertices*, const SkMatrix bones[], int boneCount, SkBlendMode,
                       const SkPaint&) override;
     void drawShadow(const SkPath&, const SkDrawShadowRec&) override;
     void drawAtlas(const SkImage* atlas, const SkRSXform[], const SkRect[],
@@ -248,7 +248,8 @@
     void drawStrokedLine(const SkPoint pts[2], const SkPaint&);
 
     void wireframeVertices(SkVertices::VertexMode, int vertexCount, const SkPoint verts[],
-                           SkBlendMode, const uint16_t indices[], int indexCount, const SkPaint&);
+                           const SkMatrix bones[], int boneCount, SkBlendMode,
+                           const uint16_t indices[], int indexCount, const SkPaint&);
 
     static sk_sp<GrRenderTargetContext> MakeRenderTargetContext(GrContext*,
                                                                 SkBudgeted,
diff --git a/src/gpu/ops/GrDrawVerticesOp.cpp b/src/gpu/ops/GrDrawVerticesOp.cpp
index ad5aba2..6abca9d 100644
--- a/src/gpu/ops/GrDrawVerticesOp.cpp
+++ b/src/gpu/ops/GrDrawVerticesOp.cpp
@@ -10,10 +10,13 @@
 #include "GrDefaultGeoProcFactory.h"
 #include "GrOpFlushState.h"
 #include "SkGr.h"
+#include "SkRectPriv.h"
 
 std::unique_ptr<GrDrawOp> GrDrawVerticesOp::Make(GrContext* context,
                                                  GrPaint&& paint,
                                                  sk_sp<SkVertices> vertices,
+                                                 const SkMatrix bones[],
+                                                 int boneCount,
                                                  const SkMatrix& viewMatrix,
                                                  GrAAType aaType,
                                                  sk_sp<GrColorSpaceXform> colorSpaceXform,
@@ -22,13 +25,14 @@
     GrPrimitiveType primType = overridePrimType ? *overridePrimType
                                                 : SkVertexModeToGrPrimitiveType(vertices->mode());
     return Helper::FactoryHelper<GrDrawVerticesOp>(context, std::move(paint), std::move(vertices),
-                                                   primType, aaType, std::move(colorSpaceXform),
-                                                   viewMatrix);
+                                                   bones, boneCount, primType, aaType,
+                                                   std::move(colorSpaceXform), viewMatrix);
 }
 
 GrDrawVerticesOp::GrDrawVerticesOp(const Helper::MakeArgs& helperArgs, GrColor color,
-                                   sk_sp<SkVertices> vertices, GrPrimitiveType primitiveType,
-                                   GrAAType aaType, sk_sp<GrColorSpaceXform> colorSpaceXform,
+                                   sk_sp<SkVertices> vertices, const SkMatrix bones[],
+                                   int boneCount, GrPrimitiveType primitiveType, GrAAType aaType,
+                                   sk_sp<GrColorSpaceXform> colorSpaceXform,
                                    const SkMatrix& viewMatrix)
         : INHERITED(ClassID())
         , fHelper(helperArgs, aaType)
@@ -45,15 +49,28 @@
     mesh.fColor = color;
     mesh.fViewMatrix = viewMatrix;
     mesh.fVertices = std::move(vertices);
+    if (bones) {
+        mesh.fBones.assign(bones, bones + boneCount);
+    }
     mesh.fIgnoreTexCoords = false;
     mesh.fIgnoreColors = false;
+    mesh.fIgnoreBones = false;
 
     fFlags = 0;
     if (mesh.hasPerVertexColors()) {
         fFlags |= kRequiresPerVertexColors_Flag;
     }
     if (mesh.hasExplicitLocalCoords()) {
-        fFlags |= kAnyMeshHasExplicitLocalCoords;
+        fFlags |= kAnyMeshHasExplicitLocalCoords_Flag;
+    }
+    if (mesh.hasBones()) {
+        fFlags |= kHasBones_Flag;
+    }
+
+    // Special case for meshes with a world transform but no bone weights.
+    // These will be considered normal vertices draws without bones.
+    if (!mesh.fVertices->hasBones() && boneCount == 1) {
+        mesh.fViewMatrix.preConcat(bones[0]);
     }
 
     IsZeroArea zeroArea;
@@ -62,7 +79,27 @@
     } else {
         zeroArea = IsZeroArea::kNo;
     }
-    this->setTransformedBounds(mesh.fVertices->bounds(), viewMatrix, HasAABloat::kNo, zeroArea);
+
+    if (this->hasBones()) {
+        // We don't know the bounds if there are deformations involved, so attempt to calculate
+        // the maximum possible.
+        SkRect bounds;
+        const SkRect& originalBounds = bones[0].mapRect(mesh.fVertices->bounds());
+        for (int i = 1; i < boneCount; i++) {
+            const SkMatrix& matrix = bones[i];
+            bounds.join(matrix.mapRect(originalBounds));
+        }
+
+        this->setTransformedBounds(bounds,
+                                   mesh.fViewMatrix,
+                                   HasAABloat::kNo,
+                                   zeroArea);
+    } else {
+        this->setTransformedBounds(mesh.fVertices->bounds(),
+                                   mesh.fViewMatrix,
+                                   HasAABloat::kNo,
+                                   zeroArea);
+    }
 }
 
 SkString GrDrawVerticesOp::dumpInfo() const {
@@ -96,13 +133,14 @@
     }
     if (!fHelper.usesLocalCoords()) {
         fMeshes[0].fIgnoreTexCoords = true;
-        fFlags &= ~kAnyMeshHasExplicitLocalCoords;
+        fFlags &= ~kAnyMeshHasExplicitLocalCoords_Flag;
     }
     return result;
 }
 
 sk_sp<GrGeometryProcessor> GrDrawVerticesOp::makeGP(bool* hasColorAttribute,
-                                                    bool* hasLocalCoordAttribute) const {
+                                                    bool* hasLocalCoordAttribute,
+                                                    bool* hasBoneAttribute) const {
     using namespace GrDefaultGeoProcFactory;
     LocalCoords::Type localCoordsType;
     if (fHelper.usesLocalCoords()) {
@@ -132,26 +170,53 @@
     } else {
         *hasColorAttribute = false;
     };
+
     const SkMatrix& vm = this->hasMultipleViewMatrices() ? SkMatrix::I() : fMeshes[0].fViewMatrix;
-    return GrDefaultGeoProcFactory::Make(color, Coverage::kSolid_Type, localCoordsType, vm);
+
+    Bones bones(fMeshes[0].fBones.data(), fMeshes[0].fBones.size());
+    *hasBoneAttribute = this->hasBones();
+
+    if (this->hasBones()) {
+        return GrDefaultGeoProcFactory::MakeWithBones(color,
+                                                      Coverage::kSolid_Type,
+                                                      localCoordsType,
+                                                      bones,
+                                                      vm);
+    } else {
+        return GrDefaultGeoProcFactory::Make(color,
+                                             Coverage::kSolid_Type,
+                                             localCoordsType,
+                                             vm);
+    }
 }
 
 void GrDrawVerticesOp::onPrepareDraws(Target* target) {
+    if (fMeshes[0].fVertices->isVolatile()) {
+        this->drawVolatile(target);
+    } else {
+        this->drawNonVolatile(target);
+    }
+}
+
+void GrDrawVerticesOp::drawVolatile(Target* target) {
     bool hasColorAttribute;
     bool hasLocalCoordsAttribute;
-    sk_sp<GrGeometryProcessor> gp = this->makeGP(&hasColorAttribute, &hasLocalCoordsAttribute);
+    bool hasBoneAttribute;
+    sk_sp<GrGeometryProcessor> gp = this->makeGP(&hasColorAttribute,
+                                                 &hasLocalCoordsAttribute,
+                                                 &hasBoneAttribute);
 
-    size_t vertexStride = sizeof(SkPoint) + (hasColorAttribute ? sizeof(uint32_t) : 0) +
-                          (hasLocalCoordsAttribute ? sizeof(SkPoint) : 0);
+    // Calculate the stride.
+    size_t vertexStride = sizeof(SkPoint) +
+                          (hasColorAttribute ? sizeof(uint32_t) : 0) +
+                          (hasLocalCoordsAttribute ? sizeof(SkPoint) : 0) +
+                          (hasBoneAttribute ? 4 * (sizeof(uint32_t) + sizeof(float)) : 0);
     SkASSERT(vertexStride == gp->debugOnly_vertexStride());
 
-    int instanceCount = fMeshes.count();
-
-    const GrBuffer* vertexBuffer;
-    int firstVertex;
-
+    // Allocate buffers.
+    const GrBuffer* vertexBuffer = nullptr;
+    int firstVertex = 0;
     void* verts = target->makeVertexSpace(vertexStride, fVertexCount, &vertexBuffer, &firstVertex);
-
     if (!verts) {
         SkDebugf("Could not allocate vertices\n");
         return;
@@ -159,37 +224,157 @@
 
     const GrBuffer* indexBuffer = nullptr;
     int firstIndex = 0;
-
     uint16_t* indices = nullptr;
     if (this->isIndexed()) {
         indices = target->makeIndexSpace(fIndexCount, &indexBuffer, &firstIndex);
-
         if (!indices) {
             SkDebugf("Could not allocate indices\n");
             return;
         }
     }
 
+    // Fill the buffers.
+    this->fillBuffers(hasColorAttribute,
+                      hasLocalCoordsAttribute,
+                      hasBoneAttribute,
+                      vertexStride,
+                      verts,
+                      indices);
+
+    // Draw the vertices.
+    this->drawVertices(target, gp.get(), vertexBuffer, firstVertex, indexBuffer, firstIndex);
+}
+
+void GrDrawVerticesOp::drawNonVolatile(Target* target) {
+    static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
+
+    bool hasColorAttribute;
+    bool hasLocalCoordsAttribute;
+    bool hasBoneAttribute;
+    sk_sp<GrGeometryProcessor> gp = this->makeGP(&hasColorAttribute,
+                                                 &hasLocalCoordsAttribute,
+                                                 &hasBoneAttribute);
+
+    int instanceCount = fMeshes.count();
+    SkASSERT(instanceCount == 1); // Non-volatile meshes should never combine.
+
+    // Get the resource provider.
+    GrResourceProvider* rp = target->resourceProvider();
+
+    // Generate keys for the buffers.
+    GrUniqueKey vertexKey, indexKey;
+    GrUniqueKey::Builder vertexKeyBuilder(&vertexKey, kDomain, instanceCount + 1);
+    GrUniqueKey::Builder indexKeyBuilder(&indexKey, kDomain, instanceCount + 1);
+    for (int i = 0; i < instanceCount; i ++) {
+        vertexKeyBuilder[i] = indexKeyBuilder[i] = fMeshes[i].fVertices->uniqueID();
+    }
+    vertexKeyBuilder[instanceCount] = 0;
+    indexKeyBuilder[instanceCount] = 1;
+    vertexKeyBuilder.finish();
+    indexKeyBuilder.finish();
+
+    // Try to grab data from the cache.
+    sk_sp<GrBuffer> vertexBuffer = rp->findByUniqueKey<GrBuffer>(vertexKey);
+    sk_sp<GrBuffer> indexBuffer = rp->findByUniqueKey<GrBuffer>(indexKey);
+
+    // Draw using the cached buffers if possible.
+    if (vertexBuffer) {
+        this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+        return;
+    }
+
+    // Calculate the stride.
+    size_t vertexStride = sizeof(SkPoint) +
+                          (hasColorAttribute ? sizeof(uint32_t) : 0) +
+                          (hasLocalCoordsAttribute ? sizeof(SkPoint) : 0) +
+                          (hasBoneAttribute ? 4 * (sizeof(uint32_t) + sizeof(float)) : 0);
+    SkASSERT(vertexStride == gp->debugOnly_vertexStride());
+
+    // Allocate vertex buffer.
+    vertexBuffer.reset(rp->createBuffer(fVertexCount * vertexStride,
+                                        kVertex_GrBufferType,
+                                        kStatic_GrAccessPattern,
+                                        0));
+    void* verts = vertexBuffer ? vertexBuffer->map() : nullptr;
+    if (!verts) {
+        SkDebugf("Could not allocate vertices\n");
+        return;
+    }
+
+    // Allocate index buffer.
+    uint16_t* indices = nullptr;
+    if (this->isIndexed()) {
+        indexBuffer.reset(rp->createBuffer(fIndexCount * sizeof(uint16_t),
+                                           kIndex_GrBufferType,
+                                           kStatic_GrAccessPattern,
+                                           0));
+        indices = indexBuffer ? static_cast<uint16_t*>(indexBuffer->map()) : nullptr;
+        if (!indices) {
+            SkDebugf("Could not allocate indices\n");
+            return;
+        }
+    }
+
+    // Fill the buffers.
+    this->fillBuffers(hasColorAttribute,
+                      hasLocalCoordsAttribute,
+                      hasBoneAttribute,
+                      vertexStride,
+                      verts,
+                      indices);
+
+    // Unmap the buffers.
+    vertexBuffer->unmap();
+    if (indexBuffer) {
+        indexBuffer->unmap();
+    }
+
+    // Cache the buffers.
+    rp->assignUniqueKeyToResource(vertexKey, vertexBuffer.get());
+    rp->assignUniqueKeyToResource(indexKey, indexBuffer.get());
+
+    // Draw the vertices.
+    this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+}
+
+void GrDrawVerticesOp::fillBuffers(bool hasColorAttribute,
+                                   bool hasLocalCoordsAttribute,
+                                   bool hasBoneAttribute,
+                                   size_t vertexStride,
+                                   void* verts,
+                                   uint16_t* indices) const {
+    int instanceCount = fMeshes.count();
+
+    // Copy data into the buffers.
     int vertexOffset = 0;
     // We have a fast case below for uploading the vertex data when the matrix is translate
-    // only and there are colors but not local coords.
-    bool fastAttrs = hasColorAttribute && !hasLocalCoordsAttribute;
+    // only and there are colors but not local coords. Fast case does not apply when there are bone
+    // transformations.
+    bool fastAttrs = hasColorAttribute && !hasLocalCoordsAttribute && !hasBoneAttribute;
     for (int i = 0; i < instanceCount; i++) {
+        // Get each mesh.
         const Mesh& mesh = fMeshes[i];
+
+        // Copy data into the index buffer.
         if (indices) {
             int indexCount = mesh.fVertices->indexCount();
             for (int j = 0; j < indexCount; ++j) {
                 *indices++ = mesh.fVertices->indices()[j] + vertexOffset;
             }
         }
+
+        // Copy data into the vertex buffer.
         int vertexCount = mesh.fVertices->vertexCount();
         const SkPoint* positions = mesh.fVertices->positions();
         const SkColor* colors = mesh.fVertices->colors();
         const SkPoint* localCoords = mesh.fVertices->texCoords();
+        const SkVertices::BoneIndices* boneIndices = mesh.fVertices->boneIndices();
+        const SkVertices::BoneWeights* boneWeights = mesh.fVertices->boneWeights();
         bool fastMesh = (!this->hasMultipleViewMatrices() ||
                          mesh.fViewMatrix.getType() <= SkMatrix::kTranslate_Mask) &&
                         mesh.hasPerVertexColors();
         if (fastAttrs && fastMesh) {
+            // Fast case.
             struct V {
                 SkPoint fPos;
                 uint32_t fColor;
@@ -207,9 +392,19 @@
             }
             verts = v + vertexCount;
         } else {
+            // Normal case.
             static constexpr size_t kColorOffset = sizeof(SkPoint);
-            size_t localCoordOffset =
-                    hasColorAttribute ? kColorOffset + sizeof(uint32_t) : kColorOffset;
+            size_t offset = kColorOffset;
+            if (hasColorAttribute) {
+                offset += sizeof(uint32_t);
+            }
+            size_t localCoordOffset = offset;
+            if (hasLocalCoordsAttribute) {
+                offset += sizeof(SkPoint);
+            }
+            size_t boneIndexOffset = offset;
+            offset += 4 * sizeof(uint32_t);
+            size_t boneWeightOffset = offset;
 
             for (int j = 0; j < vertexCount; ++j) {
                 if (this->hasMultipleViewMatrices()) {
@@ -231,22 +426,40 @@
                         *(SkPoint*)((intptr_t)verts + localCoordOffset) = positions[j];
                     }
                 }
+                if (hasBoneAttribute) {
+                    const SkVertices::BoneIndices& indices = boneIndices[j];
+                    const SkVertices::BoneWeights& weights = boneWeights[j];
+                    for (int k = 0; k < 4; k++) {
+                        size_t indexOffset = boneIndexOffset + sizeof(uint32_t) * k;
+                        size_t weightOffset = boneWeightOffset + sizeof(float) * k;
+                        *(uint32_t*)((intptr_t)verts + indexOffset) = indices.indices[k];
+                        *(float*)((intptr_t)verts + weightOffset) = weights.weights[k];
+                    }
+                }
                 verts = (void*)((intptr_t)verts + vertexStride);
             }
         }
         vertexOffset += vertexCount;
     }
+}
 
+void GrDrawVerticesOp::drawVertices(Target* target,
+                                    GrGeometryProcessor* gp,
+                                    const GrBuffer* vertexBuffer,
+                                    int firstVertex,
+                                    const GrBuffer* indexBuffer,
+                                    int firstIndex) {
     GrMesh mesh(this->primitiveType());
-    if (!indices) {
-        mesh.setNonIndexedNonInstanced(fVertexCount);
-    } else {
-        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertexCount - 1,
+    if (this->isIndexed()) {
+        mesh.setIndexed(indexBuffer, fIndexCount,
+                        firstIndex, 0, fVertexCount - 1,
                         GrPrimitiveRestart::kNo);
+    } else {
+        mesh.setNonIndexedNonInstanced(fVertexCount);
     }
     mesh.setVertexData(vertexBuffer, firstVertex);
     auto pipe = fHelper.makePipeline(target);
-    target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+    target->draw(gp, pipe.fPipeline, pipe.fFixedDynamicState, mesh);
 }
 
 bool GrDrawVerticesOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
@@ -256,6 +469,20 @@
         return false;
     }
 
+    // Meshes with bones cannot be combined because different meshes use different bones, so to
+    // combine them, the matrices would have to be combined, and the bone indices on each vertex
+    // would change, thus making the vertices uncacheable.
+    if (this->hasBones() || that->hasBones()) {
+        return false;
+    }
+
+    // Non-volatile meshes cannot batch, because if a non-volatile mesh batches with another mesh,
+    // then on the next frame, if that non-volatile mesh is drawn, it will draw the other mesh
+    // that was saved in its vertex buffer, which is not necessarily there anymore.
+    if (!this->fMeshes[0].fVertices->isVolatile() || !that->fMeshes[0].fVertices->isVolatile()) {
+        return false;
+    }
+
     if (!this->combinablePrimitive() || this->primitiveType() != that->primitiveType()) {
         return false;
     }
@@ -410,8 +637,8 @@
     if (GrFSAAType::kUnifiedMSAA == fsaaType && random->nextBool()) {
         aaType = GrAAType::kMSAA;
     }
-    return GrDrawVerticesOp::Make(context, std::move(paint), std::move(vertices), viewMatrix,
-                                  aaType, std::move(colorSpaceXform), &type);
+    return GrDrawVerticesOp::Make(context, std::move(paint), std::move(vertices), nullptr, 0,
+                                  viewMatrix, aaType, std::move(colorSpaceXform), &type);
 }
 
 #endif
diff --git a/src/gpu/ops/GrDrawVerticesOp.h b/src/gpu/ops/GrDrawVerticesOp.h
index 6d35c0d..2e10fef 100644
--- a/src/gpu/ops/GrDrawVerticesOp.h
+++ b/src/gpu/ops/GrDrawVerticesOp.h
@@ -38,13 +38,15 @@
     static std::unique_ptr<GrDrawOp> Make(GrContext* context,
                                           GrPaint&&,
                                           sk_sp<SkVertices>,
+                                          const SkMatrix bones[],
+                                          int boneCount,
                                           const SkMatrix& viewMatrix,
                                           GrAAType,
                                           sk_sp<GrColorSpaceXform>,
                                           GrPrimitiveType* overridePrimType = nullptr);
 
-    GrDrawVerticesOp(const Helper::MakeArgs& helperArgs, GrColor, sk_sp<SkVertices>,
-                     GrPrimitiveType, GrAAType, sk_sp<GrColorSpaceXform>,
+    GrDrawVerticesOp(const Helper::MakeArgs&, GrColor, sk_sp<SkVertices>, const SkMatrix bones[],
+                     int boneCount, GrPrimitiveType, GrAAType, sk_sp<GrColorSpaceXform>,
                      const SkMatrix& viewMatrix);
 
     const char* name() const override { return "DrawVerticesOp"; }
@@ -68,7 +70,26 @@
 
     void onPrepareDraws(Target*) override;
 
-    sk_sp<GrGeometryProcessor> makeGP(bool* hasColorAttribute, bool* hasLocalCoordAttribute) const;
+    void drawVolatile(Target*);
+    void drawNonVolatile(Target*);
+
+    void fillBuffers(bool hasColorAttribute,
+                     bool hasLocalCoordsAttribute,
+                     bool hasBoneAttribute,
+                     size_t vertexStride,
+                     void* verts,
+                     uint16_t* indices) const;
+
+    void drawVertices(Target*,
+                      GrGeometryProcessor*,
+                      const GrBuffer* vertexBuffer,
+                      int firstVertex,
+                      const GrBuffer* indexBuffer,
+                      int firstIndex);
+
+    sk_sp<GrGeometryProcessor> makeGP(bool* hasColorAttribute,
+                                      bool* hasLocalCoordAttribute,
+                                      bool* hasBoneAttribute) const;
 
     GrPrimitiveType primitiveType() const { return fPrimitiveType; }
     bool combinablePrimitive() const {
@@ -82,9 +103,11 @@
     struct Mesh {
         GrColor fColor;  // Used if this->hasPerVertexColors() is false.
         sk_sp<SkVertices> fVertices;
+        std::vector<SkMatrix> fBones;
         SkMatrix fViewMatrix;
         bool fIgnoreTexCoords;
         bool fIgnoreColors;
+        bool fIgnoreBones;
 
         bool hasExplicitLocalCoords() const {
             return fVertices->hasTexCoords() && !fIgnoreTexCoords;
@@ -93,6 +116,10 @@
         bool hasPerVertexColors() const {
             return fVertices->hasColors() && !fIgnoreColors;
         }
+
+        bool hasBones() const {
+            return fVertices->hasBones() && fBones.size() && !fIgnoreBones;
+        }
     };
 
     bool isIndexed() const {
@@ -105,18 +132,22 @@
     }
 
     bool anyMeshHasExplicitLocalCoords() const {
-        return SkToBool(kAnyMeshHasExplicitLocalCoords & fFlags);
+        return SkToBool(kAnyMeshHasExplicitLocalCoords_Flag & fFlags);
     }
 
     bool hasMultipleViewMatrices() const {
         return SkToBool(kHasMultipleViewMatrices_Flag & fFlags);
     }
 
-    enum Flags {
-        kRequiresPerVertexColors_Flag = 0x1,
-        kAnyMeshHasExplicitLocalCoords = 0x2,
-        kHasMultipleViewMatrices_Flag = 0x4
+    bool hasBones() const {
+        return SkToBool(kHasBones_Flag & fFlags);
+    }
 
+    enum Flags {
+        kRequiresPerVertexColors_Flag       = 0x1,
+        kAnyMeshHasExplicitLocalCoords_Flag = 0x2,
+        kHasMultipleViewMatrices_Flag       = 0x4,
+        kHasBones_Flag                      = 0x8,
     };
 
     Helper fHelper;