Add support for explicit attribute offsets and strides.

Previously attribute offsets were always computed based on their
position in an attribute array and the stride was determined
by the offset and size of the last attribute.

Now a GP has the option to create attributes with explicit offsets
and specify an explicit vertex stride. All attributes must either
be implicit or explicit (enforced by assert).

GrGeometryProcessor::AttributeSet is now responsible for handling
implicitly determined attribute offsets and strides. The backends
no longer compute them.

Bug: skia:12720
Change-Id: I0211673dc70d4797c2d66b2555d8f5fb430be056
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/484736
Reviewed-by: Greg Daniel <egdaniel@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
diff --git a/bench/VertexColorSpaceBench.cpp b/bench/VertexColorSpaceBench.cpp
index 4ec5bb4..e8b199b 100644
--- a/bench/VertexColorSpaceBench.cpp
+++ b/bench/VertexColorSpaceBench.cpp
@@ -117,7 +117,7 @@
                 fInColor = {"inColor", kHalf4_GrVertexAttribType, kHalf4_GrSLType};
                 break;
         }
-        this->setVertexAttributes(&fInPosition, 2);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 2);
     }
 
     Mode fMode;
diff --git a/gm/attributes.cpp b/gm/attributes.cpp
new file mode 100644
index 0000000..9ee73f1
--- /dev/null
+++ b/gm/attributes.cpp
@@ -0,0 +1,300 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm/gm.h"
+#include "include/core/SkPoint.h"
+#include "include/core/SkRect.h"
+#include "include/gpu/GrRecordingContext.h"
+#include "src/core/SkCanvasPriv.h"
+#include "src/gpu/GrBuffer.h"
+#include "src/gpu/GrGeometryProcessor.h"
+#include "src/gpu/GrGpuBuffer.h"
+#include "src/gpu/GrOpFlushState.h"
+#include "src/gpu/GrProcessor.h"
+#include "src/gpu/GrProcessorSet.h"
+#include "src/gpu/GrProgramInfo.h"
+#include "src/gpu/GrResourceProvider.h"
+#include "src/gpu/GrShaderVar.h"
+#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
+#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
+#include "src/gpu/ops/GrDrawOp.h"
+#include "src/gpu/ops/GrOp.h"
+#include "src/gpu/v1/SurfaceDrawContext_v1.h"
+#include "tools/gpu/ProxyUtils.h"
+
+#include <memory>
+#include <vector>
+
+class GrAppliedClip;
+class GrGLSLProgramDataManager;
+
+namespace {
+
+enum class AttrMode {
+    kAuto,
+    kManual,
+    kWacky
+};
+
+class AttributeTestProcessor : public GrGeometryProcessor {
+public:
+    static GrGeometryProcessor* Make(SkArenaAlloc* arena, AttrMode mode) {
+        return arena->make([&](void* ptr) { return new (ptr) AttributeTestProcessor(mode); });
+    }
+
+    const char* name() const final { return "AttributeTestProcessor"; }
+
+    void addToKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const final {
+        b->add32(static_cast<uint32_t>(fMode));
+    }
+
+    std::unique_ptr<ProgramImpl> makeProgramImpl(const GrShaderCaps&) const final;
+
+private:
+    AttributeTestProcessor(AttrMode mode)
+            : GrGeometryProcessor(kAttributeTestProcessor_ClassID), fMode(mode) {
+        switch (fMode) {
+            case AttrMode::kAuto:
+                fAttributes.emplace_back("pos", kFloat2_GrVertexAttribType, kFloat2_GrSLType);
+                fAttributes.emplace_back("color", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType);
+                this->setVertexAttributesWithImplicitOffsets(fAttributes.data(),
+                                                             fAttributes.size());
+                break;
+            case AttrMode::kManual:
+                // Same result as kAuto but with explicitly specified offsets and stride.
+                fAttributes.emplace_back("pos", kFloat2_GrVertexAttribType, kFloat2_GrSLType, 0);
+                fAttributes.emplace_back(
+                        "color", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType, 8);
+                this->setVertexAttributes(fAttributes.data(), fAttributes.size(), 12);
+                break;
+            case AttrMode::kWacky:
+                //  0 thru  7 : float2 aliased to "pos0" and "pos1"
+                //  8 thru 11: pad
+                // 12 thru 15: unorm4 "color"
+                // 16 thru 19: pad
+                fAttributes.emplace_back("pos0", kFloat2_GrVertexAttribType, kFloat2_GrSLType, 0);
+                fAttributes.emplace_back("pos1", kFloat2_GrVertexAttribType, kFloat2_GrSLType, 0);
+                fAttributes.emplace_back(
+                        "color", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType, 12);
+                this->setVertexAttributes(fAttributes.data(), fAttributes.size(), 20);
+                break;
+        }
+    }
+
+    const AttrMode fMode;
+
+    std::vector<Attribute> fAttributes;
+
+    using INHERITED = GrGeometryProcessor;
+};
+
+std::unique_ptr<GrGeometryProcessor::ProgramImpl> AttributeTestProcessor::makeProgramImpl(
+        const GrShaderCaps&) const {
+    class Impl : public ProgramImpl {
+    public:
+        void setData(const GrGLSLProgramDataManager&,
+                     const GrShaderCaps&,
+                     const GrGeometryProcessor&) override {}
+
+    private:
+        void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
+            const AttributeTestProcessor& proc = args.fGeomProc.cast<AttributeTestProcessor>();
+            args.fVaryingHandler->emitAttributes(proc);
+            if (proc.fMode == AttrMode::kWacky) {
+                args.fVertBuilder->codeAppend("float2 pos = pos0 + pos1;");
+            }
+            args.fFragBuilder->codeAppendf("half4 %s;", args.fOutputColor);
+            args.fVaryingHandler->addPassThroughAttribute(GrShaderVar("color", kHalf4_GrSLType),
+                                                          args.fOutputColor);
+            gpArgs->fPositionVar.set(kFloat2_GrSLType, "pos");
+            args.fFragBuilder->codeAppendf("const half4 %s = half4(1);", args.fOutputCoverage);
+        }
+    };
+
+    return std::make_unique<Impl>();
+}
+
+class AttributeTestOp : public GrDrawOp {
+public:
+    DEFINE_OP_CLASS_ID
+
+    static GrOp::Owner Make(GrRecordingContext* context, AttrMode mode, const SkRect& r) {
+        return GrOp::Make<AttributeTestOp>(context, mode, r);
+    }
+
+private:
+    AttributeTestOp(AttrMode mode, SkRect rect) : GrDrawOp(ClassID()), fMode(mode), fRect(rect) {
+        this->setBounds(fRect, HasAABloat::kNo, IsHairline::kNo);
+    }
+
+    const char* name() const override { return "AttributeTestOp"; }
+    FixedFunctionFlags fixedFunctionFlags() const override { return FixedFunctionFlags::kNone; }
+    GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, GrClampType) override {
+        return GrProcessorSet::EmptySetAnalysis();
+    }
+
+    GrProgramInfo* createProgramInfo(const GrCaps* caps,
+                                     SkArenaAlloc* arena,
+                                     const GrSurfaceProxyView& writeView,
+                                     bool usesMSAASurface,
+                                     GrAppliedClip&& appliedClip,
+                                     const GrDstProxyView& dstProxyView,
+                                     GrXferBarrierFlags renderPassXferBarriers,
+                                     GrLoadOp colorLoadOp) const {
+        GrGeometryProcessor* geomProc = AttributeTestProcessor::Make(arena, fMode);
+
+        return sk_gpu_test::CreateProgramInfo(caps,
+                                              arena,
+                                              writeView,
+                                              usesMSAASurface,
+                                              std::move(appliedClip),
+                                              dstProxyView,
+                                              geomProc,
+                                              SkBlendMode::kSrcOver,
+                                              GrPrimitiveType::kTriangleStrip,
+                                              renderPassXferBarriers,
+                                              colorLoadOp);
+    }
+
+    GrProgramInfo* createProgramInfo(GrOpFlushState* flushState) const {
+        return this->createProgramInfo(&flushState->caps(),
+                                       flushState->allocator(),
+                                       flushState->writeView(),
+                                       flushState->usesMSAASurface(),
+                                       flushState->detachAppliedClip(),
+                                       flushState->dstProxyView(),
+                                       flushState->renderPassBarriers(),
+                                       flushState->colorLoadOp());
+    }
+
+    void onPrePrepare(GrRecordingContext* context,
+                      const GrSurfaceProxyView& writeView,
+                      GrAppliedClip* clip,
+                      const GrDstProxyView& dstProxyView,
+                      GrXferBarrierFlags renderPassXferBarriers,
+                      GrLoadOp colorLoadOp) final {
+        SkArenaAlloc* arena = context->priv().recordTimeAllocator();
+
+        // DMSAA is not supported on DDL.
+        bool usesMSAASurface = writeView.asRenderTargetProxy()->numSamples() > 1;
+
+        // This is equivalent to a GrOpFlushState::detachAppliedClip
+        GrAppliedClip appliedClip = clip ? std::move(*clip) : GrAppliedClip::Disabled();
+
+        fProgramInfo = this->createProgramInfo(context->priv().caps(),
+                                               arena,
+                                               writeView,
+                                               usesMSAASurface,
+                                               std::move(appliedClip),
+                                               dstProxyView,
+                                               renderPassXferBarriers,
+                                               colorLoadOp);
+
+        context->priv().recordProgramInfo(fProgramInfo);
+    }
+
+    template <typename V> void makeVB(GrOpFlushState* flushState, const SkRect rect) {
+        V v[4];
+        v[0].p = {rect.left() , rect.top()   };
+        v[1].p = {rect.right(), rect.top()   };
+        v[2].p = {rect.left() , rect.bottom()};
+        v[3].p = {rect.right(), rect.bottom()};
+        v[0].color = SK_ColorRED;
+        v[1].color = SK_ColorGREEN;
+        v[2].color = SK_ColorYELLOW;
+        v[3].color = SK_ColorMAGENTA;
+        fVertexBuffer = flushState->resourceProvider()->createBuffer(
+                sizeof(v), GrGpuBufferType::kVertex, kStatic_GrAccessPattern, v);
+    }
+
+    void onPrepare(GrOpFlushState* flushState) override {
+        if (fMode == AttrMode::kWacky) {
+            struct V {
+                SkPoint p;
+                uint32_t pad0;
+                uint32_t color;
+                uint32_t pad1;
+            };
+            SkRect rect {fRect.fLeft/2.f, fRect.fTop/2.f, fRect.fRight/2.f, fRect.fBottom/2.f};
+            this->makeVB<V>(flushState, rect);
+        } else {
+            struct V {
+                SkPoint p;
+                uint32_t color;
+            };
+            this->makeVB<V>(flushState, fRect);
+        }
+    }
+
+    void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override {
+        if (!fVertexBuffer) {
+            return;
+        }
+
+        if (!fProgramInfo) {
+            fProgramInfo = this->createProgramInfo(flushState);
+        }
+
+        flushState->bindPipeline(*fProgramInfo, fRect);
+        flushState->bindBuffers(nullptr, nullptr, std::move(fVertexBuffer));
+        flushState->draw(4, 0);
+    }
+
+    sk_sp<GrBuffer> fVertexBuffer;
+    const AttrMode  fMode;
+    const SkRect    fRect;
+
+    // The program info (and both the GrPipeline and GrGeometryProcessor it relies on), when
+    // allocated, are allocated in either the ddl-record-time or flush-time arena. It is the
+    // arena's job to free up their memory so we just have a bare programInfo pointer here. We
+    // don't even store the GrPipeline and GrGeometryProcessor pointers here bc they are
+    // guaranteed to have the same lifetime as the program info.
+    GrProgramInfo* fProgramInfo = nullptr;
+
+    friend class ::GrOp;  // for ctor
+
+    using INHERITED = GrDrawOp;
+};
+
+}  // namespace
+
+namespace skiagm {
+
+/**
+ * This is a GPU-backend specific test that exercises explicit and implicit attribute offsets and
+ * strides.
+ */
+class AttributesGM : public GpuGM {
+    SkString onShortName() override { return SkString("attributes"); }
+    SkISize onISize() override { return {120, 340}; }
+    DrawResult onDraw(GrRecordingContext*, SkCanvas*, SkString* errorMsg) override;
+};
+
+DrawResult AttributesGM::onDraw(GrRecordingContext* rc, SkCanvas* canvas, SkString* errorMsg) {
+    auto sdc = SkCanvasPriv::TopDeviceSurfaceDrawContext(canvas);
+    if (!sdc) {
+        *errorMsg = kErrorMsg_DrawSkippedGpuOnly;
+        return DrawResult::kSkip;
+    }
+
+    sdc->clear(SK_PMColor4fBLACK);
+
+    // Draw the test directly to the frame buffer.
+    auto r = SkRect::MakeXYWH(10, 10, 100, 100);
+    for (AttrMode m : {AttrMode::kAuto, AttrMode::kManual, AttrMode::kWacky}) {
+        sdc->addDrawOp(AttributeTestOp::Make(rc, m, r));
+        r.offset(0, 110);
+    }
+
+    return DrawResult::kOk;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+DEF_GM( return new AttributesGM(); )
+
+}  // namespace skiagm
diff --git a/gm/clockwise.cpp b/gm/clockwise.cpp
index 1c8e5df..c2a606c 100644
--- a/gm/clockwise.cpp
+++ b/gm/clockwise.cpp
@@ -84,7 +84,7 @@
     ClockwiseTestProcessor(bool readSkFragCoord)
             : GrGeometryProcessor(kClockwiseTestProcessor_ClassID)
             , fReadSkFragCoord(readSkFragCoord) {
-        this->setVertexAttributes(&gVertex, 1);
+        this->setVertexAttributesWithImplicitOffsets(&gVertex, 1);
     }
 
     const bool fReadSkFragCoord;
diff --git a/gm/fwidth_squircle.cpp b/gm/fwidth_squircle.cpp
index 42d5e8b..2e02272 100644
--- a/gm/fwidth_squircle.cpp
+++ b/gm/fwidth_squircle.cpp
@@ -77,7 +77,7 @@
     FwidthSquircleTestProcessor(const SkMatrix& viewMatrix)
             : GrGeometryProcessor(kFwidthSquircleTestProcessor_ClassID)
             , fViewMatrix(viewMatrix) {
-        this->setVertexAttributes(&gVertex, 1);
+        this->setVertexAttributesWithImplicitOffsets(&gVertex, 1);
     }
 
     const SkMatrix fViewMatrix;
diff --git a/gm/tessellation.cpp b/gm/tessellation.cpp
index f7a9c79..a255361 100644
--- a/gm/tessellation.cpp
+++ b/gm/tessellation.cpp
@@ -55,7 +55,7 @@
 public:
     TessellationTestTriShader(const SkMatrix& viewMatrix)
             : GrGeometryProcessor(kTessellationTestTriShader_ClassID), fViewMatrix(viewMatrix) {
-        this->setVertexAttributes(&kPositionAttrib, 1);
+        this->setVertexAttributesWithImplicitOffsets(&kPositionAttrib, 1);
         this->setWillUseTessellationShaders();
     }
 
diff --git a/gn/gm.gni b/gn/gm.gni
index 6785e51..ae852e8 100644
--- a/gn/gm.gni
+++ b/gn/gm.gni
@@ -27,6 +27,7 @@
   "$_gm/arcto.cpp",
   "$_gm/arithmode.cpp",
   "$_gm/asyncrescaleandread.cpp",
+  "$_gm/attributes.cpp",
   "$_gm/b_119394958.cpp",
   "$_gm/backdrop.cpp",
   "$_gm/backdrop_imagefilter_croprect.cpp",
diff --git a/src/gpu/GrDefaultGeoProcFactory.cpp b/src/gpu/GrDefaultGeoProcFactory.cpp
index 2bb0051..f298653 100644
--- a/src/gpu/GrDefaultGeoProcFactory.cpp
+++ b/src/gpu/GrDefaultGeoProcFactory.cpp
@@ -223,7 +223,7 @@
         if (fFlags & kCoverageAttribute_GPFlag) {
             fInCoverage = {"inCoverage", kFloat_GrVertexAttribType, kHalf_GrSLType};
         }
-        this->setVertexAttributes(&fInPosition, 4);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 4);
     }
 
     Attribute fInPosition;
