Add a dynamic color attrib to hw tessellated stroking

Only adds color to the hardware tessellator. The indirect tessellator
reorders draws with is log2 binning, so we can't have different
colors.

Bug: chromium:1172543
Bug: skia:10419
Change-Id: I2a3700cd4572e8222002bfb028af05c6ec447708
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/369976
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/bench/TessellateBench.cpp b/bench/TessellateBench.cpp
index b1bdbeb..6d1e6fa 100644
--- a/bench/TessellateBench.cpp
+++ b/bench/TessellateBench.cpp
@@ -231,7 +231,8 @@
             SkMatrix matrix = SkMatrix::Scale(fMatrixScale, fMatrixScale);
             GrStrokeHardwareTessellator tessellator(ShaderFlags::kNone,
                                                     *fTarget->caps().shaderCaps());
-            tessellator.prepare(fTarget.get(), matrix, {fPath, fStrokeRec}, fPath.countVerbs());
+            tessellator.prepare(fTarget.get(), matrix, {fPath, fStrokeRec, SK_PMColor4fWHITE},
+                                fPath.countVerbs());
         }
     }
 
@@ -270,10 +271,10 @@
         for (int i = 0; i < loops; ++i) {
             for (const SkPath& path : fPaths) {
                 GrStrokeIndirectTessellator tessellator(ShaderFlags::kNone, SkMatrix::I(),
-                                                        {path, fStrokeRec}, path.countVerbs(),
-                                                        fTarget->allocator());
-                tessellator.prepare(fTarget.get(), SkMatrix::I(), {path, fStrokeRec},
-                                    path.countVerbs());
+                                                        {path, fStrokeRec, SK_PMColor4fWHITE},
+                                                        path.countVerbs(), fTarget->allocator());
+                tessellator.prepare(fTarget.get(), SkMatrix::I(),
+                                    {path, fStrokeRec, SK_PMColor4fWHITE}, path.countVerbs());
             }
             fTarget->resetAllocator();
         }
