Starting to load meshes.
diff --git a/include/rive/shapes/contour_mesh_vertex.hpp b/include/rive/shapes/contour_mesh_vertex.hpp
index 51d8873..fa0942f 100644
--- a/include/rive/shapes/contour_mesh_vertex.hpp
+++ b/include/rive/shapes/contour_mesh_vertex.hpp
@@ -1,7 +1,7 @@
 #ifndef _RIVE_CONTOUR_MESH_VERTEX_HPP_
 #define _RIVE_CONTOUR_MESH_VERTEX_HPP_
 #include "rive/generated/shapes/contour_mesh_vertex_base.hpp"
-#include <stdio.h>
+
 namespace rive {
     class ContourMeshVertex : public ContourMeshVertexBase {
     public:
diff --git a/include/rive/shapes/image.hpp b/include/rive/shapes/image.hpp
index 84dab09..491d9ec 100644
--- a/include/rive/shapes/image.hpp
+++ b/include/rive/shapes/image.hpp
@@ -4,11 +4,15 @@
 #include "rive/assets/file_asset_referencer.hpp"
 namespace rive {
     class ImageAsset;
+    class Mesh;
     class Image : public ImageBase, public FileAssetReferencer {
     private:
         ImageAsset* m_ImageAsset = nullptr;
+        Mesh* m_Mesh = nullptr;
 
     public:
+        Mesh* mesh() const;
+        void setMesh(Mesh* mesh);
         ImageAsset* imageAsset() const { return m_ImageAsset; }
         void draw(Renderer* renderer) override;
         StatusCode import(ImportStack& importStack) override;
diff --git a/include/rive/shapes/mesh.hpp b/include/rive/shapes/mesh.hpp
index 7e38f4f..ee3b0f2 100644
--- a/include/rive/shapes/mesh.hpp
+++ b/include/rive/shapes/mesh.hpp
@@ -1,10 +1,21 @@
 #ifndef _RIVE_MESH_HPP_
 #define _RIVE_MESH_HPP_
 #include "rive/generated/shapes/mesh_base.hpp"
-#include <stdio.h>
+
 namespace rive {
+    class MeshVertex;
     class Mesh : public MeshBase {
+    protected:
+        std::vector<MeshVertex*> m_Vertices;
+
     public:
+        StatusCode onAddedDirty(CoreContext* context) override;
+        void markDrawableDirty();
+        void addVertex(MeshVertex* vertex);
+
+#ifdef TESTING
+        std::vector<MeshVertex*>& vertices() { return m_Vertices; }
+#endif
     };
 } // namespace rive
 
diff --git a/include/rive/shapes/mesh_vertex.hpp b/include/rive/shapes/mesh_vertex.hpp
index bd58ccd..0831fac 100644
--- a/include/rive/shapes/mesh_vertex.hpp
+++ b/include/rive/shapes/mesh_vertex.hpp
@@ -5,6 +5,8 @@
 namespace rive {
     class MeshVertex : public MeshVertexBase {
     public:
+        void markGeometryDirty() override;
+        StatusCode onAddedDirty(CoreContext* context) override;
     };
 } // namespace rive
 
diff --git a/include/rive/shapes/path.hpp b/include/rive/shapes/path.hpp
index a1e20cf..73ffdca 100644
--- a/include/rive/shapes/path.hpp
+++ b/include/rive/shapes/path.hpp
@@ -39,7 +39,6 @@
     public:
         ~Path();
         Shape* shape() const { return m_Shape; }
-        StatusCode onAddedDirty(CoreContext* context) override;
         StatusCode onAddedClean(CoreContext* context) override;
         void buildDependencies() override;
         virtual const Mat2D& pathTransform() const;
diff --git a/include/rive/shapes/path_vertex.hpp b/include/rive/shapes/path_vertex.hpp
index e8e37fb..7ccd4b4 100644
--- a/include/rive/shapes/path_vertex.hpp
+++ b/include/rive/shapes/path_vertex.hpp
@@ -5,29 +5,10 @@
 #include "rive/math/mat2d.hpp"
 namespace rive {
     class PathVertex : public PathVertexBase {
-        friend class Weight;
-
-    private:
-        Weight* m_Weight = nullptr;
-        void weight(Weight* value) { m_Weight = value; }
 
     public:
         StatusCode onAddedDirty(CoreContext* context) override;
-        template <typename T> T* weight() { return m_Weight->as<T>(); }
-        virtual void deform(const Mat2D& worldTransform,
-                            const float* boneTransforms);
-        bool hasWeight() { return m_Weight != nullptr; }
-        Vec2D renderTranslation();
-
-    protected:
-        void markPathDirty();
-        void xChanged() override;
-        void yChanged() override;
-
-#ifdef TESTING
-    public:
-        Weight* weight() { return m_Weight; }
-#endif
+        void markGeometryDirty() override;
     };
 } // namespace rive
 
diff --git a/include/rive/shapes/vertex.hpp b/include/rive/shapes/vertex.hpp
index 555d170..ec09902 100644
--- a/include/rive/shapes/vertex.hpp
+++ b/include/rive/shapes/vertex.hpp
@@ -1,10 +1,32 @@
 #ifndef _RIVE_VERTEX_HPP_
 #define _RIVE_VERTEX_HPP_
+#include "rive/bones/weight.hpp"
 #include "rive/generated/shapes/vertex_base.hpp"
-#include <stdio.h>
+#include "rive/math/mat2d.hpp"
 namespace rive {
     class Vertex : public VertexBase {
+        friend class Weight;
+
+    private:
+        Weight* m_Weight = nullptr;
+        void weight(Weight* value) { m_Weight = value; }
+
     public:
+        template <typename T> T* weight() { return m_Weight->as<T>(); }
+        virtual void deform(const Mat2D& worldTransform,
+                            const float* boneTransforms);
+        bool hasWeight() { return m_Weight != nullptr; }
+        Vec2D renderTranslation();
+
+    protected:
+        virtual void markGeometryDirty() = 0;
+        void xChanged() override;
+        void yChanged() override;
+
+#ifdef TESTING
+    public:
+        Weight* weight() { return m_Weight; }
+#endif
     };
 } // namespace rive
 
diff --git a/src/shapes/cubic_asymmetric_vertex.cpp b/src/shapes/cubic_asymmetric_vertex.cpp
index 8ba1ab0..96fb60a 100644
--- a/src/shapes/cubic_asymmetric_vertex.cpp
+++ b/src/shapes/cubic_asymmetric_vertex.cpp
@@ -29,13 +29,13 @@
 void CubicAsymmetricVertex::rotationChanged() {
     m_InValid = false;
     m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicAsymmetricVertex::inDistanceChanged() {
     m_InValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicAsymmetricVertex::outDistanceChanged() {
     m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
diff --git a/src/shapes/cubic_detached_vertex.cpp b/src/shapes/cubic_detached_vertex.cpp
index 3a76fb9..c60acc8 100644
--- a/src/shapes/cubic_detached_vertex.cpp
+++ b/src/shapes/cubic_detached_vertex.cpp
@@ -28,17 +28,17 @@
 
 void CubicDetachedVertex::inRotationChanged() {
     m_InValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicDetachedVertex::inDistanceChanged() {
     m_InValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicDetachedVertex::outRotationChanged() {
     m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicDetachedVertex::outDistanceChanged() {
     m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
diff --git a/src/shapes/cubic_mirrored_vertex.cpp b/src/shapes/cubic_mirrored_vertex.cpp
index 068887c..8757d69 100644
--- a/src/shapes/cubic_mirrored_vertex.cpp
+++ b/src/shapes/cubic_mirrored_vertex.cpp
@@ -23,9 +23,9 @@
 
 void CubicMirroredVertex::rotationChanged() {
     m_InValid = m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
 void CubicMirroredVertex::distanceChanged() {
     m_InValid = m_OutValid = false;
-    markPathDirty();
+    markGeometryDirty();
 }
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp
index 05da6ef..d1a01e3 100644
--- a/src/shapes/image.cpp
+++ b/src/shapes/image.cpp
@@ -55,3 +55,6 @@
     twin->m_ImageAsset = m_ImageAsset;
     return twin;
 }
+
+void Image::setMesh(Mesh* mesh) { m_Mesh = mesh; }
+Mesh* Image::mesh() const { return m_Mesh; }
\ No newline at end of file
diff --git a/src/shapes/mesh.cpp b/src/shapes/mesh.cpp
new file mode 100644
index 0000000..207410c
--- /dev/null
+++ b/src/shapes/mesh.cpp
@@ -0,0 +1,24 @@
+#include "rive/shapes/mesh.hpp"
+#include "rive/shapes/image.hpp"
+
+using namespace rive;
+
+void Mesh::markDrawableDirty() {
+    // TODO: add dirty for rebuilding vertex buffer (including deform).
+}
+
+void Mesh::addVertex(MeshVertex* vertex) { m_Vertices.push_back(vertex); }
+
+StatusCode Mesh::onAddedDirty(CoreContext* context) {
+    StatusCode result = Super::onAddedDirty(context);
+    if (result != StatusCode::Ok) {
+        return result;
+    }
+
+    if (!parent()->is<Image>()) {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<Image>()->setMesh(this);
+
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/shapes/mesh_vertex.cpp b/src/shapes/mesh_vertex.cpp
new file mode 100644
index 0000000..1d2439a
--- /dev/null
+++ b/src/shapes/mesh_vertex.cpp
@@ -0,0 +1,19 @@
+#include "rive/shapes/mesh_vertex.hpp"
+#include "rive/shapes/mesh.hpp"
+
+using namespace rive;
+void MeshVertex::markGeometryDirty() {
+    parent()->as<Mesh>()->markDrawableDirty();
+}
+
+StatusCode MeshVertex::onAddedDirty(CoreContext* context) {
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok) {
+        return code;
+    }
+    if (!parent()->is<Mesh>()) {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<Mesh>()->addVertex(this);
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/shapes/path.cpp b/src/shapes/path.cpp
index 0e20a37..4f877a4 100644
--- a/src/shapes/path.cpp
+++ b/src/shapes/path.cpp
@@ -12,14 +12,6 @@
 
 Path::~Path() { delete m_CommandPath; }
 
-StatusCode Path::onAddedDirty(CoreContext* context) {
-    StatusCode code = Super::onAddedDirty(context);
-    if (code != StatusCode::Ok) {
-        return code;
-    }
-    return StatusCode::Ok;
-}
-
 StatusCode Path::onAddedClean(CoreContext* context) {
     StatusCode code = Super::onAddedClean(context);
     if (code != StatusCode::Ok) {
@@ -91,10 +83,10 @@
 
             Vec2D pos = point.renderTranslation();
 
-            Vec2D toPrev = (prev->is<CubicVertex>()
-                                ? prev->as<CubicVertex>()->renderOut()
-                                : prev->renderTranslation())
-                         - pos;
+            Vec2D toPrev =
+                (prev->is<CubicVertex>() ? prev->as<CubicVertex>()->renderOut()
+                                         : prev->renderTranslation()) -
+                pos;
 
             auto toPrevLength = toPrev.length();
             toPrev[0] /= toPrevLength;
@@ -102,10 +94,10 @@
 
             auto next = vertices[1];
 
-            Vec2D toNext = (next->is<CubicVertex>()
-                                ? next->as<CubicVertex>()->renderIn()
-                                : next->renderTranslation())
-                         - pos;
+            Vec2D toNext =
+                (next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn()
+                                         : next->renderTranslation()) -
+                pos;
             auto toNextLength = toNext.length();
             toNext[0] /= toNextLength;
             toNext[1] /= toNextLength;
@@ -117,8 +109,10 @@
             commandPath.moveTo(startInX = startX = translation[0],
                                startInY = startY = translation[1]);
 
-            Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, icircleConstant * renderRadius);
-            Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, icircleConstant * renderRadius);
+            Vec2D outPoint =
+                Vec2D::scaleAndAdd(pos, toPrev, icircleConstant * renderRadius);
+            Vec2D inPoint =
+                Vec2D::scaleAndAdd(pos, toNext, icircleConstant * renderRadius);
             Vec2D posNext = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
             commandPath.cubicTo(outPoint[0],
                                 outPoint[1],
@@ -167,8 +161,8 @@
 
                 Vec2D toNext = (next->is<CubicVertex>()
                                     ? next->as<CubicVertex>()->renderIn()
-                                    : next->renderTranslation())
-                             - pos;
+                                    : next->renderTranslation()) -
+                               pos;
                 auto toNextLength = toNext.length();
                 toNext[0] /= toNextLength;
                 toNext[1] /= toNextLength;
@@ -176,7 +170,8 @@
                 float renderRadius =
                     std::min(toPrevLength, std::min(toNextLength, radius));
 
-                Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
+                Vec2D translation =
+                    Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
                 if (prevIsCubic) {
                     commandPath.cubicTo(outX,
                                         outY,
@@ -188,8 +183,10 @@
                     commandPath.lineTo(translation[0], translation[1]);
                 }
 
-                Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, icircleConstant * renderRadius);
-                Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, icircleConstant * renderRadius);
+                Vec2D outPoint = Vec2D::scaleAndAdd(
+                    pos, toPrev, icircleConstant * renderRadius);
+                Vec2D inPoint = Vec2D::scaleAndAdd(
+                    pos, toNext, icircleConstant * renderRadius);
                 Vec2D posNext = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
                 commandPath.cubicTo(outPoint[0],
                                     outPoint[1],
@@ -325,9 +322,11 @@
 
                     auto renderRadius = std::min(
                         toPrevLength, std::min(toNextLength, point.radius()));
-                    Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
+                    Vec2D translation =
+                        Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
 
-                    Vec2D out = Vec2D::scaleAndAdd(pos, toPrev, icircleConstant * renderRadius);
+                    Vec2D out = Vec2D::scaleAndAdd(
+                        pos, toPrev, icircleConstant * renderRadius);
                     {
                         auto v1 = new DisplayCubicVertex(
                             translation, out, translation);
@@ -337,7 +336,8 @@
 
                     translation = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
 
-                    Vec2D in = Vec2D::scaleAndAdd(pos, toNext, icircleConstant * renderRadius);
+                    Vec2D in = Vec2D::scaleAndAdd(
+                        pos, toNext, icircleConstant * renderRadius);
                     auto v2 =
                         new DisplayCubicVertex(in, translation, translation);
 
diff --git a/src/shapes/path_vertex.cpp b/src/shapes/path_vertex.cpp
index 32f90a4..6e070b3 100644
--- a/src/shapes/path_vertex.cpp
+++ b/src/shapes/path_vertex.cpp
@@ -3,13 +3,6 @@
 
 using namespace rive;
 
-Vec2D PathVertex::renderTranslation() {
-    if (hasWeight()) {
-        return m_Weight->translation();
-    }
-    return Vec2D(x(), y());
-}
-
 StatusCode PathVertex::onAddedDirty(CoreContext* context) {
     StatusCode code = Super::onAddedDirty(context);
     if (code != StatusCode::Ok) {
@@ -22,25 +15,11 @@
     return StatusCode::Ok;
 }
 
-void PathVertex::markPathDirty() {
+void PathVertex::markGeometryDirty() {
     if (parent() == nullptr) {
         // This is an acceptable condition as the parametric paths create points
         // that are not part of the core context.
         return;
     }
     parent()->as<Path>()->markPathDirty();
-}
-
-void PathVertex::xChanged() { markPathDirty(); }
-void PathVertex::yChanged() { markPathDirty(); }
-
-void PathVertex::deform(const Mat2D& worldTransform,
-                        const float* boneTransforms) {
-    Weight::deform(x(),
-                   y(),
-                   m_Weight->indices(),
-                   m_Weight->values(),
-                   worldTransform,
-                   boneTransforms,
-                   m_Weight->translation());
 }
\ No newline at end of file
diff --git a/src/shapes/straight_vertex.cpp b/src/shapes/straight_vertex.cpp
index 07d0d2b..30c30cd 100644
--- a/src/shapes/straight_vertex.cpp
+++ b/src/shapes/straight_vertex.cpp
@@ -2,4 +2,4 @@
 
 using namespace rive;
 
-void StraightVertex::radiusChanged() { markPathDirty(); }
+void StraightVertex::radiusChanged() { markGeometryDirty(); }
diff --git a/src/shapes/vertex.cpp b/src/shapes/vertex.cpp
new file mode 100644
index 0000000..4375555
--- /dev/null
+++ b/src/shapes/vertex.cpp
@@ -0,0 +1,23 @@
+#include "rive/shapes/vertex.hpp"
+
+using namespace rive;
+
+Vec2D Vertex::renderTranslation() {
+    if (hasWeight()) {
+        return m_Weight->translation();
+    }
+    return Vec2D(x(), y());
+}
+
+void Vertex::xChanged() { markGeometryDirty(); }
+void Vertex::yChanged() { markGeometryDirty(); }
+
+void Vertex::deform(const Mat2D& worldTransform, const float* boneTransforms) {
+    Weight::deform(x(),
+                   y(),
+                   m_Weight->indices(),
+                   m_Weight->values(),
+                   worldTransform,
+                   boneTransforms,
+                   m_Weight->translation());
+}
\ No newline at end of file
diff --git a/test/assets/tape.riv b/test/assets/tape.riv
new file mode 100644
index 0000000..cd4fd79
--- /dev/null
+++ b/test/assets/tape.riv
Binary files differ
diff --git a/test/image_mesh_test.cpp b/test/image_mesh_test.cpp
new file mode 100644
index 0000000..3a983c7
--- /dev/null
+++ b/test/image_mesh_test.cpp
@@ -0,0 +1,27 @@
+#include <rive/core/binary_reader.hpp>
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/image.hpp>
+#include <rive/shapes/mesh.hpp>
+#include <rive/assets/image_asset.hpp>
+#include <rive/relative_local_asset_resolver.hpp>
+#include "no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("image with mesh loads correctly", "[assets]") {
+    RiveFileReader reader("../../test/assets/tape.riv");
+    auto file = reader.file();
+
+    auto node = file->artboard()->find("Tape body.png");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Image>());
+    auto tape = node->as<rive::Image>();
+    REQUIRE(tape->imageAsset() != nullptr);
+    REQUIRE(tape->imageAsset()->decodedByteSize == 70903);
+    REQUIRE(tape->mesh() != nullptr);
+    REQUIRE(tape->mesh()->vertices().size() == 24);
+}