diff --git a/src/gpu/GrGeometryProcessor.cpp b/src/gpu/GrGeometryProcessor.cpp
index f0c59d2..95ae44c 100644
--- a/src/gpu/GrGeometryProcessor.cpp
+++ b/src/gpu/GrGeometryProcessor.cpp
@@ -486,3 +486,89 @@
                           &gpArgs->fLocalCoordVar,
                           localMatrixUniform);
 }
+
+//////////////////////////////////////////////////////////////////////////////
+
+using Attribute    = GrGeometryProcessor::Attribute;
+using AttributeSet = GrGeometryProcessor::AttributeSet;
+
+GrGeometryProcessor::Attribute AttributeSet::Iter::operator*() const {
+    if (fCurr->offset().has_value()) {
+        return *fCurr;
+    }
+    return Attribute(fCurr->name(), fCurr->cpuType(), fCurr->gpuType(), fImplicitOffset);
+}
+
+void AttributeSet::Iter::operator++() {
+    if (fRemaining) {
+        fRemaining--;
+        fImplicitOffset += Attribute::AlignOffset(fCurr->size());
+        fCurr++;
+        this->skipUninitialized();
+    }
+}
+
+void AttributeSet::Iter::skipUninitialized() {
+    if (!fRemaining) {
+        fCurr = nullptr;
+    } else {
+        while (!fCurr->isInitialized()) {
+            ++fCurr;
+        }
+    }
+}
+
+void AttributeSet::initImplicit(const Attribute* attrs, int count) {
+    fAttributes = attrs;
+    fRawCount   = count;
+    fCount      = 0;
+    fStride     = 0;
+    for (int i = 0; i < count; ++i) {
+        if (attrs[i].isInitialized()) {
+            fCount++;
+            fStride += Attribute::AlignOffset(attrs[i].size());
+        }
+    }
+}
+
+void AttributeSet::initExplicit(const Attribute* attrs, int count, size_t stride) {
+    fAttributes = attrs;
+    fRawCount   = count;
+    fCount      = count;
+    fStride     = stride;
+    SkASSERT(Attribute::AlignOffset(fStride) == fStride);
+    for (int i = 0; i < count; ++i) {
+        SkASSERT(attrs[i].isInitialized());
+        SkASSERT(attrs[i].offset().has_value());
+        SkASSERT(Attribute::AlignOffset(*attrs[i].offset()) == *attrs[i].offset());
+        SkASSERT(*attrs[i].offset() + attrs[i].size() <= fStride);
+    }
+}
+
+void AttributeSet::addToKey(GrProcessorKeyBuilder* b) const {
+    int rawCount = SkAbs32(fRawCount);
+    b->addBits(16, SkToU16(this->stride()), "stride");
+    b->addBits(16, rawCount, "attribute count");
+    size_t implicitOffset = 0;
+    for (int i = 0; i < rawCount; ++i) {
+        const Attribute& attr = fAttributes[i];
+        b->appendComment(attr.isInitialized() ? attr.name() : "unusedAttr");
+        static_assert(kGrVertexAttribTypeCount < (1 << 8), "");
+        static_assert(kGrSLTypeCount           < (1 << 8), "");
+        b->addBits(8,  attr.isInitialized() ? attr.cpuType() : 0xff, "attrType");
+        b->addBits(8 , attr.isInitialized() ? attr.gpuType() : 0xff, "attrGpuType");
+        int16_t offset = -1;
+        if (attr.isInitialized()) {
+            if (attr.offset().has_value()) {
+                offset = *attr.offset();
+            } else {
+                offset = implicitOffset;
+                implicitOffset += Attribute::AlignOffset(attr.size());
+            }
+        }
+        b->addBits(16, static_cast<uint16_t>(offset), "attrOffset");
+    }
+}
+
+AttributeSet::Iter AttributeSet::begin() const { return Iter(fAttributes, fCount); }
+AttributeSet::Iter AttributeSet::end() const { return Iter(); }
diff --git a/src/gpu/GrGeometryProcessor.h b/src/gpu/GrGeometryProcessor.h
index f0d3e8f..6867768 100644
--- a/src/gpu/GrGeometryProcessor.h
+++ b/src/gpu/GrGeometryProcessor.h
@@ -63,98 +63,111 @@
     /** Describes a vertex or instance attribute. */
     class Attribute {
     public:
+        static constexpr size_t AlignOffset(size_t offset) { return SkAlign4(offset); }
+
         constexpr Attribute() = default;
+        /**
+         * Makes an attribute whose offset will be implicitly determined by the types and ordering
+         * of an array attributes.
+         */
         constexpr Attribute(const char* name,
                             GrVertexAttribType cpuType,
                             GrSLType gpuType)
                 : fName(name), fCPUType(cpuType), fGPUType(gpuType) {
             SkASSERT(name && gpuType != kVoid_GrSLType);
         }
+        /**
+         * Makes an attribute with an explicit offset.
+         */
+        constexpr Attribute(const char*        name,
+                            GrVertexAttribType cpuType,
+                            GrSLType           gpuType,
+                            size_t             offset)
+                : fName(name), fCPUType(cpuType), fGPUType(gpuType), fOffset(SkToU32(offset)) {
+            SkASSERT(AlignOffset(offset) == offset);
+            SkASSERT(name && gpuType != kVoid_GrSLType);
+        }
         constexpr Attribute(const Attribute&) = default;
 
         Attribute& operator=(const Attribute&) = default;
 
         constexpr bool isInitialized() const { return fGPUType != kVoid_GrSLType; }
 
-        constexpr const char* name() const { return fName; }
+        constexpr const char*           name() const { return fName; }
         constexpr GrVertexAttribType cpuType() const { return fCPUType; }
         constexpr GrSLType           gpuType() const { return fGPUType; }
+        /**
+         * Returns the offset if attributes were specified with explicit offsets. Otherwise,
+         * offsets (and total vertex stride) are implicitly determined from attribute order and
+         * types.
+         */
+        skstd::optional<size_t> offset() const {
+            if (fOffset != kImplicitOffset) {
+                SkASSERT(AlignOffset(fOffset) == fOffset);
+                return {fOffset};
+            }
+            return skstd::nullopt;
+        }
 
         inline constexpr size_t size() const;
-        constexpr size_t sizeAlign4() const { return SkAlign4(this->size()); }
 
         GrShaderVar asShaderVar() const {
             return {fName, fGPUType, GrShaderVar::TypeModifier::In};
         }
 
     private:
-        const char* fName = nullptr;
+        static constexpr uint32_t kImplicitOffset = 1;  // 1 is not valid because it isn't aligned.
+
+        const char*        fName    = nullptr;
         GrVertexAttribType fCPUType = kFloat_GrVertexAttribType;
-        GrSLType fGPUType = kVoid_GrSLType;
+        GrSLType           fGPUType = kVoid_GrSLType;
+        uint32_t           fOffset  = kImplicitOffset;
     };
 