diff --git a/src/gpu/GrColor.h b/src/gpu/GrColor.h
index 26f0937..f9bab67 100644
--- a/src/gpu/GrColor.h
+++ b/src/gpu/GrColor.h
@@ -92,13 +92,19 @@
  */
 class GrVertexColor {
 public:
-    explicit GrVertexColor(const SkPMColor4f& color, bool wideColor)
-            : fWideColor(wideColor) {
+    GrVertexColor() = default;
+
+    explicit GrVertexColor(const SkPMColor4f& color, bool wideColor) {
+        this->set(color, wideColor);
+    }
+
+    void set(const SkPMColor4f& color, bool wideColor) {
         if (wideColor) {
             memcpy(fColor, color.vec(), sizeof(fColor));
         } else {
             fColor[0] = color.toBytes_RGBA();
         }
+        fWideColor = wideColor;
     }
 
     size_t size() const { return fWideColor ? 16 : 4; }
diff --git a/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
index 8211e8c..31dc3b0 100644
--- a/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
@@ -121,10 +121,17 @@
             auto tolerances = Tolerances::MakePreTransform(matrixScales.data(), stroke.getWidth());
             this->updateTolerances(tolerances, stroke.getJoin());
         }
+        fStroke = &stroke;
+
         if (fShaderFlags & ShaderFlags::kDynamicStroke) {
             fDynamicStroke.set(stroke);
         }
-        fStroke = &stroke;
+        if (fShaderFlags & ShaderFlags::kDynamicColor) {
+            bool wideColor = fShaderFlags & ShaderFlags::kWideColor;
+            SkASSERT(wideColor || pathStroke.fColor.fitsInBytes());
+            fDynamicColor.set(pathStroke.fColor, wideColor);
+        }
+
         fHasLastControlPoint = false;
         SkDEBUGCODE(fHasCurrentPoint = false;)
         SkPathVerb previousVerb = SkPathVerb::kClose;
@@ -655,6 +662,9 @@
     if (fShaderFlags & ShaderFlags::kDynamicStroke) {
         fPatchWriter.write(fDynamicStroke);
     }
+    if (fShaderFlags & ShaderFlags::kDynamicColor) {
+        fPatchWriter.write(fDynamicColor);
+    }
 }
 
 bool GrStrokeHardwareTessellator::reservePatch() {
diff --git a/src/gpu/tessellate/GrStrokeHardwareTessellator.h b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
index 3488bd8..20766a6 100644
--- a/src/gpu/tessellate/GrStrokeHardwareTessellator.h
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
@@ -125,6 +125,7 @@
 
     // Stateful values for the dynamic state (if any) that will get written out with each patch.
     GrStrokeTessellateShader::DynamicStroke fDynamicStroke;
+    GrVertexColor fDynamicColor;
 
     friend class GrOp;  // For ctor.
 
diff --git a/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp b/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
index 63fc2fb..15ed2a9 100644
--- a/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
+++ b/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
@@ -444,6 +444,8 @@
         const GrSTArenaList<PathStroke>& pathStrokeList, int totalCombinedVerbCnt,
         SkArenaAlloc* alloc)
         : GrStrokeTessellator(shaderFlags) {
+    // We can't combine colors because our log2 binning draws things out of order.
+    SkASSERT(!(fShaderFlags & ShaderFlags::kDynamicColor));
     SkASSERT(!fTotalInstanceCount);
     SkASSERT(!fResolveLevels);
     SkASSERT(!fResolveLevelArrayCount);
diff --git a/src/gpu/tessellate/GrStrokeTessellateOp.cpp b/src/gpu/tessellate/GrStrokeTessellateOp.cpp
index 0af849e..5885e10 100644
--- a/src/gpu/tessellate/GrStrokeTessellateOp.cpp
+++ b/src/gpu/tessellate/GrStrokeTessellateOp.cpp
@@ -20,13 +20,15 @@
         : GrDrawOp(ClassID())
         , fAAType(aaType)
         , fViewMatrix(viewMatrix)
-        , fColor(paint.getColor4f())
-        , fProcessors(std::move(paint))
-        , fPathStrokeList(path, stroke)
-        , fTotalCombinedVerbCnt(path.countVerbs()) {
+        , fPathStrokeList(path, stroke, paint.getColor4f())
+        , fTotalCombinedVerbCnt(path.countVerbs())
+        , fProcessors(std::move(paint)) {
     if (SkPathPriv::ConicWeightCnt(path) != 0) {
         fShaderFlags |= ShaderFlags::kHasConics;
     }
+    if (!this->headColor().fitsInBytes()) {
+        fShaderFlags |= ShaderFlags::kWideColor;
+    }
     SkRect devBounds = path.getBounds();
     float inflationRadius = stroke.getInflationRadius();
     devBounds.outset(inflationRadius, inflationRadius);
@@ -62,21 +64,21 @@
     SkASSERT(fPathStrokeList.begin().fCurr->fNext == nullptr);
     SkASSERT(fAAType != GrAAType::kCoverage || hasMixedSampledCoverage);
     const GrProcessorSet::Analysis& analysis = fProcessors.finalize(
-            fColor, GrProcessorAnalysisCoverage::kNone, clip, &GrUserStencilSettings::kUnused,
-            hasMixedSampledCoverage, caps, clampType, &fColor);
+            this->headColor(), GrProcessorAnalysisCoverage::kNone, clip,
+            &GrUserStencilSettings::kUnused, hasMixedSampledCoverage, caps, clampType,
+            &this->headColor());
     fNeedsStencil = !analysis.unaffectedByDstValue();
     return analysis;
 }
 
 GrOp::CombineResult GrStrokeTessellateOp::onCombineIfPossible(GrOp* grOp, SkArenaAlloc* alloc,
-                                                              const GrCaps&) {
+                                                              const GrCaps& caps) {
     using DynamicStroke = GrStrokeTessellateShader::DynamicStroke;
     SkASSERT(grOp->classID() == this->classID());
     auto* op = static_cast<GrStrokeTessellateOp*>(grOp);
 
     if (fNeedsStencil ||
         op->fNeedsStencil ||
-        fColor != op->fColor ||
         fViewMatrix != op->fViewMatrix ||
         fAAType != op->fAAType ||
         fProcessors != op->fProcessors ||
@@ -91,14 +93,29 @@
         // still decide to combine them.
         combinedFlags |= ShaderFlags::kDynamicStroke;
     }
-    if (combinedFlags & ShaderFlags::kDynamicStroke) {
-        // Don't actually enable dynamic stroke on ops that already have lots of verbs.
-        if (!this->shouldUseDynamicState(ShaderFlags::kDynamicStroke) ||
-            !op->shouldUseDynamicState(ShaderFlags::kDynamicStroke)) {
+    if (!(combinedFlags & ShaderFlags::kDynamicColor) && this->headColor() != op->headColor()) {
+        // The paths have different colors. We will need to enable dynamic color if we still decide
+        // to combine them.
+        combinedFlags |= ShaderFlags::kDynamicColor;
+    }
+
+    // Don't actually enable new dynamic state on ops that already have lots of verbs.
+    constexpr static GrTFlagsMask<ShaderFlags> kDynamicStatesMask(ShaderFlags::kDynamicStroke |
+                                                                  ShaderFlags::kDynamicColor);
+    ShaderFlags neededDynamicStates = combinedFlags & kDynamicStatesMask;
+    if (neededDynamicStates != ShaderFlags::kNone) {
+        if (!this->shouldUseDynamicStates(neededDynamicStates) ||
+            !op->shouldUseDynamicStates(neededDynamicStates)) {
             return CombineResult::kCannotCombine;
         }
     }
 
+    // The indirect tessellator can't combine colors because its log2 binning draws things out of
+    // order. Only enable dynamic color if we have hardware tessellation.
+    if ((combinedFlags & ShaderFlags::kDynamicColor) && !this->canUseHardwareTessellation(caps)) {
+        return CombineResult::kCannotCombine;
+    }
+
     fPathStrokeList.concat(std::move(op->fPathStrokeList), alloc);
     fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
     fShaderFlags = combinedFlags;
@@ -136,14 +153,11 @@
     const GrCaps& caps = *args.fCaps;
     SkArenaAlloc* arena = args.fArena;
 
-    // Only use hardware tessellation if the path has a somewhat large number of verbs. Otherwise we
-    // seem to be better off using indirect draws. Our back door for HW tessellation shaders isn't
-    // currently capable of passing varyings to the fragment shader either, so if the processors
-    // have varyings we need to use indirect draws.
+    // Only use hardware tessellation if we need dynamic color or if the path has a somewhat large
+    // number of verbs. Otherwise we seem to be better off using indirect draws.
     GrStrokeTessellateShader::Mode shaderMode;
-    if (caps.shaderCaps()->tessellationSupport() &&
-        fTotalCombinedVerbCnt > 50 &&
-        !fProcessors.usesVaryingCoords()) {
+    if (this->canUseHardwareTessellation(caps) &&
+        ((fShaderFlags & ShaderFlags::kDynamicColor) || fTotalCombinedVerbCnt > 50)) {
         fTessellator = arena->make<GrStrokeHardwareTessellator>(fShaderFlags, *caps.shaderCaps());
         shaderMode = GrStrokeTessellateShader::Mode::kTessellation;
     } else {
@@ -164,7 +178,7 @@
     }
 
     auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
-            shaderMode, fShaderFlags, fViewMatrix, this->headStroke(), fColor);
+            shaderMode, fShaderFlags, fViewMatrix, this->headStroke(), this->headColor());
     auto* fillPipeline = GrFillPathShader::MakeFillPassPipeline(args, fAAType, std::move(clip),
                                                                 std::move(fProcessors));
     auto fillStencil = &GrUserStencilSettings::kUnused;
diff --git a/src/gpu/tessellate/GrStrokeTessellateOp.h b/src/gpu/tessellate/GrStrokeTessellateOp.h
index 2278fbc..42b8d45 100644
--- a/src/gpu/tessellate/GrStrokeTessellateOp.h
+++ b/src/gpu/tessellate/GrStrokeTessellateOp.h
@@ -24,9 +24,11 @@
     GrStrokeTessellator(ShaderFlags shaderFlags) : fShaderFlags(shaderFlags) {}
 
     struct PathStroke {
-        PathStroke(const SkPath& path, const SkStrokeRec& stroke) : fPath(path), fStroke(stroke) {}
+        PathStroke(const SkPath& path, const SkStrokeRec& stroke, const SkPMColor4f& color)
+                : fPath(path), fStroke(stroke), fColor(color) {}
         SkPath fPath;
         SkStrokeRec fStroke;
+        SkPMColor4f fColor;
     };
 
     // Called before draw(). Prepares GPU buffers containing the geometry to tessellate.
@@ -55,16 +57,25 @@
     DEFINE_OP_CLASS_ID
 
     SkStrokeRec& headStroke() { return fPathStrokeList.head().fStroke; }
+    SkPMColor4f& headColor() { return fPathStrokeList.head().fColor; }
 
-    // Returns whether it is a good tradeoff to use the given dynamic state. Dynamic state improves
-    // batching, but if it isn't already enabled, it comes at the cost of having to write out more
-    // data with each patch or instance.
-    bool shouldUseDynamicState(ShaderFlags dynamicState) const {
-        // Use the dynamic state if either (1) the state is already enabled anyway, or (2) we don't
+    // Returns whether it is a good tradeoff to use the dynamic states flagged in the given
+    // bitfield. Dynamic states improve batching, but if they aren't already enabled, they come at
+    // the cost of having to write out more data with each patch or instance.
+    bool shouldUseDynamicStates(ShaderFlags neededDynamicStates) const {
+        // Use the dynamic states if either (1) they are all already enabled anyway, or (2) we don't
         // have many verbs.
         constexpr static int kMaxVerbsToEnableDynamicState = 50;
-        return (fShaderFlags & dynamicState) ||
-               (fTotalCombinedVerbCnt <= kMaxVerbsToEnableDynamicState);
+        bool anyStateDisabled = (bool)(~fShaderFlags & neededDynamicStates);
+        bool allStatesEnabled = !anyStateDisabled;
+        return allStatesEnabled || (fTotalCombinedVerbCnt <= kMaxVerbsToEnableDynamicState);
+    }
+
+    bool canUseHardwareTessellation(const GrCaps& caps) {
+        SkASSERT(!fStencilProgram && !fFillProgram);  // Ensure we haven't std::moved fProcessors.
+        // Our back door for HW tessellation shaders isn't currently capable of passing varyings to
+        // the fragment shader, so if the processors have varyings we need to use indirect draws.
+        return caps.shaderCaps()->tessellationSupport() && !fProcessors.usesVaryingCoords();
     }
 
     const char* name() const override { return "GrStrokeTessellateOp"; }
@@ -87,13 +98,11 @@
 
     const GrAAType fAAType;
     const SkMatrix fViewMatrix;
-    SkPMColor4f fColor;
-    bool fNeedsStencil = false;
-    GrProcessorSet fProcessors;
-
     ShaderFlags fShaderFlags = ShaderFlags::kNone;
     GrSTArenaList<PathStroke> fPathStrokeList;
     int fTotalCombinedVerbCnt = 0;
+    GrProcessorSet fProcessors;
+    bool fNeedsStencil = false;
 
     GrStrokeTessellator* fTessellator = nullptr;
     const GrProgramInfo* fStencilProgram = nullptr;  // Only used if the stroke has transparency.
diff --git a/src/gpu/tessellate/GrStrokeTessellateShader.cpp b/src/gpu/tessellate/GrStrokeTessellateShader.cpp
index cdc9eee..73d81d4 100644
--- a/src/gpu/tessellate/GrStrokeTessellateShader.cpp
+++ b/src/gpu/tessellate/GrStrokeTessellateShader.cpp
@@ -135,6 +135,9 @@
             // [NUM_RADIAL_SEGMENTS_PER_RADIAN, STROKE_RADIUS]
             v->declareGlobal(GrShaderVar("vsStrokeArgs", kFloat2_GrSLType, TypeModifier::Out));
         }
+        if (shader.hasDynamicColor()) {
+            v->declareGlobal(GrShaderVar("vsColor", kHalf4_GrSLType, TypeModifier::Out));
+        }
 
         v->insertFunction(kAtan2Fn);
         v->insertFunction(kCosineBetweenVectorsFn);
@@ -380,12 +383,25 @@
             v->codeAppend(R"(
             vsStrokeArgs = float2(NUM_RADIAL_SEGMENTS_PER_RADIAN, STROKE_RADIUS);)");
         }
+        if (shader.hasDynamicColor()) {
+            v->codeAppend(R"(
+            vsColor = dynamicColorAttr;)");
+        }
 
-        // The fragment shader just outputs a uniform color.
-        const char* colorUniformName;
-        fColorUniform = uniHandler->addUniform(nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType,
-                                               "color", &colorUniformName);
-        args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
+        if (!shader.hasDynamicColor()) {
+            // The fragment shader just outputs a uniform color.
+            const char* colorUniformName;
+            fColorUniform = uniHandler->addUniform(nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType,
+                                                   "color", &colorUniformName);
+            args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
+        } else {
+            // Color gets passed in from the tess evaluation shader.
+            SkString flatness(args.fShaderCaps->preferFlatInterpolation() ? "flat" : "");
+            args.fFragBuilder->declareGlobal(GrShaderVar(SkString("tesColor"), kHalf4_GrSLType,
+                                                         TypeModifier::In, 0, SkString(),
+                                                         flatness));
+            args.fFragBuilder->codeAppendf("%s = tesColor;", args.fOutputColor);
+        }
         args.fFragBuilder->codeAppendf("%s = half4(1);", args.fOutputCoverage);
     }
 
@@ -423,7 +439,9 @@
                         m.getScaleY());
         }
 
