Starting to render something for meshes.
diff --git a/include/rive/bones/skinnable.hpp b/include/rive/bones/skinnable.hpp
index 0cbce5a..d71aa38 100644
--- a/include/rive/bones/skinnable.hpp
+++ b/include/rive/bones/skinnable.hpp
@@ -1,6 +1,8 @@
 #ifndef _RIVE_SKINNABLE_HPP_
 #define _RIVE_SKINNABLE_HPP_
 
+#include "rive/rive_types.hpp"
+
 namespace rive {
     class Skin;
     class Component;
diff --git a/include/rive/shapes/mesh.hpp b/include/rive/shapes/mesh.hpp
index 69ea246..5bef62d 100644
--- a/include/rive/shapes/mesh.hpp
+++ b/include/rive/shapes/mesh.hpp
@@ -1,19 +1,25 @@
 #ifndef _RIVE_MESH_HPP_
 #define _RIVE_MESH_HPP_
 #include "rive/generated/shapes/mesh_base.hpp"
+#include "rive/bones/skinnable.hpp"
 #include "rive/span.hpp"
 #include "rive/refcnt.hpp"
+#include "rive/renderer.hpp"
 
 namespace rive {
     class MeshVertex;
 
-    class Mesh : public MeshBase {
+    class Mesh : public MeshBase, public Skinnable {
 
     protected:
         class IndexBuffer : public std::vector<uint16_t>, public RefCnt {};
         std::vector<MeshVertex*> m_Vertices;
         rcp<IndexBuffer> m_IndexBuffer;
 
+        rcp<RenderBuffer> m_IndexRenderBuffer;
+        rcp<RenderBuffer> m_VertexRenderBuffer;
+        rcp<RenderBuffer> m_UVRenderBuffer;
+
     public:
         StatusCode onAddedDirty(CoreContext* context) override;
         StatusCode onAddedClean(CoreContext* context) override;
@@ -21,6 +27,14 @@
         void addVertex(MeshVertex* vertex);
         void decodeTriangleIndexBytes(Span<const uint8_t> value) override;
         void copyTriangleIndexBytes(const MeshBase& object) override;
+        void buildDependencies() override;
+        void draw(Renderer* renderer,
+                  const RenderImage* image,
+                  BlendMode blendMode,
+                  float opacity);
+
+        void updateVertexRenderBuffer(Renderer* renderer);
+        void markSkinDirty() override;
 #ifdef TESTING
         std::vector<MeshVertex*>& vertices() { return m_Vertices; }
         rcp<IndexBuffer> indices() { return m_IndexBuffer; }
diff --git a/skia/renderer/src/skia_renderer.cpp b/skia/renderer/src/skia_renderer.cpp
index d1d60ab..96a9c51 100644
--- a/skia/renderer/src/skia_renderer.cpp
+++ b/skia/renderer/src/skia_renderer.cpp
@@ -11,19 +11,16 @@
 class SkiaBuffer : public RenderBuffer {
     const size_t m_ElemSize;
     void* m_Buffer;
+
 public:
-    SkiaBuffer(const void* src, size_t count, size_t elemSize)
-        : RenderBuffer(count)
-        , m_ElemSize(elemSize)
-    {
+    SkiaBuffer(const void* src, size_t count, size_t elemSize) :
+        RenderBuffer(count), m_ElemSize(elemSize) {
         size_t bytes = count * elemSize;
         m_Buffer = malloc(bytes);
         memcpy(m_Buffer, src, bytes);
     }
-    
-    ~SkiaBuffer() {
-        free(m_Buffer);
-    }
+
+    ~SkiaBuffer() { free(m_Buffer); }
 
     const float* f32s() const {
         assert(m_ElemSize == sizeof(float));
@@ -34,7 +31,7 @@
         assert(m_ElemSize == sizeof(uint16_t));
         return static_cast<const uint16_t*>(m_Buffer);
     }
-    
+
     const SkPoint* points() const {
         return reinterpret_cast<const SkPoint*>(this->f32s());
     }
@@ -45,7 +42,8 @@
 };
 
 template <typename T> rcp<RenderBuffer> make_buffer(Span<T> span) {
-    return rcp<RenderBuffer>(new SkiaBuffer(span.data(), span.size(), sizeof(T)));
+    return rcp<RenderBuffer>(
+        new SkiaBuffer(span.data(), span.size(), sizeof(T)));
 }
 
 class SkiaRenderShader : public RenderShader {
@@ -143,20 +141,21 @@
                                  float opacity) {
     // need our vertices and uvs to agree
     assert(vertices->count() == uvCoords->count());
-    // vertices and uvs are arrays of floats, so we need their counts to be even,
-    // since we treat them as arrays of points
+    // vertices and uvs are arrays of floats, so we need their counts to be
+    // even, since we treat them as arrays of points
     assert((vertices->count() & 1) == 0);
 
     // We do this because our UVs are normalized, but Skia expects them to be
     // sized to the shader (i.e. 0..width, 0..height).
     // To accomdate this, we effectively scaling the image down to 0..1 to
     // match the scale of the UVs.
-    const auto scale = SkMatrix::Scale(1.0f/image->width(), 1.0f/image->height());
+    const auto scale =
+        SkMatrix::Scale(1.0f / image->width(), 1.0f / image->height());
 
     auto skiaImage = reinterpret_cast<const SkiaRenderImage*>(image)->skImage();
     const SkSamplingOptions sampling(SkFilterMode::kLinear);
-    auto shader = skiaImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp,
-                                        sampling, &scale);
+    auto shader = skiaImage->makeShader(
+        SkTileMode::kClamp, SkTileMode::kClamp, sampling, &scale);
 
     SkPaint paint;
     paint.setAlphaf(opacity);
@@ -166,6 +165,7 @@
     const SkColor* no_colors = nullptr;
     auto vertexMode = SkVertices::kTriangles_VertexMode;
     const int vertexCount = vertices->count() >> 1;
+    // clang-format off
     auto vt = SkVertices::MakeCopy(vertexMode,
                                    vertexCount,
                                    SkiaBuffer::Cast(vertices.get())->points(),
@@ -173,6 +173,7 @@
                                    no_colors,
                                    indices->count(),
                                    SkiaBuffer::Cast(indices.get())->u16s());
+    // clang-format on
 
     // The blend mode is ignored if we don't have colors && uvs
     m_Canvas->drawVertices(vt, SkBlendMode::kModulate, paint);
@@ -180,19 +181,22 @@
 
 bool SkiaRenderImage::decode(Span<const uint8_t> encodedData) {
 
-    sk_sp<SkData> data = SkData::MakeWithoutCopy(encodedData.data(),
-                                                 encodedData.size());
+    sk_sp<SkData> data =
+        SkData::MakeWithoutCopy(encodedData.data(), encodedData.size());
     m_SkImage = SkImage::MakeFromEncoded(data);
     m_Width = m_SkImage->width();
     m_Height = m_SkImage->height();
     return true;
 }
 
-rcp<RenderShader> SkiaRenderImage::makeShader(RenderTileMode tx, RenderTileMode ty,
+rcp<RenderShader> SkiaRenderImage::makeShader(RenderTileMode tx,
+                                              RenderTileMode ty,
                                               const Mat2D* localMatrix) const {
-    const SkMatrix lm = localMatrix ? ToSkia::convert(*localMatrix) : SkMatrix();
+    const SkMatrix lm =
+        localMatrix ? ToSkia::convert(*localMatrix) : SkMatrix();
     const SkSamplingOptions options(SkFilterMode::kLinear);
-    auto sh = m_SkImage->makeShader(ToSkia::convert(tx), ToSkia::convert(ty), options, &lm);
+    auto sh = m_SkImage->makeShader(
+        ToSkia::convert(tx), ToSkia::convert(ty), options, &lm);
     return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
 }
 
@@ -211,38 +215,57 @@
     RenderPaint* makeRenderPaint() { return new SkiaRenderPaint(); }
     RenderImage* makeRenderImage() { return new SkiaRenderImage(); }
 
-    rcp<RenderShader> makeLinearGradient(float sx, float sy, float ex, float ey,
-                                         const ColorInt colors[], const float stops[],
-                                         int count, RenderTileMode mode,
-                                         const Mat2D* localm)
-    {
-        const SkPoint pts[] = { {sx, sy}, {ex, ey} };
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[],
+                                         const float stops[],
+                                         int count,
+                                         RenderTileMode mode,
+                                         const Mat2D* localm) {
+        const SkPoint pts[] = {{sx, sy}, {ex, ey}};
         const SkMatrix lm = localm ? ToSkia::convert(*localm) : SkMatrix();
-        auto sh = SkGradientShader::MakeLinear(pts, (const SkColor*)colors, stops, count,
-                                               ToSkia::convert(mode), 0, &lm);
+        auto sh = SkGradientShader::MakeLinear(pts,
+                                               (const SkColor*)colors,
+                                               stops,
+                                               count,
+                                               ToSkia::convert(mode),
+                                               0,
+                                               &lm);
         return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
     }
 
-    rcp<RenderShader> makeRadialGradient(float cx, float cy, float radius,
-                                         const ColorInt colors[], const float stops[],
-                                         int count, RenderTileMode mode,
-                                         const Mat2D* localm)
-    {
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[],
+                                         const float stops[],
+                                         int count,
+                                         RenderTileMode mode,
+                                         const Mat2D* localm) {
         const SkMatrix lm = localm ? ToSkia::convert(*localm) : SkMatrix();
-        auto sh = SkGradientShader::MakeRadial({cx, cy}, radius,
-                                               (const SkColor*)colors, stops, count,
-                                               ToSkia::convert(mode), 0, &lm);
+        auto sh = SkGradientShader::MakeRadial({cx, cy},
+                                               radius,
+                                               (const SkColor*)colors,
+                                               stops,
+                                               count,
+                                               ToSkia::convert(mode),
+                                               0,
+                                               &lm);
         return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
     }
 
-    rcp<RenderShader> makeSweepGradient(float cx, float cy,
-                                        const ColorInt colors[], const float stops[],
-                                        int count, RenderTileMode mode,
-                                        const Mat2D* localm)
-   {
-       const SkMatrix lm = localm ? ToSkia::convert(*localm) : SkMatrix();
-       auto sh = SkGradientShader::MakeSweep(cx, cy, (const SkColor*)colors, stops, count,
-                                             0, &lm);
-       return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
-   }
+    rcp<RenderShader> makeSweepGradient(float cx,
+                                        float cy,
+                                        const ColorInt colors[],
+                                        const float stops[],
+                                        int count,
+                                        RenderTileMode mode,
+                                        const Mat2D* localm) {
+        const SkMatrix lm = localm ? ToSkia::convert(*localm) : SkMatrix();
+        auto sh = SkGradientShader::MakeSweep(
+            cx, cy, (const SkColor*)colors, stops, count, 0, &lm);
+        return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
+    }
 } // namespace rive
diff --git a/src/bones/skinnable.cpp b/src/bones/skinnable.cpp
index 8dabc6f..781267b 100644
--- a/src/bones/skinnable.cpp
+++ b/src/bones/skinnable.cpp
@@ -1,5 +1,6 @@
 #include "rive/bones/skinnable.hpp"
 #include "rive/shapes/points_path.hpp"
+#include "rive/shapes/mesh.hpp"
 
 using namespace rive;
 
@@ -8,6 +9,9 @@
         case PointsPath::typeKey:
             return component->as<PointsPath>();
             break;
+        case Mesh::typeKey:
+            return component->as<Mesh>();
+            break;
     }
     return nullptr;
 }
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp
index d1a01e3..7684707 100644
--- a/src/shapes/image.cpp
+++ b/src/shapes/image.cpp
@@ -3,6 +3,7 @@
 #include "rive/importers/backboard_importer.hpp"
 #include "rive/assets/file_asset.hpp"
 #include "rive/assets/image_asset.hpp"
+#include "rive/shapes/mesh.hpp"
 
 using namespace rive;
 
@@ -24,7 +25,11 @@
     renderer->transform(worldTransform());
     renderer->translate(-width / 2.0f, -height / 2.0f);
 
-    renderer->drawImage(renderImage, blendMode(), renderOpacity());
+    if (m_Mesh != nullptr) {
+        m_Mesh->draw(renderer, renderImage, blendMode(), renderOpacity());
+    } else {
+        renderer->drawImage(renderImage, blendMode(), renderOpacity());
+    }
 
     renderer->restore();
 }
diff --git a/src/shapes/mesh.cpp b/src/shapes/mesh.cpp
index 9dbb9b7..8dd26a1 100644
--- a/src/shapes/mesh.cpp
+++ b/src/shapes/mesh.cpp
@@ -1,13 +1,11 @@
 #include "rive/shapes/mesh.hpp"
 #include "rive/shapes/image.hpp"
+#include "rive/shapes/mesh_vertex.hpp"
 #include <limits>
-#include <cassert>
 
 using namespace rive;
 
-void Mesh::markDrawableDirty() {
-    // TODO: add dirty for rebuilding vertex buffer (including deform).
-}
+void Mesh::markDrawableDirty() { m_VertexRenderBuffer = nullptr; }
 
 void Mesh::addVertex(MeshVertex* vertex) { m_Vertices.push_back(vertex); }
 
@@ -28,6 +26,11 @@
 }
 
 StatusCode Mesh::onAddedClean(CoreContext* context) {
+    // Make sure Core found indices in the file for this Mesh.
+    if (m_IndexBuffer == nullptr) {
+        return StatusCode::InvalidObject;
+    }
+
     // Check the indices are all in range. We should consider having a better
     // error reporting system to the implementor.
     for (auto index : *m_IndexBuffer) {
@@ -53,4 +56,50 @@
 
 void Mesh::copyTriangleIndexBytes(const MeshBase& object) {
     m_IndexBuffer = object.as<Mesh>()->m_IndexBuffer;
+}
+
+void Mesh::markSkinDirty() {}
+
+void Mesh::buildDependencies() {
+    Super::buildDependencies();
+
+    // TODO: This logic really needs to move to a "intializeGraphics/Renderer"
+    // method that is passed a reference to the Renderer.
+
+    // TODO: if this is an instance, share the index and uv buffer from the
+    // source. Consider storing an m_SourceMesh.
+
+    std::vector<float> uv = std::vector<float>(m_Vertices.size() * 2);
+    std::size_t index = 0;
+
+    for (auto vertex : m_Vertices) {
+        uv[index++] = vertex->u();
+        uv[index++] = vertex->v();
+    }
+    m_UVRenderBuffer = makeBufferF32(uv.data(), uv.size());
+    m_IndexRenderBuffer =
+        makeBufferU16(m_IndexBuffer->data(), m_IndexBuffer->size());
+}
+
+void Mesh::draw(Renderer* renderer,
+                const RenderImage* image,
+                BlendMode blendMode,
+                float opacity) {
+    if (m_VertexRenderBuffer == nullptr) {
+
+        std::vector<float> vertices = std::vector<float>(m_Vertices.size() * 2);
+        std::size_t index = 0;
+        for (auto vertex : m_Vertices) {
+            vertices[index++] = vertex->x();
+            vertices[index++] = vertex->y();
+        }
+        m_VertexRenderBuffer = makeBufferF32(vertices.data(), vertices.size());
+    }
+
+    renderer->drawImageMesh(image,
+                            m_VertexRenderBuffer,
+                            m_UVRenderBuffer,
+                            m_IndexRenderBuffer,
+                            blendMode,
+                            opacity);
 }
\ No newline at end of file
diff --git a/test/no_op_renderer.cpp b/test/no_op_renderer.cpp
index a07b17e..a85f778 100644
--- a/test/no_op_renderer.cpp
+++ b/test/no_op_renderer.cpp
@@ -6,8 +6,10 @@
     RenderPath* makeRenderPath() { return new NoOpRenderPath(); }
     RenderImage* makeRenderImage() { return new NoOpRenderImage(); }
 
-    rcp<RenderShader> makeLinearGradient(float sx, float sy,
-                                         float ex, float ey,
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
                                          const ColorInt colors[],
                                          const float stops[],
                                          int count,
@@ -16,7 +18,9 @@
         return nullptr;
     }
 
-    rcp<RenderShader> makeRadialGradient(float cx, float cy, float radius,
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
                                          const ColorInt colors[],
                                          const float stops[],
                                          int count,
@@ -25,7 +29,8 @@
         return nullptr;
     }
 
-    rcp<RenderShader> makeSweepGradient(float cx, float cy,
+    rcp<RenderShader> makeSweepGradient(float cx,
+                                        float cy,
                                         const ColorInt colors[],
                                         const float stops[],
                                         int count,
@@ -33,4 +38,13 @@
         return nullptr;
     }
 
+    rcp<RenderBuffer> makeBufferU16(const uint16_t[], size_t count) {
+        return nullptr;
+    }
+    rcp<RenderBuffer> makeBufferU32(const uint32_t[], size_t count) {
+        return nullptr;
+    }
+    rcp<RenderBuffer> makeBufferF32(const float[], size_t count) {
+        return nullptr;
+    }
 } // namespace rive