-    class Iter {
-    public:
-        Iter() : fCurr(nullptr), fRemaining(0) {}
-        Iter(const Iter& iter) : fCurr(iter.fCurr), fRemaining(iter.fRemaining) {}
-        Iter& operator= (const Iter& iter) {
-            fCurr = iter.fCurr;
-            fRemaining = iter.fRemaining;
-            return *this;
-        }
-        Iter(const Attribute* attrs, int count) : fCurr(attrs), fRemaining(count) {
-            this->skipUninitialized();
-        }
+    /**
+     * A set of attributes that can iterated. The iterator handles hides two pieces of complexity:
+     * 1) It skips uninitialized attributes.
+     * 2) It always returns an attribute with a known offset.
+     */
+    class AttributeSet {
+        class Iter {
+        public:
+            Iter() = default;
+            Iter(const Iter& iter) = default;
+            Iter& operator=(const Iter& iter) = default;
 
-        bool operator!=(const Iter& that) const { return fCurr != that.fCurr; }
-        const Attribute& operator*() const { return *fCurr; }
-        void operator++() {
-            if (fRemaining) {
-                fRemaining--;
-                fCurr++;
+            Iter(const Attribute* attrs, int count) : fCurr(attrs), fRemaining(count) {
                 this->skipUninitialized();
             }
-        }
 
-    private:
-        void skipUninitialized() {
-            if (!fRemaining) {
-                fCurr = nullptr;
-            } else {
-                while (!fCurr->isInitialized()) {
-                    ++fCurr;
-                }
-            }
-        }
+            bool operator!=(const Iter& that) const { return fCurr != that.fCurr; }
+            Attribute operator*() const;
+            void operator++();
 
-        const Attribute* fCurr;
-        int fRemaining;
-    };
+        private:
+            void skipUninitialized();
 
-    class AttributeSet {
+            const Attribute* fCurr           = nullptr;
+            int              fRemaining      = 0;
+            size_t           fImplicitOffset = 0;
+        };
+
     public:
-        Iter begin() const { return Iter(fAttributes, fCount); }
-        Iter end() const { return Iter(); }
+        Iter begin() const;
+        Iter end() const;
 
         int count() const { return fCount; }
         size_t stride() const { return fStride; }
 
+        // Init with implicit offsets and stride. No attributes can have a predetermined stride.
+        void initImplicit(const Attribute* attrs, int count);
+        // Init with explicit offsets and stride. All attributes must be initialized and have
+        // an explicit offset aligned to 4 bytes and with no attribute crossing stride boundaries.
+        void initExplicit(const Attribute* attrs, int count, size_t stride);
+
+        void addToKey(GrProcessorKeyBuilder* b) const;
+
     private:
-        friend class GrGeometryProcessor;
-
-        void init(const Attribute* attrs, int count) {
-            fAttributes = attrs;
-            fRawCount = count;
-            fCount = 0;
-            fStride = 0;
-            for (int i = 0; i < count; ++i) {
-                if (attrs[i].isInitialized()) {
-                    fCount++;
-                    fStride += attrs[i].sizeAlign4();
-                }
-            }
-        }
-
         const Attribute* fAttributes = nullptr;
         int              fRawCount = 0;
         int              fCount = 0;
@@ -165,21 +178,21 @@
 
     int numTextureSamplers() const { return fTextureSamplerCnt; }
     const TextureSampler& textureSampler(int index) const;
-    int numVertexAttributes() const { return fVertexAttributes.fCount; }
+    int numVertexAttributes() const { return fVertexAttributes.count(); }
     const AttributeSet& vertexAttributes() const { return fVertexAttributes; }
-    int numInstanceAttributes() const { return fInstanceAttributes.fCount; }
+    int numInstanceAttributes() const { return fInstanceAttributes.count(); }
     const AttributeSet& instanceAttributes() const { return fInstanceAttributes; }
 
-    bool hasVertexAttributes() const { return SkToBool(fVertexAttributes.fCount); }
-    bool hasInstanceAttributes() const { return SkToBool(fInstanceAttributes.fCount); }
+    bool hasVertexAttributes() const { return SkToBool(fVertexAttributes.count()); }
+    bool hasInstanceAttributes() const { return SkToBool(fInstanceAttributes.count()); }
 
     /**
      * A common practice is to populate the the vertex/instance's memory using an implicit array of
      * structs. In this case, it is best to assert that:
      *     stride == sizeof(struct)
      */
-    size_t vertexStride() const { return fVertexAttributes.fStride; }
-    size_t instanceStride() const { return fInstanceAttributes.fStride; }
+    size_t vertexStride() const { return fVertexAttributes.stride(); }
+    size_t instanceStride() const { return fInstanceAttributes.stride(); }
 
     bool willUseTessellationShaders() const {
         return fShaders & (kTessControl_GrShaderFlag | kTessEvaluation_GrShaderFlag);
@@ -200,23 +213,10 @@
     virtual void addToKey(const GrShaderCaps&, GrProcessorKeyBuilder*) const = 0;
 
     void getAttributeKey(GrProcessorKeyBuilder* b) const {
-        // Ensure that our CPU and GPU type fields fit together in a 32-bit value, and we never
-        // collide with the "uninitialized" value.
-        static_assert(kGrVertexAttribTypeCount < (1 << 8), "");
-        static_assert(kGrSLTypeCount           < (1 << 8), "");
-
-        auto add_attributes = [=](const Attribute* attrs, int attrCount) {
-            for (int i = 0; i < attrCount; ++i) {
-                const Attribute& attr = attrs[i];
-                b->appendComment(attr.isInitialized() ? attr.name() : "unusedAttr");
-                b->addBits(8, attr.isInitialized() ? attr.cpuType() : 0xff, "attrType");
-                b->addBits(8, attr.isInitialized() ? attr.gpuType() : 0xff, "attrGpuType");
-            }
-        };
-        b->add32(fVertexAttributes.fRawCount, "numVertexAttributes");
-        add_attributes(fVertexAttributes.fAttributes, fVertexAttributes.fRawCount);
-        b->add32(fInstanceAttributes.fRawCount, "numInstanceAttributes");
-        add_attributes(fInstanceAttributes.fAttributes, fInstanceAttributes.fRawCount);
+        b->appendComment("vertex attributes");
+        fVertexAttributes.addToKey(b);
+        b->appendComment("instance attributes");
+        fInstanceAttributes.addToKey(b);
     }
 
     /**
@@ -233,13 +233,20 @@
                  wideColor ? kFloat4_GrVertexAttribType : kUByte4_norm_GrVertexAttribType,
                  kHalf4_GrSLType };
     }
-
-    void setVertexAttributes(const Attribute* attrs, int attrCount) {
-        fVertexAttributes.init(attrs, attrCount);
+    void setVertexAttributes(const Attribute* attrs, int attrCount, size_t stride) {
+        fVertexAttributes.initExplicit(attrs, attrCount, stride);
     }
-    void setInstanceAttributes(const Attribute* attrs, int attrCount) {
+    void setInstanceAttributes(const Attribute* attrs, int attrCount, size_t stride) {
         SkASSERT(attrCount >= 0);
-        fInstanceAttributes.init(attrs, attrCount);
+        fInstanceAttributes.initExplicit(attrs, attrCount, stride);
+    }
+
+    void setVertexAttributesWithImplicitOffsets(const Attribute* attrs, int attrCount) {
+        fVertexAttributes.initImplicit(attrs, attrCount);
+    }
+    void setInstanceAttributesWithImplicitOffsets(const Attribute* attrs, int attrCount) {
+        SkASSERT(attrCount >= 0);
+        fInstanceAttributes.initImplicit(attrs, attrCount);
     }
     void setWillUseTessellationShaders() {
         fShaders |= kTessControl_GrShaderFlag | kTessEvaluation_GrShaderFlag;
diff --git a/src/gpu/GrProcessor.h b/src/gpu/GrProcessor.h
index 104537b..8617644 100644
--- a/src/gpu/GrProcessor.h
+++ b/src/gpu/GrProcessor.h
@@ -32,6 +32,7 @@
     enum ClassID {
         kNull_ClassID,  // Reserved ID for missing (null) processors
 
+        kAttributeTestProcessor_ClassID,
         kBigKeyProcessor_ClassID,
         kBlendFragmentProcessor_ClassID,
         kBlockInputFragmentProcessor_ClassID,
diff --git a/src/gpu/d3d/GrD3DPipelineStateBuilder.cpp b/src/gpu/d3d/GrD3DPipelineStateBuilder.cpp
index d1d17b5..431aa25 100644
--- a/src/gpu/d3d/GrD3DPipelineStateBuilder.cpp
+++ b/src/gpu/d3d/GrD3DPipelineStateBuilder.cpp
@@ -245,32 +245,26 @@
     }
 
     unsigned int currentAttrib = 0;
-    unsigned int vertexAttributeOffset = 0;
 
     for (const auto& attrib : geomProc.vertexAttributes()) {
         // When using SPIRV-Cross it converts the location modifier in SPIRV to be
         // TEXCOORD<N> where N is the location value for eveery vertext attribute
         inputElements[currentAttrib] = { "TEXCOORD", currentAttrib,
                                         attrib_type_to_format(attrib.cpuType()),
-                                        vertexSlot, vertexAttributeOffset,
+                                        vertexSlot, SkToU32(*attrib.offset()),
                                         D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 };
-        vertexAttributeOffset += attrib.sizeAlign4();
         currentAttrib++;
     }
-    SkASSERT(vertexAttributeOffset == geomProc.vertexStride());
 
-    unsigned int instanceAttributeOffset = 0;
     for (const auto& attrib : geomProc.instanceAttributes()) {
         // When using SPIRV-Cross it converts the location modifier in SPIRV to be
         // TEXCOORD<N> where N is the location value for eveery vertext attribute
         inputElements[currentAttrib] = { "TEXCOORD", currentAttrib,
                                         attrib_type_to_format(attrib.cpuType()),
-                                        instanceSlot, instanceAttributeOffset,
+                                        instanceSlot, SkToU32(*attrib.offset()),
                                         D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1 };
-        instanceAttributeOffset += attrib.sizeAlign4();
         currentAttrib++;
     }
-    SkASSERT(instanceAttributeOffset == geomProc.instanceStride());
 }
 
 static D3D12_BLEND blend_coeff_to_d3d_blend(GrBlendCoeff coeff) {
diff --git a/src/gpu/dawn/GrDawnProgramBuilder.cpp b/src/gpu/dawn/GrDawnProgramBuilder.cpp
index 37c3e3a..42ff416 100644
--- a/src/gpu/dawn/GrDawnProgramBuilder.cpp
+++ b/src/gpu/dawn/GrDawnProgramBuilder.cpp
@@ -338,18 +338,16 @@
     const GrGeometryProcessor& geomProc = programInfo.geomProc();
     int i = 0;
     if (geomProc.numVertexAttributes() > 0) {
-        size_t offset = 0;
         for (const auto& attrib : geomProc.vertexAttributes()) {
             wgpu::VertexAttribute attribute;
             attribute.shaderLocation = i;
-            attribute.offset = offset;
+            attribute.offset = *attrib.offset();
             attribute.format = to_dawn_vertex_format(attrib.cpuType());
             vertexAttributes.push_back(attribute);
-            offset += attrib.sizeAlign4();
             i++;
         }
         wgpu::VertexBufferLayout input;
-        input.arrayStride = offset;
+        input.arrayStride = geomProc.vertexStride();
         input.stepMode = wgpu::VertexStepMode::Vertex;
         input.attributeCount = vertexAttributes.size();
         input.attributes = &vertexAttributes.front();
@@ -357,18 +355,16 @@
     }
     std::vector<wgpu::VertexAttribute> instanceAttributes;
     if (geomProc.numInstanceAttributes() > 0) {
-        size_t offset = 0;
         for (const auto& attrib : geomProc.instanceAttributes()) {
             wgpu::VertexAttribute attribute;
             attribute.shaderLocation = i;
-            attribute.offset = offset;
+            attribute.offset = *attrib.offset();
             attribute.format = to_dawn_vertex_format(attrib.cpuType());
             instanceAttributes.push_back(attribute);
-            offset += attrib.sizeAlign4();
             i++;
         }
         wgpu::VertexBufferLayout input;
-        input.arrayStride = offset;
+        input.arrayStride = geomProc.instanceStride();
         input.stepMode = wgpu::VertexStepMode::Instance;
         input.attributeCount = instanceAttributes.size();
         input.attributes = &instanceAttributes.front();
diff --git a/src/gpu/effects/GrBezierEffect.cpp b/src/gpu/effects/GrBezierEffect.cpp
index 088dc11..a489387 100644
--- a/src/gpu/effects/GrBezierEffect.cpp
+++ b/src/gpu/effects/GrBezierEffect.cpp
@@ -175,7 +175,7 @@
         , fLocalMatrix(viewMatrix)
         , fUsesLocalCoords(usesLocalCoords)
         , fCoverageScale(coverage) {
-    this->setVertexAttributes(kAttributes, SK_ARRAY_COUNT(kAttributes));
+    this->setVertexAttributesWithImplicitOffsets(kAttributes, SK_ARRAY_COUNT(kAttributes));
 }
 
 //////////////////////////////////////////////////////////////////////////////
@@ -328,7 +328,7 @@
     , fLocalMatrix(localMatrix)
     , fUsesLocalCoords(usesLocalCoords)
     , fCoverageScale(coverage) {
-    this->setVertexAttributes(kAttributes, SK_ARRAY_COUNT(kAttributes));
+    this->setVertexAttributesWithImplicitOffsets(kAttributes, SK_ARRAY_COUNT(kAttributes));
 }
 
 //////////////////////////////////////////////////////////////////////////////
diff --git a/src/gpu/effects/GrBitmapTextGeoProc.cpp b/src/gpu/effects/GrBitmapTextGeoProc.cpp
index e0f2a2a..48a867b 100644
--- a/src/gpu/effects/GrBitmapTextGeoProc.cpp
+++ b/src/gpu/effects/GrBitmapTextGeoProc.cpp
@@ -140,7 +140,7 @@
 
     fInTextureCoords = {"inTextureCoords", kUShort2_GrVertexAttribType,
                         caps.integerSupport() ? kUShort2_GrSLType : kFloat2_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 
     if (numActiveViews) {
         fAtlasDimensions = views[0].proxy()->dimensions();
diff --git a/src/gpu/effects/GrDistanceFieldGeoProc.cpp b/src/gpu/effects/GrDistanceFieldGeoProc.cpp
index d125cc6..87bf7d3 100644
--- a/src/gpu/effects/GrDistanceFieldGeoProc.cpp
+++ b/src/gpu/effects/GrDistanceFieldGeoProc.cpp
@@ -228,7 +228,7 @@
     fInColor = {"inColor", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType };
     fInTextureCoords = {"inTextureCoords", kUShort2_GrVertexAttribType,
                         caps.integerSupport() ? kUShort2_GrSLType : kFloat2_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 
     if (numViews) {
         fAtlasDimensions = views[0].proxy()->dimensions();
@@ -494,7 +494,7 @@
     fInColor = MakeColorAttribute("inColor", wideColor);
     fInTextureCoords = {"inTextureCoords", kUShort2_GrVertexAttribType,
                         caps.integerSupport() ? kUShort2_GrSLType : kFloat2_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 
     if (numViews) {
         fAtlasDimensions = views[0].proxy()->dimensions();
@@ -807,7 +807,7 @@
     fInColor = {"inColor", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType};
     fInTextureCoords = {"inTextureCoords", kUShort2_GrVertexAttribType,
                         caps.integerSupport() ? kUShort2_GrSLType : kFloat2_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 
     if (numViews) {
         fAtlasDimensions = views[0].proxy()->dimensions();
diff --git a/src/gpu/effects/GrShadowGeoProc.cpp b/src/gpu/effects/GrShadowGeoProc.cpp
index 4d2528f..25fd74d 100644
--- a/src/gpu/effects/GrShadowGeoProc.cpp
+++ b/src/gpu/effects/GrShadowGeoProc.cpp
@@ -56,7 +56,7 @@
     fInPosition = {"inPosition", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
     fInColor = {"inColor", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType};
     fInShadowParams = {"inShadowParams", kFloat3_GrVertexAttribType, kHalf3_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 
     SkASSERT(lutView.proxy());
     fLUTTextureSampler.reset(GrSamplerState::Filter::kLinear, lutView.proxy()->backendFormat(),
diff --git a/src/gpu/gl/builders/GrGLProgramBuilder.cpp b/src/gpu/gl/builders/GrGLProgramBuilder.cpp
index eaf67b3..ece26fa 100644
--- a/src/gpu/gl/builders/GrGLProgramBuilder.cpp
+++ b/src/gpu/gl/builders/GrGLProgramBuilder.cpp
@@ -123,25 +123,23 @@
     fInstanceAttributeCnt = geomProc.numInstanceAttributes();
     fAttributes = std::make_unique<GrGLProgram::Attribute[]>(
             fVertexAttributeCnt + fInstanceAttributeCnt);
-    auto addAttr = [&](int i, const auto& a, size_t* stride) {
+    auto addAttr = [&](int i, const auto& a) {
         fAttributes[i].fCPUType = a.cpuType();
         fAttributes[i].fGPUType = a.gpuType();
-        fAttributes[i].fOffset = *stride;
-        *stride += a.sizeAlign4();
+        fAttributes[i].fOffset = *a.offset();
         fAttributes[i].fLocation = i;
         if (bindAttribLocations) {
             GL_CALL(BindAttribLocation(programID, i, a.name()));
         }
     };
-    fVertexStride = 0;
+    fVertexStride = geomProc.vertexStride();
     int i = 0;
     for (const auto& attr : geomProc.vertexAttributes()) {
-        addAttr(i++, attr, &fVertexStride);
+        addAttr(i++, attr);
     }
-    SkASSERT(fVertexStride == geomProc.vertexStride());
-    fInstanceStride = 0;
+    fInstanceStride = geomProc.instanceStride();
     for (const auto& attr : geomProc.instanceAttributes()) {
-        addAttr(i++, attr, &fInstanceStride);
+        addAttr(i++, attr);
     }
     SkASSERT(fInstanceStride == geomProc.instanceStride());
 }
diff --git a/src/gpu/mtl/GrMtlPipelineStateBuilder.mm b/src/gpu/mtl/GrMtlPipelineStateBuilder.mm
index b16cb77..f7eedd4 100644
--- a/src/gpu/mtl/GrMtlPipelineStateBuilder.mm
+++ b/src/gpu/mtl/GrMtlPipelineStateBuilder.mm
@@ -199,33 +199,30 @@
     if (writer) {
         writer->writeInt(vertexAttributeCount);
     }
-    size_t vertexAttributeOffset = 0;
     for (const auto& attribute : geomProc.vertexAttributes()) {
         MTLVertexAttributeDescriptor* mtlAttribute = vertexDescriptor.attributes[attributeIndex];
         MTLVertexFormat format = attribute_type_to_mtlformat(attribute.cpuType());
         SkASSERT(MTLVertexFormatInvalid != format);
         mtlAttribute.format = format;
-        mtlAttribute.offset = vertexAttributeOffset;
+        mtlAttribute.offset = *attribute.offset();
         mtlAttribute.bufferIndex = vertexBinding;
         if (writer) {
             writer->writeInt(format);
-            writer->writeUInt(vertexAttributeOffset);
+            writer->writeUInt(*attribute.offset());
             writer->writeUInt(vertexBinding);
         }
 
-        vertexAttributeOffset += attribute.sizeAlign4();
         attributeIndex++;
     }
-    SkASSERT(vertexAttributeOffset == geomProc.vertexStride());
 
     if (vertexAttributeCount) {
         MTLVertexBufferLayoutDescriptor* vertexBufferLayout =
                 vertexDescriptor.layouts[vertexBinding];
         vertexBufferLayout.stepFunction = MTLVertexStepFunctionPerVertex;
         vertexBufferLayout.stepRate = 1;
-        vertexBufferLayout.stride = vertexAttributeOffset;
+        vertexBufferLayout.stride = geomProc.vertexStride();
         if (writer) {
-            writer->writeUInt(vertexAttributeOffset);
+            writer->writeUInt(geomProc.vertexStride());
         }
     }
 
@@ -233,33 +230,30 @@
     if (writer) {
         writer->writeInt(instanceAttributeCount);
     }
-    size_t instanceAttributeOffset = 0;
     for (const auto& attribute : geomProc.instanceAttributes()) {
         MTLVertexAttributeDescriptor* mtlAttribute = vertexDescriptor.attributes[attributeIndex];
         MTLVertexFormat format = attribute_type_to_mtlformat(attribute.cpuType());
         SkASSERT(MTLVertexFormatInvalid != format);
         mtlAttribute.format = format;
-        mtlAttribute.offset = instanceAttributeOffset;
+        mtlAttribute.offset = *attribute.offset();
         mtlAttribute.bufferIndex = instanceBinding;
         if (writer) {
             writer->writeInt(format);
-            writer->writeUInt(instanceAttributeOffset);
+            writer->writeUInt(*attribute.offset());
             writer->writeUInt(instanceBinding);
         }
 
-        instanceAttributeOffset += attribute.sizeAlign4();
         attributeIndex++;
     }
-    SkASSERT(instanceAttributeOffset == geomProc.instanceStride());
 
     if (instanceAttributeCount) {
         MTLVertexBufferLayoutDescriptor* instanceBufferLayout =
                 vertexDescriptor.layouts[instanceBinding];
         instanceBufferLayout.stepFunction = MTLVertexStepFunctionPerInstance;
         instanceBufferLayout.stepRate = 1;
-        instanceBufferLayout.stride = instanceAttributeOffset;
+        instanceBufferLayout.stride = geomProc.instanceStride();
         if (writer) {
-            writer->writeUInt(instanceAttributeOffset);
+            writer->writeUInt(geomProc.instanceStride());
         }
     }
     return vertexDescriptor;
diff --git a/src/gpu/ops/AAConvexPathRenderer.cpp b/src/gpu/ops/AAConvexPathRenderer.cpp
index aaead12..6a3944b 100644
--- a/src/gpu/ops/AAConvexPathRenderer.cpp
+++ b/src/gpu/ops/AAConvexPathRenderer.cpp
@@ -577,7 +577,7 @@
         fInColor = MakeColorAttribute("inColor", wideColor);
         // GL on iOS 14 needs more precision for the quadedge attributes
         fInQuadEdge = {"inQuadEdge", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
-        this->setVertexAttributes(&fInPosition, 3);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
     }
 
     Attribute fInPosition;
diff --git a/src/gpu/ops/DashOp.cpp b/src/gpu/ops/DashOp.cpp
index b336711..25b27f0 100644
--- a/src/gpu/ops/DashOp.cpp
+++ b/src/gpu/ops/DashOp.cpp
@@ -867,7 +867,7 @@
     fInPosition = {"inPosition", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
     fInDashParams = {"inDashParams", kFloat3_GrVertexAttribType, kHalf3_GrSLType};
     fInCircleParams = {"inCircleParams", kFloat2_GrVertexAttribType, kHalf2_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 }
 
 GR_DEFINE_GEOMETRY_PROCESSOR_TEST(DashingCircleEffect);
@@ -1081,7 +1081,7 @@
     fInPosition = {"inPosition", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
     fInDashParams = {"inDashParams", kFloat3_GrVertexAttribType, kHalf3_GrSLType};
     fInRect = {"inRect", kFloat4_GrVertexAttribType, kHalf4_GrSLType};
-    this->setVertexAttributes(&fInPosition, 3);
+    this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
 }
 
 GR_DEFINE_GEOMETRY_PROCESSOR_TEST(DashingLineEffect);
diff --git a/src/gpu/ops/DrawAtlasPathOp.cpp b/src/gpu/ops/DrawAtlasPathOp.cpp
index 35c4104..44c2828 100644
--- a/src/gpu/ops/DrawAtlasPathOp.cpp
+++ b/src/gpu/ops/DrawAtlasPathOp.cpp
@@ -33,7 +33,7 @@
         if (!shaderCaps.vertexIDSupport()) {
             constexpr static Attribute kUnitCoordAttrib(
                     "unitCoord", kFloat2_GrVertexAttribType, kFloat2_GrSLType);
-            this->setVertexAttributes(&kUnitCoordAttrib, 1);
+            this->setVertexAttributesWithImplicitOffsets(&kUnitCoordAttrib, 1);
         }
         fAttribs.emplace_back("fillBounds", kFloat4_GrVertexAttribType, kFloat4_GrSLType);
         if (fUsesLocalCoords) {
@@ -44,7 +44,7 @@
         fAttribs.emplace_back("color", kFloat4_GrVertexAttribType, kHalf4_GrSLType);
         fAtlasHelper->appendInstanceAttribs(&fAttribs);
         SkASSERT(fAttribs.count() <= kMaxInstanceAttribs);
-        this->setInstanceAttributes(fAttribs.data(), fAttribs.count());
+        this->setInstanceAttributesWithImplicitOffsets(fAttribs.data(), fAttribs.count());
         this->setTextureSamplerCnt(1);
     }
 
diff --git a/src/gpu/ops/DrawVerticesOp.cpp b/src/gpu/ops/DrawVerticesOp.cpp
index 3955a1d..a76cd94 100644
--- a/src/gpu/ops/DrawVerticesOp.cpp
+++ b/src/gpu/ops/DrawVerticesOp.cpp
@@ -174,7 +174,7 @@
                         ? Attribute{"inLocalCoord", kFloat2_GrVertexAttribType, kFloat2_GrSLType}
                         : missingAttr);
 
-        this->setVertexAttributes(fAttributes.data(), fAttributes.size());
+        this->setVertexAttributesWithImplicitOffsets(fAttributes.data(), fAttributes.size());
     }
 
     enum {
diff --git a/src/gpu/ops/FillRRectOp.cpp b/src/gpu/ops/FillRRectOp.cpp
index 275ec2b..37121b8 100644
--- a/src/gpu/ops/FillRRectOp.cpp
+++ b/src/gpu/ops/FillRRectOp.cpp
@@ -353,7 +353,8 @@
     Processor(GrAAType aaType, ProcessorFlags flags)
             : GrGeometryProcessor(kGrFillRRectOp_Processor_ClassID)
             , fFlags(flags) {
-        this->setVertexAttributes(kVertexAttribs, SK_ARRAY_COUNT(kVertexAttribs));
+        this->setVertexAttributesWithImplicitOffsets(kVertexAttribs,
+                                                     SK_ARRAY_COUNT(kVertexAttribs));
 
         fInstanceAttribs.emplace_back("skew", kFloat4_GrVertexAttribType, kFloat4_GrSLType);
         fInstanceAttribs.emplace_back("translate", kFloat2_GrVertexAttribType, kFloat2_GrSLType);
@@ -366,7 +367,8 @@
                     "local_rect", kFloat4_GrVertexAttribType, kFloat4_GrSLType);
         }
         SkASSERT(fInstanceAttribs.count() <= kMaxInstanceAttribs);
-        this->setInstanceAttributes(fInstanceAttribs.begin(), fInstanceAttribs.count());
+        this->setInstanceAttributesWithImplicitOffsets(fInstanceAttribs.begin(),
+                                                       fInstanceAttribs.count());
     }
 
     inline static constexpr Attribute kVertexAttribs[] = {
diff --git a/src/gpu/ops/GrOvalOpFactory.cpp b/src/gpu/ops/GrOvalOpFactory.cpp
index 2362ebb..0cb7248 100644
--- a/src/gpu/ops/GrOvalOpFactory.cpp
+++ b/src/gpu/ops/GrOvalOpFactory.cpp
@@ -118,7 +118,7 @@
             fInRoundCapCenters =
                     {"inRoundCapCenters", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
         }
-        this->setVertexAttributes(&fInPosition, 7);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 7);
     }
 
     class Impl : public ProgramImpl {
@@ -298,7 +298,7 @@
         fInColor = MakeColorAttribute("inColor", wideColor);
         fInCircleEdge = {"inCircleEdge", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
         fInDashParams = {"inDashParams", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
-        this->setVertexAttributes(&fInPosition, 4);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 4);
     }
 
     class Impl : public ProgramImpl {
@@ -562,7 +562,7 @@
             fInEllipseOffset = {"inEllipseOffset", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
         }
         fInEllipseRadii = {"inEllipseRadii", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
-        this->setVertexAttributes(&fInPosition, 4);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 4);
     }
 
     class Impl : public ProgramImpl {
@@ -761,7 +761,7 @@
                                   kFloat2_GrSLType};
         }
         fInEllipseOffsets1 = {"inEllipseOffsets1", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
-        this->setVertexAttributes(&fInPosition, 4);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 4);
     }
 
     class Impl : public ProgramImpl {
diff --git a/src/gpu/ops/LatticeOp.cpp b/src/gpu/ops/LatticeOp.cpp
index 81d4fda..613146a 100644
--- a/src/gpu/ops/LatticeOp.cpp
+++ b/src/gpu/ops/LatticeOp.cpp
@@ -111,7 +111,7 @@
         fInTextureCoords = {"textureCoords", kFloat2_GrVertexAttribType, kFloat2_GrSLType};
         fInTextureDomain = {"textureDomain", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
         fInColor = MakeColorAttribute("color", wideColor);
-        this->setVertexAttributes(&fInPosition, 4);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 4);
     }
 
     const TextureSampler& onTextureSampler(int) const override { return fSampler; }
diff --git a/src/gpu/ops/PathInnerTriangulateOp.cpp b/src/gpu/ops/PathInnerTriangulateOp.cpp
index f434358..6f718a0 100644
--- a/src/gpu/ops/PathInnerTriangulateOp.cpp
+++ b/src/gpu/ops/PathInnerTriangulateOp.cpp
@@ -36,13 +36,14 @@
             // each patch that explicitly tells the shader what type of curve it is.
             fInstanceAttribs.emplace_back("curveType", kFloat_GrVertexAttribType, kFloat_GrSLType);
         }
-        this->setInstanceAttributes(fInstanceAttribs.data(), fInstanceAttribs.count());
+        this->setInstanceAttributesWithImplicitOffsets(fInstanceAttribs.data(),
+                                                       fInstanceAttribs.count());
         SkASSERT(fInstanceAttribs.count() <= kMaxInstanceAttribCount);
 
         if (!shaderCaps.vertexIDSupport()) {
             constexpr static Attribute kVertexIdxAttrib("vertexidx", kFloat_GrVertexAttribType,
                                                         kFloat_GrSLType);
-            this->setVertexAttributes(&kVertexIdxAttrib, 1);
+            this->setVertexAttributesWithImplicitOffsets(&kVertexIdxAttrib, 1);
         }
     }
 
diff --git a/src/gpu/ops/PathStencilCoverOp.cpp b/src/gpu/ops/PathStencilCoverOp.cpp
index 0772f32..f1cfe6b 100644
--- a/src/gpu/ops/PathStencilCoverOp.cpp
+++ b/src/gpu/ops/PathStencilCoverOp.cpp
@@ -36,14 +36,15 @@
         if (!shaderCaps.vertexIDSupport()) {
             constexpr static Attribute kUnitCoordAttrib("unitCoord", kFloat2_GrVertexAttribType,
                                                         kFloat2_GrSLType);
-            this->setVertexAttributes(&kUnitCoordAttrib, 1);
+            this->setVertexAttributesWithImplicitOffsets(&kUnitCoordAttrib, 1);
         }
         constexpr static Attribute kInstanceAttribs[] = {
             {"matrix2d", kFloat4_GrVertexAttribType, kFloat4_GrSLType},
             {"translate", kFloat2_GrVertexAttribType, kFloat2_GrSLType},
             {"pathBounds", kFloat4_GrVertexAttribType, kFloat4_GrSLType}
         };
-        this->setInstanceAttributes(kInstanceAttribs, SK_ARRAY_COUNT(kInstanceAttribs));
+        this->setInstanceAttributesWithImplicitOffsets(kInstanceAttribs,
+                                                       SK_ARRAY_COUNT(kInstanceAttribs));
     }
 
 private:
diff --git a/src/gpu/ops/QuadPerEdgeAA.cpp b/src/gpu/ops/QuadPerEdgeAA.cpp
index f73bd5d..da66d28 100644
--- a/src/gpu/ops/QuadPerEdgeAA.cpp
+++ b/src/gpu/ops/QuadPerEdgeAA.cpp
@@ -894,7 +894,7 @@
             fTexSubset = {"texSubset", kFloat4_GrVertexAttribType, kFloat4_GrSLType};
         }
 
-        this->setVertexAttributes(&fPosition, 6);
+        this->setVertexAttributesWithImplicitOffsets(&fPosition, 6);
     }
 
     const TextureSampler& onTextureSampler(int) const override { return fSampler; }
diff --git a/src/gpu/tessellate/shaders/GrPathTessellationShader.cpp b/src/gpu/tessellate/shaders/GrPathTessellationShader.cpp
index 439cf43..64211d7 100644
--- a/src/gpu/tessellate/shaders/GrPathTessellationShader.cpp
+++ b/src/gpu/tessellate/shaders/GrPathTessellationShader.cpp
@@ -25,7 +25,7 @@
                                        PatchAttribs::kNone) {
         constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
                                                      kFloat2_GrSLType};
-        this->setVertexAttributes(&kInputPointAttrib, 1);
+        this->setVertexAttributesWithImplicitOffsets(&kInputPointAttrib, 1);
     }
 
     int maxTessellationSegments(const GrShaderCaps&) const override { SkUNREACHABLE; }
diff --git a/src/gpu/tessellate/shaders/GrPathTessellationShader_Hardware.cpp b/src/gpu/tessellate/shaders/GrPathTessellationShader_Hardware.cpp
index b312f5d..d66fa3c 100644
--- a/src/gpu/tessellate/shaders/GrPathTessellationShader_Hardware.cpp
+++ b/src/gpu/tessellate/shaders/GrPathTessellationShader_Hardware.cpp
@@ -39,7 +39,7 @@
                                        GrPrimitiveType::kPatches, 5, viewMatrix, color, attribs) {
         constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
                                                      kFloat2_GrSLType};
-        this->setVertexAttributes(&kInputPointAttrib, 1);
+        this->setVertexAttributesWithImplicitOffsets(&kInputPointAttrib, 1);
         SkASSERT(this->vertexStride() * 5 ==
                  sizeof(SkPoint) * 4 + skgpu::PatchAttribsStride(fAttribs));
     }
@@ -180,7 +180,7 @@
                                        attribs) {
         constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
                                                      kFloat2_GrSLType};
-        this->setVertexAttributes(&kInputPointAttrib, 1);
+        this->setVertexAttributesWithImplicitOffsets(&kInputPointAttrib, 1);
         SkASSERT(this->vertexStride() * 4 ==
                  sizeof(SkPoint) * 4 + skgpu::PatchAttribsStride(fAttribs));
     }
diff --git a/src/gpu/tessellate/shaders/GrPathTessellationShader_MiddleOut.cpp b/src/gpu/tessellate/shaders/GrPathTessellationShader_MiddleOut.cpp
index f3654eb..e8a66ec 100644
--- a/src/gpu/tessellate/shaders/GrPathTessellationShader_MiddleOut.cpp
+++ b/src/gpu/tessellate/shaders/GrPathTessellationShader_MiddleOut.cpp
@@ -56,14 +56,15 @@
             // each patch that explicitly tells the shader what type of curve it is.
             fInstanceAttribs.emplace_back("curveType", kFloat_GrVertexAttribType, kFloat_GrSLType);
         }
-        this->setInstanceAttributes(fInstanceAttribs.data(), fInstanceAttribs.count());
+        this->setInstanceAttributesWithImplicitOffsets(fInstanceAttribs.data(),
+                                                       fInstanceAttribs.count());
         SkASSERT(fInstanceAttribs.count() <= kMaxInstanceAttribCount);
         SkASSERT(this->instanceStride() ==
                  sizeof(SkPoint) * 4 + skgpu::PatchAttribsStride(fAttribs));
 
         constexpr static Attribute kVertexAttrib("resolveLevel_and_idx", kFloat2_GrVertexAttribType,
                                                  kFloat2_GrSLType);
-        this->setVertexAttributes(&kVertexAttrib, 1);
+        this->setVertexAttributesWithImplicitOffsets(&kVertexAttrib, 1);
     }
 
     int maxTessellationSegments(const GrShaderCaps&) const override {
diff --git a/src/gpu/tessellate/shaders/GrStrokeTessellationShader.cpp b/src/gpu/tessellate/shaders/GrStrokeTessellationShader.cpp
index baf7055..6b10300 100644
--- a/src/gpu/tessellate/shaders/GrStrokeTessellationShader.cpp
+++ b/src/gpu/tessellate/shaders/GrStrokeTessellationShader.cpp
@@ -91,15 +91,15 @@
         fAttribs.emplace_back("curveTypeAttr", kFloat_GrVertexAttribType, kFloat_GrSLType);
     }
     if (fMode == Mode::kHardwareTessellation) {
-        this->setVertexAttributes(fAttribs.data(), fAttribs.count());
+        this->setVertexAttributesWithImplicitOffsets(fAttribs.data(), fAttribs.count());
         SkASSERT(this->vertexStride() == sizeof(SkPoint) * 5 + PatchAttribsStride(fPatchAttribs));
     } else {
-        this->setInstanceAttributes(fAttribs.data(), fAttribs.count());
+        this->setInstanceAttributesWithImplicitOffsets(fAttribs.data(), fAttribs.count());
         SkASSERT(this->instanceStride() == sizeof(SkPoint) * 5 + PatchAttribsStride(fPatchAttribs));
         if (!shaderCaps.vertexIDSupport()) {
             constexpr static Attribute kVertexAttrib("edgeID", kFloat_GrVertexAttribType,
                                                      kFloat_GrSLType);
-            this->setVertexAttributes(&kVertexAttrib, 1);
+            this->setVertexAttributesWithImplicitOffsets(&kVertexAttrib, 1);
         }
     }
     SkASSERT(fAttribs.count() <= kMaxAttribCount);
diff --git a/src/gpu/vk/GrVkPipeline.cpp b/src/gpu/vk/GrVkPipeline.cpp
index 118e751..e7466f0 100644
--- a/src/gpu/vk/GrVkPipeline.cpp
+++ b/src/gpu/vk/GrVkPipeline.cpp
@@ -100,39 +100,33 @@
 
     // setup attribute descriptions
     int attribIndex = 0;
-    size_t vertexAttributeOffset = 0;
     for (const auto& attrib : vertexAttribs) {
         VkVertexInputAttributeDescription& vkAttrib = attributeDesc[attribIndex];
         vkAttrib.location = attribIndex++;  // for now assume location = attribIndex
         vkAttrib.binding = vertexBinding;
         vkAttrib.format = attrib_type_to_vkformat(attrib.cpuType());
-        vkAttrib.offset = vertexAttributeOffset;
-        vertexAttributeOffset += attrib.sizeAlign4();
+        vkAttrib.offset = *attrib.offset();
     }
-    SkASSERT(vertexAttributeOffset == vertexAttribs.stride());
 
-    size_t instanceAttributeOffset = 0;
     for (const auto& attrib : instanceAttribs) {
         VkVertexInputAttributeDescription& vkAttrib = attributeDesc[attribIndex];
         vkAttrib.location = attribIndex++;  // for now assume location = attribIndex
         vkAttrib.binding = instanceBinding;
         vkAttrib.format = attrib_type_to_vkformat(attrib.cpuType());
-        vkAttrib.offset = instanceAttributeOffset;
-        instanceAttributeOffset += attrib.sizeAlign4();
+        vkAttrib.offset = *attrib.offset();
     }
-    SkASSERT(instanceAttributeOffset == instanceAttribs.stride());
 
     if (vaCount) {
         bindingDescs->push_back() = {
                 vertexBinding,
-                (uint32_t) vertexAttributeOffset,
+                (uint32_t) vertexAttribs.stride(),
                 VK_VERTEX_INPUT_RATE_VERTEX
         };
     }
     if (iaCount) {
         bindingDescs->push_back() = {
                 instanceBinding,
-                (uint32_t) instanceAttributeOffset,
+                (uint32_t) instanceAttribs.stride(),
                 VK_VERTEX_INPUT_RATE_INSTANCE
         };
     }
diff --git a/tests/GrMeshTest.cpp b/tests/GrMeshTest.cpp
index 47e6d48..64bde9a 100644
--- a/tests/GrMeshTest.cpp
+++ b/tests/GrMeshTest.cpp
@@ -469,15 +469,15 @@
         if (instanced) {
             fInstanceLocation = {"location", kFloat2_GrVertexAttribType, kHalf2_GrSLType};
             fInstanceColor = {"color", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType};
-            this->setInstanceAttributes(&fInstanceLocation, 2);
+            this->setInstanceAttributesWithImplicitOffsets(&fInstanceLocation, 2);
             if (hasVertexBuffer) {
                 fVertexPosition = {"vertex", kFloat2_GrVertexAttribType, kHalf2_GrSLType};
-                this->setVertexAttributes(&fVertexPosition, 1);
+                this->setVertexAttributesWithImplicitOffsets(&fVertexPosition, 1);
             }
         } else {
             fVertexPosition = {"vertex", kFloat2_GrVertexAttribType, kHalf2_GrSLType};
             fVertexColor = {"color", kUByte4_norm_GrVertexAttribType, kHalf4_GrSLType};
-            this->setVertexAttributes(&fVertexPosition, 2);
+            this->setVertexAttributesWithImplicitOffsets(&fVertexPosition, 2);
         }
     }
 
diff --git a/tests/GrPipelineDynamicStateTest.cpp b/tests/GrPipelineDynamicStateTest.cpp
index fbc952a..809c439 100644
--- a/tests/GrPipelineDynamicStateTest.cpp
+++ b/tests/GrPipelineDynamicStateTest.cpp
@@ -72,7 +72,7 @@
 
 private:
     PipelineDynamicStateTestProcessor() : INHERITED(kGrPipelineDynamicStateTestProcessor_ClassID) {
-        this->setVertexAttributes(kAttributes, SK_ARRAY_COUNT(kAttributes));
+        this->setVertexAttributesWithImplicitOffsets(kAttributes, SK_ARRAY_COUNT(kAttributes));
     }
 
     const Attribute& inVertex() const { return kAttributes[0]; }
diff --git a/tests/PrimitiveProcessorTest.cpp b/tests/PrimitiveProcessorTest.cpp
index c3b64d0..52bf499 100644
--- a/tests/PrimitiveProcessorTest.cpp
+++ b/tests/PrimitiveProcessorTest.cpp
@@ -115,7 +115,7 @@
                                                                    kFloat2_GrSLType};
                     }
                 }
-                this->setVertexAttributes(fAttributes.get(), numAttribs);
+                this->setVertexAttributesWithImplicitOffsets(fAttributes.get(), numAttribs);
             }
 
             int fNumAttribs;
diff --git a/tools/gpu/TestOps.cpp b/tools/gpu/TestOps.cpp
index f600729..cb0f90d 100644
--- a/tools/gpu/TestOps.cpp
+++ b/tools/gpu/TestOps.cpp
@@ -26,7 +26,7 @@
     GP(const SkMatrix& localMatrix, bool wideColor)
             : GrGeometryProcessor(kTestRectOp_ClassID), fLocalMatrix(localMatrix) {
         fInColor = MakeColorAttribute("color", wideColor);
-        this->setVertexAttributes(&fInPosition, 3);
+        this->setVertexAttributesWithImplicitOffsets(&fInPosition, 3);
     }
 
     const char* name() const override { return "TestRectOp::GP"; }