-        pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
+        if (!shader.hasDynamicColor()) {
+            pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
+        }
     }
 
     GrGLSLUniformHandler::UniformHandle fTessArgsUniform;
@@ -487,6 +505,10 @@
         code.append(R"(
         in vec2 vsStrokeArgs[];)");
     }
+    if (this->hasDynamicColor()) {
+        code.append(R"(
+        in mediump vec4 vsColor[];)");
+    }
 
     code.append(R"(
     out vec4 tcsPts01[];
@@ -500,6 +522,10 @@
         code.append(R"(
         patch out float tcsStrokeRadius;)");
     }
+    if (this->hasDynamicColor()) {
+        code.append(R"(
+        patch out mediump vec4 tcsColor;)");
+    }
 
     code.append(R"(
     void main() {
@@ -510,6 +536,10 @@
         code.append(R"(
         tcsStrokeRadius = vsStrokeArgs[0].y;)");
     }
+    if (this->hasDynamicColor()) {
+        code.append(R"(
+        tcsColor = vsColor[0];)");
+    }
 
     code.append(R"(
         // Unpack the curve args from the vertex shader.
@@ -831,6 +861,11 @@
         code.append(R"(
         patch in float tcsStrokeRadius;)");
     }
+    if (this->hasDynamicColor()) {
+        code.appendf(R"(
+        patch in mediump vec4 tcsColor;
+        %s out mediump vec4 tesColor;)", shaderCaps.preferFlatInterpolation() ? "flat" : "");
+    }
 
     code.append(R"(
     uniform vec4 sk_RTAdjust;)");
@@ -932,7 +967,14 @@
     }
 
     code.append(R"(
-        gl_Position = vec4(vertexPos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
+        gl_Position = vec4(vertexPos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);)");
+
+    if (this->hasDynamicColor()) {
+        code.append(R"(
+        tesColor = tcsColor;)");
+    }
+
+    code.append(R"(
     })");
 
     return code;
@@ -1242,9 +1284,11 @@
 
 void GrStrokeTessellateShader::getGLSLProcessorKey(const GrShaderCaps&,
                                                    GrProcessorKeyBuilder* b) const {
-    bool keyNeedsJoin = fMode == Mode::kIndirect && !(fShaderFlags & ShaderFlags::kDynamicStroke);
+    bool keyNeedsJoin = (fMode == Mode::kIndirect) && !(fShaderFlags & ShaderFlags::kDynamicStroke);
     SkASSERT(fStroke.getJoin() >> 2 == 0);
-    uint32_t key = (uint32_t)fShaderFlags;
+    // Attribs get worked into the key automatically during GrPrimitiveProcessor::getAttributeKey().
+    // When color is in a uniform, it's always wide. kWideColor doesn't need to be considered here.
+    uint32_t key = (uint32_t)(fShaderFlags & ~ShaderFlags::kWideColor);
     key = (key << 1) | (uint32_t)fMode;
     key = (key << 2) | ((keyNeedsJoin) ? fStroke.getJoin() : 0);
     key = (key << 1) | (uint32_t)fStroke.isHairlineStyle();
diff --git a/src/gpu/tessellate/GrStrokeTessellateShader.h b/src/gpu/tessellate/GrStrokeTessellateShader.h
index d146917..d491d1d 100644
--- a/src/gpu/tessellate/GrStrokeTessellateShader.h
+++ b/src/gpu/tessellate/GrStrokeTessellateShader.h
@@ -34,7 +34,9 @@
     enum class ShaderFlags {
         kNone          = 0,
         kHasConics     = 1 << 0,
-        kDynamicStroke = 1 << 1  // Each patch or instance has its own stroke width and join type.
+        kWideColor     = 1 << 1,
+        kDynamicStroke = 1 << 2,  // Each patch or instance has its own stroke width and join type.
+        kDynamicColor  = 1 << 3,  // Each patch or instance has its own color.
     };
 
     GR_DECL_BITFIELD_CLASS_OPS_FRIENDS(ShaderFlags);
@@ -142,19 +144,23 @@
 
     // Size in bytes of a tessellation patch with the given shader flags.
     static size_t PatchStride(ShaderFlags shaderFlags) {
-        size_t stride = sizeof(SkPoint) * 5;
-        if (shaderFlags & ShaderFlags::kDynamicStroke) {
-            stride += sizeof(DynamicStroke);
-        }
-        return stride;
+        return sizeof(SkPoint) * 5 + DynamicStateStride(shaderFlags);
     }
 
     // Size in bytes of an indirect draw instance with the given shader flags.
     static size_t IndirectInstanceStride(ShaderFlags shaderFlags) {
-        size_t stride = sizeof(float) * 11;
+        return sizeof(float) * 11 + DynamicStateStride(shaderFlags);
+    }
+
+    // Combined size in bytes of the dynamic state attribs enabled in the given shader flags.
+    static size_t DynamicStateStride(ShaderFlags shaderFlags) {
+        size_t stride = 0;
         if (shaderFlags & ShaderFlags::kDynamicStroke) {
             stride += sizeof(DynamicStroke);
         }
+        if (shaderFlags & ShaderFlags::kDynamicColor) {
+            stride += (shaderFlags & ShaderFlags::kWideColor) ? sizeof(float) * 4 : 4;
+        }
         return stride;
     }
 
@@ -207,6 +213,13 @@
             fAttribs.emplace_back("dynamicStrokeAttr", kFloat2_GrVertexAttribType,
                                   kFloat2_GrSLType);
         }
+        if (fShaderFlags & ShaderFlags::kDynamicColor) {
+            fAttribs.emplace_back("dynamicColorAttr",
+                                  (fShaderFlags & ShaderFlags::kWideColor)
+                                          ? kFloat4_GrVertexAttribType
+                                          : kUByte4_norm_GrVertexAttribType,
+                                  kHalf4_GrSLType);
+        }
         if (fMode == Mode::kTessellation) {
             this->setVertexAttributes(fAttribs.data(), fAttribs.count());
             SkASSERT(this->vertexStride() == PatchStride(fShaderFlags));
@@ -214,10 +227,12 @@
             this->setInstanceAttributes(fAttribs.data(), fAttribs.count());
             SkASSERT(this->instanceStride() == IndirectInstanceStride(fShaderFlags));
         }
+        SkASSERT(fAttribs.count() <= kMaxAttribCount);
     }
 
     bool hasConics() const { return fShaderFlags & ShaderFlags::kHasConics; }
     bool hasDynamicStroke() const { return fShaderFlags & ShaderFlags::kDynamicStroke; }
+    bool hasDynamicColor() const { return fShaderFlags & ShaderFlags::kDynamicColor; }
 
 private:
     const char* name() const override { return "GrStrokeTessellateShader"; }
@@ -237,7 +252,9 @@
     const ShaderFlags fShaderFlags;
     const SkStrokeRec fStroke;
     const SkPMColor4f fColor;
-    SkSTArray<4, Attribute> fAttribs;
+
+    constexpr static int kMaxAttribCount = 5;
+    SkSTArray<kMaxAttribCount, Attribute> fAttribs;
 
     class TessellationImpl;
     class IndirectImpl;
diff --git a/tests/StrokeIndirectTest.cpp b/tests/StrokeIndirectTest.cpp
index f955e24..39135a0 100644
--- a/tests/StrokeIndirectTest.cpp
+++ b/tests/StrokeIndirectTest.cpp
@@ -42,10 +42,11 @@
             float scale = ldexpf(rand.nextF() + 1, i);
             auto matrix = SkMatrix::Scale(scale, scale);
             GrStrokeIndirectTessellator tessellator(GrStrokeTessellateShader::ShaderFlags::kNone,
-                                                    matrix, {path, stroke}, path.countVerbs(),
-                                                    target->allocator());
+                                                    matrix, {path, stroke, SK_PMColor4fWHITE},
+                                                    path.countVerbs(), target->allocator());
             tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
-            tessellator.prepare(target, matrix, {path, stroke}, path.countVerbs());
+            tessellator.prepare(target, matrix, {path, stroke, SK_PMColor4fWHITE},
+                                path.countVerbs());
             tessellator.verifyBuffers(r, target, matrix, stroke);
         }
     }