Merge branch 'master' of https://github.com/rive-app/rive-cpp into low_level_rendering
diff --git a/.clang-format b/.clang-format
index fe1a732..40aff81 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,10 +1,10 @@
+---
 BasedOnStyle: LLVM
 IndentWidth: 4
 UseTab: ForIndentation
 TabWidth: 4
 BreakBeforeBraces: Allman
 IndentCaseLabels: true
-Language: Cpp
 # Force pointers to the type for C++.
 DerivePointerAlignment: false
 PointerAlignment: Left
@@ -16,7 +16,6 @@
 BinPackArguments: false
 BinPackParameters: false
 AlignAfterOpenBracket: Align
-BreakBeforeBraces: Custom
 SortIncludes: false
 BraceWrapping:
   AfterEnum: true
@@ -27,9 +26,12 @@
   AfterStruct: true
   SplitEmptyFunction: true
   AfterObjCDeclaration: true
-  AfterStruct: true
   AfterUnion: true
   AfterExternBlock: true
   BeforeCatch: true
   BeforeElse: true
-  AfterCaseLabel: true
\ No newline at end of file
+  AfterCaseLabel: true
+---
+Language: Cpp
+---
+Language: ObjC
diff --git a/.gitignore b/.gitignore
index 97ddc8a..b1c96d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,3 +71,6 @@
 /skia/dependencies/libzip
 /skia/dependencies/libzip_build
 /skia/viewer/imgui.ini
+/diligent/viewer/build/bin/debug/rive_diligent_viewer
+/build/obj
+/renderer/viewer/build/bin/debug/rive_low_level_viewer
diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json
index c74f34f..eeb1e29 100644
--- a/.vscode/c_cpp_properties.json
+++ b/.vscode/c_cpp_properties.json
@@ -1,19 +1,20 @@
 {
-    "configurations": [
-        {
-            "name": "Mac",
-            "includePath": [
-                "${workspaceFolder}/**",
-                "${workspaceFolder}/dev/test/include",
-                "${workspaceFolder}/include"
-            ],
-            "defines": [],
-            "macFrameworkPath": [],
-            "compilerPath": "/usr/local/bin/arm-none-eabi-gcc",
-            "cStandard": "gnu11",
-            "cppStandard": "gnu++14",
-            "intelliSenseMode": "clang-x64"
-        }
-    ],
+    "configurations": [{
+        "name": "Mac",
+        "includePath": [
+            "${workspaceFolder}/**",
+            "${workspaceFolder}/dev/test/include",
+            "${workspaceFolder}/include"
+        ],
+        "defines": [
+            "LOW_LEVEL_RENDERING",
+            "CONTOUR_RECURSIVE"
+        ],
+        "macFrameworkPath": [],
+        "compilerPath": "/usr/local/bin/arm-none-eabi-gcc",
+        "cStandard": "gnu11",
+        "cppStandard": "gnu++14",
+        "intelliSenseMode": "clang-x64"
+    }],
     "version": 4
 }
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8e596c5..526bc95 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -93,7 +93,8 @@
         "valarray": "cpp",
         "variant": "cpp",
         "vector": "cpp",
-        "*.ipp": "cpp"
+        "*.ipp": "cpp",
+        "*.tcc": "cpp"
     },
     "git.ignoreLimitWarning": true
 }
\ No newline at end of file
diff --git a/build/premake5.lua b/build/premake5.lua
index 6e1c83c..116435c 100644
--- a/build/premake5.lua
+++ b/build/premake5.lua
@@ -1,29 +1,38 @@
 workspace "rive"
-    configurations {"debug", "release"}
+configurations {"debug", "release"}
 
 project "rive"
-    kind "StaticLib"
-    language "C++"
-    cppdialect "C++17"
-    targetdir "bin/%{cfg.buildcfg}"
-    objdir "obj/%{cfg.buildcfg}"
-    includedirs {"../include"}
+kind "StaticLib"
+language "C++"
+cppdialect "C++17"
+targetdir "bin/%{cfg.buildcfg}"
+objdir "obj/%{cfg.buildcfg}"
+includedirs {"../include"}
 
-    files {"../src/**.cpp"}
+files {"../src/**.cpp"}
 
-    buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-Werror=format"}
+buildoptions {"-Wall", "-fno-exceptions", "-fno-rtti", "-Werror=format"}
 
-    filter "system:windows"
-        defines {"_USE_MATH_DEFINES"}
+newoption {
+    trigger = "with-low-level-rendering",
+    description = "Builds in utility classes and methods used for low level renderering implementations."
+}
 
-    filter "configurations:debug"
-        defines {"DEBUG"}
-        symbols "On"
+filter "system:windows"
+defines {"_USE_MATH_DEFINES"}
 
-    filter "configurations:release"
-        defines {"RELEASE"}
-        defines {"NDEBUG"}
-        optimize "On"
+filter "configurations:debug"
+defines {"DEBUG"}
+symbols "On"
+
+filter "configurations:release"
+defines {"RELEASE"}
+defines {"NDEBUG"}
+optimize "On"
+
+configuration "with-low-level-rendering"
+defines("LOW_LEVEL_RENDERING")
+defines("CONTOUR_RECURSIVE")
 
 -- Clean Function --
 newaction {
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua
index 5fcf6b5..d1c3428 100644
--- a/dev/test/premake5.lua
+++ b/dev/test/premake5.lua
@@ -31,7 +31,7 @@
 "../../test/**.cpp" -- the tests
 }
 
-defines {"TESTING", "ENABLE_QUERY_FLAT_VERTICES"}
+defines {"TESTING", "ENABLE_QUERY_FLAT_VERTICES", "LOW_LEVEL_RENDERING", "CONTOUR_RECURSIVE"}
 
 filter "configurations:debug"
 defines {"DEBUG"}
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index a7ec00c..e59518f 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -97,6 +97,10 @@
 		/// longer needed.
 		Artboard* instance() const;
 
+		/// Make an instance of this artboard using a concrete object, allowing
+		/// for inheriting from Artboard to add runtime specific functionality.
+		Artboard* instance(Artboard* instanceObject) const;
+
 		/// Returns true if the artboard is an instance of another
 		bool isInstance() const { return m_IsInstance; }
 	};
diff --git a/include/rive/contour_render_path.hpp b/include/rive/contour_render_path.hpp
new file mode 100644
index 0000000..1947812
--- /dev/null
+++ b/include/rive/contour_render_path.hpp
@@ -0,0 +1,113 @@
+#ifndef _RIVE_CONTOUR_RENDER_PATH_HPP_
+#define _RIVE_CONTOUR_RENDER_PATH_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/math/aabb.hpp"
+#include <vector>
+#include <cstdint>
+
+namespace rive
+{
+	enum class PathCommandType : uint8_t
+	{
+		/// Corresponds to CommandPath::moveTo
+		move,
+		/// Corresponds to CommandPath::lineTo
+		line,
+		/// Corresponds to CommandPath::cubicTo
+		cubic,
+		/// Corresponds to CommandPath::close
+		close
+	};
+
+	class PathCommand
+	{
+	private:
+		PathCommandType m_Type;
+		/// Only used when m_Type is cubic.
+		Vec2D m_OutPoint;
+
+		/// Only used when m_Type is cubic.
+		Vec2D m_InPoint;
+
+		/// Only used when m_Type is move or close or cubic.
+		Vec2D m_Point;
+
+	public:
+		PathCommand(PathCommandType type);
+		PathCommand(PathCommandType type, float x, float y);
+		PathCommand(PathCommandType type,
+		            float outX,
+		            float outY,
+		            float inX,
+		            float inY,
+		            float x,
+		            float y);
+
+		PathCommandType type() const { return m_Type; }
+		const Vec2D& outPoint() const { return m_OutPoint; }
+		const Vec2D& inPoint() const { return m_InPoint; }
+		const Vec2D& point() const { return m_Point; }
+	};
+	///
+	/// A reference to a sub-path added to a ContourRenderPath with its relative
+	/// transform.
+	///
+	class ContourSubPath
+	{
+	private:
+		RenderPath* m_Path;
+		Mat2D m_Transform;
+
+	public:
+		ContourSubPath(RenderPath* path, const Mat2D& transform);
+
+		RenderPath* path() const;
+		const Mat2D& transform() const;
+	};
+
+	class ContourStroke;
+	///
+	/// Segments curves into line segments and computes the bounds of the
+	/// segmented curve.
+	///
+	class ContourRenderPath : public RenderPath
+	{
+	protected:
+		AABB m_ContourBounds;
+		std::vector<Vec2D> m_ContourVertices;
+		std::vector<ContourSubPath> m_SubPaths;
+		std::vector<PathCommand> m_Commands;
+		bool m_IsDirty = true;
+		float m_ContourThreshold = 1.0f;
+		bool m_IsClosed = false;
+
+	public:
+		std::size_t contourLength() const { return m_ContourVertices.size(); }
+		const std::vector<Vec2D>& contourVertices() const
+		{
+			return m_ContourVertices;
+		}
+		bool isClosed() const { return m_IsClosed; }
+
+		bool isContainer() const;
+		void addRenderPath(RenderPath* path, const Mat2D& transform) override;
+
+		void reset() override;
+		void moveTo(float x, float y) override;
+		void lineTo(float x, float y) override;
+		void cubicTo(
+		    float ox, float oy, float ix, float iy, float x, float y) override;
+		void close() override;
+
+		void computeContour();
+		bool isDirty() const { return m_IsDirty; }
+
+		void extrudeStroke(ContourStroke* stroke,
+		                   StrokeJoin join,
+		                   StrokeCap cap,
+		                   float strokeWidth,
+		                   const Mat2D& transform);
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/contour_stroke.hpp b/include/rive/contour_stroke.hpp
new file mode 100644
index 0000000..59c77a7
--- /dev/null
+++ b/include/rive/contour_stroke.hpp
@@ -0,0 +1,42 @@
+#ifndef _RIVE_CONTOUR_STROKE_HPP_
+#define _RIVE_CONTOUR_STROKE_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+#include <vector>
+#include <cstdint>
+
+namespace rive
+{
+	class ContourRenderPath;
+
+	///
+	/// Builds a triangle strip vertex buffer from a ContourRenderPath.
+	///
+	class ContourStroke
+	{
+	protected:
+		std::vector<Vec2D> m_TriangleStrip;
+		std::vector<std::size_t> m_Offsets;
+		uint32_t m_RenderOffset = 0;
+
+	public:
+		const std::vector<Vec2D>& triangleStrip() const
+		{
+			return m_TriangleStrip;
+		}
+
+		void reset();
+		void resetRenderOffset();
+		void nextRenderOffset(std::size_t& start, std::size_t& end);
+
+		void extrude(const ContourRenderPath* renderPath,
+		             bool isClosed,
+		             StrokeJoin join,
+		             StrokeCap cap,
+		             float strokeWidth,
+		             const Mat2D& transform);
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp
index c9123cd..5483db1 100644
--- a/include/rive/math/aabb.hpp
+++ b/include/rive/math/aabb.hpp
@@ -4,6 +4,7 @@
 #include "rive/math/mat2d.hpp"
 #include "rive/math/vec2d.hpp"
 #include <cstddef>
+#include <limits>
 
 namespace rive
 {
@@ -38,10 +39,29 @@
 		static bool testOverlap(const AABB& a, const AABB& b);
 		static bool areIdentical(const AABB& a, const AABB& b);
 		static void transform(AABB& out, const AABB& a, const Mat2D& matrix);
+		static void copy(AABB& out, const AABB& a);
+
+		///
+		/// Grow the AABB to fit the point.
+		///
+		static void expandTo(AABB& out, const Vec2D& point);
+		static void expandTo(AABB& out, float x, float y);
 
 		float width() const;
 		float height() const;
 		float perimeter() const;
+
+		///
+		/// Initialize an AABB to values that represent an invalid/collapsed
+		/// AABB that can then expand to points that are added to it.
+		///
+		inline static AABB forExpansion()
+		{
+			return AABB(std::numeric_limits<float>::max(),
+			            std::numeric_limits<float>::max(),
+			            -std::numeric_limits<float>::max(),
+			            -std::numeric_limits<float>::max());
+		}
 	};
 } // namespace rive
 #endif
diff --git a/include/rive/math/cubic_utilities.hpp b/include/rive/math/cubic_utilities.hpp
new file mode 100644
index 0000000..684cf46
--- /dev/null
+++ b/include/rive/math/cubic_utilities.hpp
@@ -0,0 +1,60 @@
+#ifndef _RIVE_CUBIC_UTILITIES_HPP_
+#define _RIVE_CUBIC_UTILITIES_HPP_
+
+#include "rive/math/vec2d.hpp"
+#include <algorithm>
+
+namespace rive
+{
+	///
+	/// Utility functions for recursively subdividing a cubic.
+	///
+	class CubicUtilities
+	{
+	public:
+		static void computeHull(const Vec2D& from,
+		                        const Vec2D& fromOut,
+		                        const Vec2D& toIn,
+		                        const Vec2D& to,
+		                        float t,
+		                        Vec2D* hull)
+		{
+			Vec2D::lerp(hull[0], from, fromOut, t);
+			Vec2D::lerp(hull[1], fromOut, toIn, t);
+			Vec2D::lerp(hull[2], toIn, to, t);
+
+			Vec2D::lerp(hull[3], hull[0], hull[1], t);
+			Vec2D::lerp(hull[4], hull[1], hull[2], t);
+
+			Vec2D::lerp(hull[5], hull[3], hull[4], t);
+		}
+
+		static bool tooFar(const Vec2D& a, const Vec2D& b, float threshold)
+		{
+			return std::max(std::abs(a[0] - b[0]), std::abs(a[1] - b[1])) >
+			       threshold;
+		}
+
+		static bool shouldSplitCubic(const Vec2D& from,
+		                             const Vec2D& fromOut,
+		                             const Vec2D& toIn,
+		                             const Vec2D& to,
+		                             float threshold)
+		{
+			Vec2D oneThird, twoThird;
+			Vec2D::lerp(oneThird, from, to, 1.0f / 3.0f);
+			Vec2D::lerp(twoThird, from, to, 2.0f / 3.0f);
+			return tooFar(fromOut, oneThird, threshold) ||
+			       tooFar(toIn, twoThird, threshold);
+		}
+
+		static float cubicAt(float t, float a, float b, float c, float d)
+		{
+			float ti = 1.0f - t;
+			float value = ti * ti * ti * a + 3.0f * ti * ti * t * b +
+			              3.0f * ti * t * t * c + t * t * t * d;
+			return value;
+		}
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/math/mat2d.hpp b/include/rive/math/mat2d.hpp
index 025b961..9518975 100644
--- a/include/rive/math/mat2d.hpp
+++ b/include/rive/math/mat2d.hpp
@@ -33,6 +33,8 @@
 			result[5] = 0.0f;
 		}
 
+		static const Mat2D& identity();
+
 		static void fromRotation(Mat2D& result, float rad);
 		static void scale(Mat2D& result, const Mat2D& mat, const Vec2D& vec);
 		static void multiply(Mat2D& result, const Mat2D& a, const Mat2D& b);
@@ -49,6 +51,13 @@
 		float yy() const { return m_Buffer[3]; }
 		float tx() const { return m_Buffer[4]; }
 		float ty() const { return m_Buffer[5]; }
+
+		void print() const
+		{
+			printf("X: %f %f\n", m_Buffer[0], m_Buffer[1]);
+			printf("Y: %f %f\n", m_Buffer[2], m_Buffer[3]);
+			printf("T: %f %f\n", m_Buffer[4], m_Buffer[5]);
+		}
 	};
 
 	inline Mat2D operator*(const Mat2D& a, const Mat2D& b)
@@ -63,5 +72,6 @@
 		return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3] &&
 		       a[4] == b[4] && a[5] == b[5];
 	}
+
 } // namespace rive
 #endif
\ No newline at end of file
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
index 1cbcb7a..76d1b07 100644
--- a/include/rive/renderer.hpp
+++ b/include/rive/renderer.hpp
@@ -35,6 +35,7 @@
 		virtual void radialGradient(float sx, float sy, float ex, float ey) = 0;
 		virtual void addStop(unsigned int color, float stop) = 0;
 		virtual void completeGradient() = 0;
+		virtual void invalidateStroke() = 0;
 		virtual ~RenderPaint() {}
 	};
 
diff --git a/include/rive/shapes/paint/stroke.hpp b/include/rive/shapes/paint/stroke.hpp
index 3cceb2f..38cb498 100644
--- a/include/rive/shapes/paint/stroke.hpp
+++ b/include/rive/shapes/paint/stroke.hpp
@@ -16,7 +16,8 @@
 		void draw(Renderer* renderer, CommandPath* path) override;
 		void addStrokeEffect(StrokeEffect* effect);
 		bool hasStrokeEffect() { return m_Effect != nullptr; }
-		void invalidateEffects();
+		void invalidate();
+		void invalidateRendering();
 		bool isVisible() const override;
 
 	protected:
diff --git a/include/rive/shapes/shape_paint_container.hpp b/include/rive/shapes/shape_paint_container.hpp
index f406d2f..d00b832 100644
--- a/include/rive/shapes/shape_paint_container.hpp
+++ b/include/rive/shapes/shape_paint_container.hpp
@@ -26,7 +26,7 @@
 
 		PathSpace pathSpace() const;
 
-		void invalidateStrokeEffects();
+		void invalidateStroke();
 
 		CommandPath* makeCommandPath(PathSpace space);
 	};
diff --git a/renderer/README.md b/renderer/README.md
new file mode 100644
index 0000000..6b1c217
--- /dev/null
+++ b/renderer/README.md
@@ -0,0 +1,24 @@
+:warning: | This is experimental tech in very early R&D.
+| -- | -- |
+
+# Low Level Renderering
+Provides concrete [rive-cpp renderers](https://github.com/rive-app/rive-cpp/blob/master/include/renderer.hpp) for displaying Rive content on any platform.
+
+# Renderer Support
+- [] OpenGL
+- [] Metal
+- [] Vulkan
+- [] D3D11
+- [] D3D12
+
+## OpenGL (ES 2.0 compliant)
+Primarily for older mobile devices where OpenGL ES 2.0 is supported and more modern renderers are not. This is not strictly limited to ES 2.0 devices, however. The API surface used is only the common set between OpenGL and OpenGL ES 2.0 in order to guarantee at minimum ES 2.0 support.
+
+## Metal
+For OSX and iOS. Planned to be implemented via Objective-C for easy interop with C++.
+
+## Vulkan
+Intended for primary use on modern Android devices and Linux.
+
+## D3D11 and D3D12
+For Windows and Windows based systems (Xbox).
\ No newline at end of file
diff --git a/renderer/dependencies/.gitignore b/renderer/dependencies/.gitignore
new file mode 100644
index 0000000..c2c027f
--- /dev/null
+++ b/renderer/dependencies/.gitignore
@@ -0,0 +1 @@
+local
\ No newline at end of file
diff --git a/renderer/dependencies/make_dependencies.sh b/renderer/dependencies/make_dependencies.sh
new file mode 100755
index 0000000..7444ba5
--- /dev/null
+++ b/renderer/dependencies/make_dependencies.sh
@@ -0,0 +1,3 @@
+cd scripts
+./make_glfw.sh
+./make_gl3w.sh
\ No newline at end of file
diff --git a/renderer/dependencies/scripts/make_gl3w.sh b/renderer/dependencies/scripts/make_gl3w.sh
new file mode 100755
index 0000000..169ebbf
--- /dev/null
+++ b/renderer/dependencies/scripts/make_gl3w.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+set -e
+
+if ! command -v cmake &> /dev/null
+then
+    echo "cmake is required"
+    exit
+fi
+
+mkdir -p ../local/sources
+cd ../local/sources
+
+GL3W_REPO=https://github.com/skaslev/gl3w
+GL3W_STABLE_BRANCH=master
+
+if [ ! -d gl3w ]; then
+	echo "Cloning gl3w."
+    git clone $GL3W_REPO
+fi
+
+cd gl3w && git checkout $GL3W_STABLE_BRANCH && git fetch && git pull
+
+mkdir -p build
+mkdir -p ../../../../local
+cd build
+cmake ../ -DCMAKE_INSTALL_PREFIX=../../../../local
+make
+make install
\ No newline at end of file
diff --git a/renderer/dependencies/scripts/make_glfw.sh b/renderer/dependencies/scripts/make_glfw.sh
new file mode 100755
index 0000000..236fd0b
--- /dev/null
+++ b/renderer/dependencies/scripts/make_glfw.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+set -e
+
+if ! command -v cmake &> /dev/null
+then
+    echo "cmake is required"
+    exit
+fi
+
+mkdir -p ../local/sources
+cd ../local/sources
+
+GLFW_REPO=https://github.com/glfw/glfw
+GLFW_STABLE_BRANCH=master
+
+if [ ! -d glfw ]; then
+	echo "Cloning GLFW."
+    git clone $GLFW_REPO
+fi
+
+cd glfw && git checkout $GLFW_STABLE_BRANCH && git fetch && git pull
+
+mkdir -p build
+mkdir -p ../../../../local
+cd build
+cmake ../ -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=../../../../local
+make
+make install
\ No newline at end of file
diff --git a/renderer/library/build.sh b/renderer/library/build.sh
new file mode 100755
index 0000000..cc912b1
--- /dev/null
+++ b/renderer/library/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+cd ../..
+./build.sh $@
+
+cd renderer/library
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]
+then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library 
+elif [ "$OPTION" = "clean" ]
+then
+    echo Cleaning project ...
+    premake5 clean
+elif [ "$OPTION" = "release" ]
+then
+    premake5 gmake && make config=release -j7
+else
+    premake5 gmake && make -j7
+fi
\ No newline at end of file
diff --git a/renderer/library/build/premake5.lua b/renderer/library/build/premake5.lua
new file mode 100644
index 0000000..a28d333
--- /dev/null
+++ b/renderer/library/build/premake5.lua
@@ -0,0 +1,49 @@
+workspace "rive"
+configurations {"debug", "release"}
+
+project "rive_renderer"
+kind "StaticLib"
+language "C++"
+cppdialect "C++17"
+targetdir "bin/%{cfg.buildcfg}"
+objdir "obj/%{cfg.buildcfg}"
+includedirs {"../include", "../../../include"}
+
+if os.host() == "macosx" then
+    links {"Cocoa.framework", "rive"}
+    defines {"RIVE_HAS_METAL", "RIVE_HAS_OPENGL"}
+    defines {"GL_SILENCE_DEPRECATION"}
+    includedirs {"../../dependencies/DiligentEngine_build/build/include"}
+    files {"../src/**.mm"}
+else
+    links {"rive"}
+end
+
+libdirs {"../../../build/bin/%{cfg.buildcfg}", "../../dependencies/skia/out/Static"}
+
+files {"../src/**.cpp"}
+
+buildoptions {"-Wall", "-fno-rtti"}
+
+filter "configurations:debug"
+defines {"DEBUG"}
+symbols "On"
+
+filter "configurations:release"
+defines {"RELEASE"}
+optimize "On"
+
+-- Clean Function --
+newaction {
+    trigger = "clean",
+    description = "clean the build",
+    execute = function()
+        print("clean the build...")
+        os.rmdir("./bin")
+        os.rmdir("./obj")
+        os.remove("Makefile")
+        -- no wildcards in os.remove, so use shell
+        os.execute("rm *.make")
+        print("build cleaned")
+    end
+}
diff --git a/renderer/library/include/graphics_api.hpp b/renderer/library/include/graphics_api.hpp
new file mode 100644
index 0000000..cf92c40
--- /dev/null
+++ b/renderer/library/include/graphics_api.hpp
@@ -0,0 +1,53 @@
+#ifndef _RIVE_GRAPHICS_API_HPP_
+#define _RIVE_GRAPHICS_API_HPP_
+
+#include "rive/renderer.hpp"
+
+namespace rive
+{
+	class LowLevelRenderer;
+	class GraphicsApi
+	{
+	public:
+		///
+		/// The Rive rendering apis that are (or will be) available.
+		///
+		enum Type : unsigned int
+		{
+			unknown = 0,
+			opengl = 1,
+			metal = 2,
+			vulkan = 3,
+			d3d11 = 4,
+			d3d12 = 5
+		};
+		///
+		/// Get the currently active GraphicsApi.
+		///
+		LowLevelRenderer* activeRenderer();
+
+		static LowLevelRenderer* makeRenderer(GraphicsApi::Type api);
+		static LowLevelRenderer* makeRenderer();
+
+		static const char* name(GraphicsApi::Type type)
+		{
+			switch (type)
+			{
+				case GraphicsApi::unknown:
+					return "Unknown";
+				case GraphicsApi::opengl:
+					return "OpenGL";
+				case GraphicsApi::metal:
+					return "Metal";
+				case GraphicsApi::vulkan:
+					return "Vulkan";
+				case GraphicsApi::d3d11:
+					return "Direct3D 11";
+				case GraphicsApi::d3d12:
+					return "Direct3D 12";
+			}
+		}
+	};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/low_level/low_level_renderer.hpp b/renderer/library/include/low_level/low_level_renderer.hpp
new file mode 100644
index 0000000..17682e9
--- /dev/null
+++ b/renderer/library/include/low_level/low_level_renderer.hpp
@@ -0,0 +1,83 @@
+#ifndef _RIVE_LOW_LEVEL_RENDERER_HPP_
+#define _RIVE_LOW_LEVEL_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/math/mat2d.hpp"
+#include "graphics_api.hpp"
+#include <stdint.h>
+#include <list>
+#include <vector>
+
+namespace rive
+{
+	class SubPath
+	{
+	private:
+		RenderPath* m_Path;
+		Mat2D m_Transform;
+
+	public:
+		SubPath(RenderPath* path, const Mat2D& transform);
+
+		RenderPath* path();
+		const Mat2D& transform();
+	};
+
+	struct RenderState
+	{
+		Mat2D transform;
+		std::vector<SubPath> clipPaths;
+	};
+
+	///
+	/// Low level implementation of a generalized rive::Renderer. It's
+	/// specifically tailored for use with low level graphics apis like Metal,
+	/// OpenGL, Vulkan, D3D, etc.
+	///
+	class LowLevelRenderer : public Renderer
+	{
+	protected:
+		float m_ModelViewProjection[16] = {0.0f};
+		std::list<RenderState> m_Stack;
+		bool m_IsClippingDirty = false;
+		std::vector<SubPath> m_ClipPaths;
+
+	public:
+		LowLevelRenderer();
+
+		///
+		/// Checks if clipping is dirty and clears the clipping flag. Hard
+		/// expectation for whoever checks this to also apply it. That's why
+		/// it's not marked const.
+		///
+		bool isClippingDirty();
+
+		virtual GraphicsApi::Type type() const = 0;
+
+		virtual void startFrame();
+		virtual void endFrame() = 0;
+
+		virtual RenderPaint* makeRenderPaint() = 0;
+		virtual RenderPath* makeRenderPath() = 0;
+		virtual bool initialize(void* data) = 0;
+		bool initialize() { return initialize(nullptr); }
+
+		void modelViewProjection(float value[16]);
+
+		virtual void orthographicProjection(float dst[16],
+		                                    float left,
+		                                    float right,
+		                                    float bottom,
+		                                    float top,
+		                                    float near,
+		                                    float far);
+
+		void save() override;
+		void restore() override;
+		void transform(const Mat2D& transform) override;
+		const Mat2D& transform();
+		void clipPath(RenderPath* path) override;
+	};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/metal/metal_render_paint.hpp b/renderer/library/include/metal/metal_render_paint.hpp
new file mode 100644
index 0000000..fde31ab
--- /dev/null
+++ b/renderer/library/include/metal/metal_render_paint.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_METAL_RENDER_PAINT_HPP_
+#define _RIVE_METAL_RENDER_PAINT_HPP_
+
+#include "rive/renderer.hpp"
+
+namespace rive
+{
+	class MetalRenderPaint : public RenderPaint
+	{
+	private:
+		RenderPaintStyle m_PaintStyle;
+
+	public:
+		void style(RenderPaintStyle style) override;
+		RenderPaintStyle style() const { return m_PaintStyle; }
+		void color(unsigned int value) override;
+		void thickness(float value) override;
+		void join(StrokeJoin value) override;
+		void cap(StrokeCap value) override;
+		void blendMode(BlendMode value) override;
+
+		void linearGradient(float sx, float sy, float ex, float ey) override;
+		void radialGradient(float sx, float sy, float ex, float ey) override;
+		void addStop(unsigned int color, float stop) override;
+		void completeGradient() override;
+		void invalidateStroke() override;
+		~MetalRenderPaint();
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/metal/metal_render_path.hpp b/renderer/library/include/metal/metal_render_path.hpp
new file mode 100644
index 0000000..08bb2f0
--- /dev/null
+++ b/renderer/library/include/metal/metal_render_path.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_METAL_RENDER_PATH_HPP_
+#define _RIVE_METAL_RENDER_PATH_HPP_
+
+#include "rive/contour_render_path.hpp"
+
+namespace rive
+{
+	class MetalRenderPath : public ContourRenderPath
+	{
+	private:
+		FillRule m_FillRule;
+
+	public:
+		void fillRule(FillRule value) override;
+		FillRule fillRule() const { return m_FillRule; }
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/metal/metal_renderer.hpp b/renderer/library/include/metal/metal_renderer.hpp
new file mode 100644
index 0000000..4db6dc4
--- /dev/null
+++ b/renderer/library/include/metal/metal_renderer.hpp
@@ -0,0 +1,48 @@
+#ifndef _RIVE_METAL_RENDERER_HPP_
+#define _RIVE_METAL_RENDERER_HPP_
+
+#include "low_level/low_level_renderer.hpp"
+
+#ifndef __OBJC__
+#error MetalRenderer can only be included from Objective-C files.
+#endif
+#ifndef __clang__
+#error MetalRenderer can only be compiled with Clang.
+#endif
+
+#import <Metal/Metal.h>
+
+namespace rive
+{
+	class MetalRenderer : public LowLevelRenderer
+	{
+	public:
+		GraphicsApi::Type type() const override { return GraphicsApi::metal; }
+		~MetalRenderer();
+		void save() override;
+		void restore() override;
+		void transform(const Mat2D& transform) override;
+		void drawPath(RenderPath* path, RenderPaint* paint) override;
+		void clipPath(RenderPath* path) override;
+
+		void startFrame() override;
+		void endFrame() override;
+
+		RenderPaint* makeRenderPaint() override;
+		RenderPath* makeRenderPath() override;
+		bool initialize(void* data) override;
+
+		virtual id<MTLDevice> acquireDevice() = 0;
+		virtual id<MTLRenderCommandEncoder> currentCommandEncoder() = 0;
+
+	private:
+		id<MTLBuffer> m_FillScreenVertexBuffer;
+		id<MTLRenderPipelineState> m_Pipeline;
+		id<MTLDepthStencilState> m_DepthStencil;
+
+	protected:
+		void fillScreen();
+	};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/opengl/opengl.h b/renderer/library/include/opengl/opengl.h
new file mode 100644
index 0000000..1f1cd48
--- /dev/null
+++ b/renderer/library/include/opengl/opengl.h
@@ -0,0 +1,22 @@
+#if defined(_WIN32) || defined(_WIN64)
+#include <gl/glew.h>
+#include <GL/gl.h>
+#include <GL/glu.h>
+#elif __APPLE__
+#include "TargetConditionals.h"
+#if (TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR) || TARGET_OS_IPHONE
+#include <OpenGLES/ES2/gl.h>
+#include <OpenGLES/ES2/glext.h>
+#else
+#include <OpenGL/gl3.h>
+#endif
+#elif defined(__ANDROID__) || defined(ANDROID)
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#elif defined(__linux__) || defined(__unix__) || defined(__posix__)
+#include <GL/gl.h>
+#include <GL/glu.h>
+#include <GL/glext.h>
+#else
+#error platform not supported.
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/opengl/opengl_render_paint.hpp b/renderer/library/include/opengl/opengl_render_paint.hpp
new file mode 100644
index 0000000..72ca144
--- /dev/null
+++ b/renderer/library/include/opengl/opengl_render_paint.hpp
@@ -0,0 +1,68 @@
+#ifndef _RIVE_OPENGL_RENDER_PAINT_HPP_
+#define _RIVE_OPENGL_RENDER_PAINT_HPP_
+
+#include "rive/renderer.hpp"
+#include "opengl/opengl.h"
+#include <vector>
+
+namespace rive
+{
+	class OpenGLRenderer;
+	class OpenGLRenderPaint;
+	class OpenGLRenderPath;
+	class ContourStroke;
+
+	class OpenGLGradient
+	{
+		friend class OpenGLRenderPaint;
+
+	private:
+		float m_Position[4];
+		int m_Type = 0;
+		std::vector<float> m_Colors;
+		std::vector<float> m_Stops;
+		bool m_IsVisible = false;
+
+	public:
+		OpenGLGradient(int type);
+		void position(float sx, float sy, float ex, float ey);
+		void addStop(unsigned int color, float stop);
+		void bind(OpenGLRenderer* renderer);
+	};
+
+	class OpenGLRenderPaint : public RenderPaint
+	{
+	private:
+		RenderPaintStyle m_PaintStyle;
+		float m_Color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+		OpenGLGradient* m_Gradient = nullptr;
+		ContourStroke* m_Stroke = nullptr;
+		StrokeJoin m_StrokeJoin = StrokeJoin::miter;
+		StrokeCap m_StrokeCap = StrokeCap::butt;
+		float m_StrokeThickness = 0.0f;
+		GLuint m_StrokeBuffer = 0;
+		bool m_StrokeDirty = false;
+
+	public:
+		void style(RenderPaintStyle style) override;
+		RenderPaintStyle style() const { return m_PaintStyle; }
+		void color(unsigned int value) override;
+		void thickness(float value) override;
+		void join(StrokeJoin value) override;
+		void cap(StrokeCap value) override;
+		void blendMode(BlendMode value) override;
+
+		void linearGradient(float sx, float sy, float ex, float ey) override;
+		void radialGradient(float sx, float sy, float ex, float ey) override;
+		void addStop(unsigned int color, float stop) override;
+		void completeGradient() override;
+		void invalidateStroke() override;
+		~OpenGLRenderPaint();
+
+		bool doesDraw() const;
+		void draw(OpenGLRenderer* renderer,
+		          const Mat2D& transform,
+		          OpenGLRenderPath* path);
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/opengl/opengl_render_path.hpp b/renderer/library/include/opengl/opengl_render_path.hpp
new file mode 100644
index 0000000..e56e5b4
--- /dev/null
+++ b/renderer/library/include/opengl/opengl_render_path.hpp
@@ -0,0 +1,33 @@
+#ifndef _RIVE_OPENGL_RENDER_PATH_HPP_
+#define _RIVE_OPENGL_RENDER_PATH_HPP_
+
+#include "opengl.h"
+#include "rive/contour_render_path.hpp"
+#include "rive/math/mat2d.hpp"
+
+namespace rive
+{
+	class OpenGLRenderer;
+	class OpenGLRenderPath : public ContourRenderPath
+	{
+	private:
+		FillRule m_FillRule;
+		GLuint m_ContourBuffer = 0;
+
+	public:
+		OpenGLRenderPath();
+		~OpenGLRenderPath();
+		void fillRule(FillRule value) override;
+		FillRule fillRule() const { return m_FillRule; }
+
+		void stencil(OpenGLRenderer* renderer, const Mat2D& transform);
+		void cover(OpenGLRenderer* renderer,
+		           const Mat2D& transform,
+		           const Mat2D& localTransform = Mat2D::identity());
+		void renderStroke(ContourStroke* stroke,
+		                  OpenGLRenderer* renderer,
+		                  const Mat2D& transform,
+		                  const Mat2D& localTransform = Mat2D::identity());
+	};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/include/opengl/opengl_renderer.hpp b/renderer/library/include/opengl/opengl_renderer.hpp
new file mode 100644
index 0000000..2b779e6
--- /dev/null
+++ b/renderer/library/include/opengl/opengl_renderer.hpp
@@ -0,0 +1,76 @@
+#ifndef _RIVE_OPENGL_RENDERER_HPP_
+#define _RIVE_OPENGL_RENDERER_HPP_
+
+#include "rive/math/mat2d.hpp"
+#include "low_level/low_level_renderer.hpp"
+#include "opengl.h"
+#include <vector>
+
+namespace rive
+{
+	class OpenGLRenderer : public LowLevelRenderer
+	{
+	private:
+		Mat2D m_Projection;
+		GLuint m_VertexShader = 0, m_FragmentShader = 0;
+		GLuint m_Program = 0;
+		GLuint m_IndexBuffer = 0;
+		GLint m_ProjectionUniformIndex = -1;
+		GLint m_TransformUniformIndex = -1;
+		GLint m_FillTypeUniformIndex = -1;
+		GLint m_StopCountUniformIndex = -1;
+		GLint m_StopColorsUniformIndex = -1;
+		GLint m_ColorUniformIndex = -1;
+		GLint m_StopsUniformIndex = -1;
+		GLint m_GradientPositionUniformIndex = -1;
+		GLint m_ShapeTransformUniformIndex = -1;
+		GLuint m_VertexArray = 0;
+		GLuint m_BlitBuffer = 0;
+		bool m_IsClipping = false;
+
+		/// Indices for the max sized contour, prepended with 2 triangles for
+		/// bounding boxes.
+		std::vector<unsigned short> m_Indices;
+
+	public:
+		const GLuint indexBuffer() const { return m_IndexBuffer; }
+		GraphicsApi::Type type() const override { return GraphicsApi::opengl; }
+		OpenGLRenderer();
+		~OpenGLRenderer();
+		void drawPath(RenderPath* path, RenderPaint* paint) override;
+
+		void startFrame() override;
+		void endFrame() override;
+
+		RenderPaint* makeRenderPaint() override;
+		RenderPath* makeRenderPath() override;
+
+		bool initialize(void* data) override;
+
+		void updateIndexBuffer(std::size_t contourLength);
+
+		GLint transformUniformIndex() const { return m_TransformUniformIndex; }
+
+		GLint fillTypeUniformIndex() const { return m_FillTypeUniformIndex; }
+		GLint stopCountUniformIndex() const { return m_StopCountUniformIndex; }
+		GLint stopColorsUniformIndex() const
+		{
+			return m_StopColorsUniformIndex;
+		}
+		GLint colorUniformIndex() const { return m_ColorUniformIndex; }
+		GLint stopsUniformIndex() const { return m_StopsUniformIndex; }
+		GLint shapeTransformUniformIndex() const
+		{
+			return m_ShapeTransformUniformIndex;
+		}
+		GLint gradientPositionUniformIndex() const
+		{
+			return m_GradientPositionUniformIndex;
+		}
+
+		GLuint program() const { return m_Program; }
+		virtual const char* shaderHeader() const { return nullptr; };
+	};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/library/src/graphics_api.cpp b/renderer/library/src/graphics_api.cpp
new file mode 100644
index 0000000..579af1f
--- /dev/null
+++ b/renderer/library/src/graphics_api.cpp
@@ -0,0 +1,79 @@
+#include "graphics_api.hpp"
+#include "low_level/low_level_renderer.hpp"
+
+#include <stdio.h>
+#include <cassert>
+
+using namespace rive;
+
+static LowLevelRenderer* g_GraphicsApi = nullptr;
+LowLevelRenderer* GraphicsApi::activeRenderer() { return g_GraphicsApi; }
+
+LowLevelRenderer* GraphicsApi::makeRenderer(GraphicsApi::Type api)
+{
+	switch (api)
+	{
+		case GraphicsApi::unknown:
+			fprintf(stderr, "cannot instance unknown api\n");
+			break;
+		case GraphicsApi::opengl:
+#ifdef RIVE_HAS_OPENGL
+			printf("Rive: Renderering with OpenGL\n");
+			extern LowLevelRenderer* makeRendererOpenGL();
+			return (g_GraphicsApi = makeRendererOpenGL());
+#else
+			fprintf(stderr, "opengl is not supported\n");
+#endif
+			break;
+		case GraphicsApi::metal:
+
+#ifdef RIVE_HAS_METAL
+			printf("Rive: Renderering with Metal\n");
+			extern LowLevelRenderer* makeRendererMetal();
+			return (g_GraphicsApi = makeRendererMetal());
+#else
+			fprintf(stderr, "metal is not supported\n");
+#endif
+			break;
+		case GraphicsApi::vulkan:
+			fprintf(stderr, "vulkan is not supported\n");
+			break;
+		case GraphicsApi::d3d11:
+			fprintf(stderr, "d3d11 is not supported\n");
+			break;
+		case GraphicsApi::d3d12:
+			fprintf(stderr, "d3d12 is not supported\n");
+			break;
+		default:
+			fprintf(stderr, "unhandled graphics api type\n");
+			break;
+	}
+
+	return nullptr;
+}
+
+LowLevelRenderer* GraphicsApi::makeRenderer()
+{
+#ifdef RIVE_HAS_METAL
+	return makeRenderer(GraphicsApi::metal);
+#elif defined RIVE_HAS_OPENGL
+	return makeRenderer(GraphicsApi::opengl);
+#else
+	return nullptr;
+#endif
+}
+
+namespace rive
+{
+	RenderPaint* makeRenderPaint()
+	{
+		assert(g_GraphicsApi != nullptr);
+		return g_GraphicsApi->makeRenderPaint();
+	}
+
+	RenderPath* makeRenderPath()
+	{
+		assert(g_GraphicsApi != nullptr);
+		return g_GraphicsApi->makeRenderPath();
+	}
+} // namespace rive
\ No newline at end of file
diff --git a/renderer/library/src/low_level_renderer/low_level_renderer.cpp b/renderer/library/src/low_level_renderer/low_level_renderer.cpp
new file mode 100644
index 0000000..d64bf05
--- /dev/null
+++ b/renderer/library/src/low_level_renderer/low_level_renderer.cpp
@@ -0,0 +1,116 @@
+#include "low_level/low_level_renderer.hpp"
+#include <cstring>
+#include <cassert>
+
+using namespace rive;
+
+SubPath::SubPath(RenderPath* path, const Mat2D& transform) :
+    m_Path(path), m_Transform(transform)
+{
+}
+
+RenderPath* SubPath::path() { return m_Path; }
+const Mat2D& SubPath::transform() { return m_Transform; }
+
+LowLevelRenderer::LowLevelRenderer() { m_Stack.emplace_back(RenderState()); }
+
+void LowLevelRenderer::modelViewProjection(float value[16])
+{
+	std::memcpy(m_ModelViewProjection, value, sizeof(m_ModelViewProjection));
+}
+
+void LowLevelRenderer::orthographicProjection(float dst[16],
+                                              float left,
+                                              float right,
+                                              float bottom,
+                                              float top,
+                                              float near,
+                                              float far)
+{
+	dst[0] = 2.0f / (right - left);
+	dst[1] = 0.0f;
+	dst[2] = 0.0f;
+	dst[3] = 0.0f;
+
+	dst[4] = 0.0f;
+	dst[5] = 2.0f / (top - bottom);
+	dst[6] = 0.0f;
+	dst[7] = 0.0f;
+
+	dst[8] = 0.0f;
+	dst[9] = 0.0f;
+	dst[10] = 2.0f / (near - far);
+	dst[11] = 0.0f;
+
+	dst[12] = (right + left) / (left - right);
+	dst[13] = (top + bottom) / (bottom - top);
+	dst[14] = (far + near) / (near - far);
+	dst[15] = 1.0f;
+}
+
+void LowLevelRenderer::save() { m_Stack.push_back(m_Stack.back()); }
+
+void LowLevelRenderer::restore()
+{
+	assert(m_Stack.size() > 1);
+	RenderState& state = m_Stack.back();
+	m_Stack.pop_back();
+
+	// We can only add clipping paths so if they're still the same, nothing has
+	// changed.
+	m_IsClippingDirty =
+	    state.clipPaths.size() != m_Stack.back().clipPaths.size();
+}
+
+void LowLevelRenderer::transform(const Mat2D& transform)
+{
+	Mat2D& stackMat = m_Stack.back().transform;
+	Mat2D::multiply(stackMat, stackMat, transform);
+}
+const Mat2D& LowLevelRenderer::transform() { return m_Stack.back().transform; }
+
+void LowLevelRenderer::clipPath(RenderPath* path)
+{
+	RenderState& state = m_Stack.back();
+	state.clipPaths.emplace_back(SubPath(path, state.transform));
+	m_IsClippingDirty = true;
+}
+
+void LowLevelRenderer::startFrame()
+{
+	assert(m_Stack.size() == 1);
+	m_ClipPaths.clear();
+	m_IsClippingDirty = false;
+}
+
+bool LowLevelRenderer::isClippingDirty()
+{
+	if (!m_IsClippingDirty)
+	{
+		return false;
+	}
+
+	m_IsClippingDirty = false;
+	RenderState& state = m_Stack.back();
+	auto currentClipLength = m_ClipPaths.size();
+	if (currentClipLength == state.clipPaths.size())
+	{
+		// Same length so now check if they're all the same.
+		bool allSame = true;
+		for (std::size_t i = 0; i < currentClipLength; i++)
+		{
+			if (state.clipPaths[i].path() != m_ClipPaths[i].path())
+			{
+				allSame = false;
+				break;
+			}
+		}
+		if (allSame)
+		{
+			return false;
+		}
+	}
+	m_ClipPaths = state.clipPaths;
+
+	return true;
+}
\ No newline at end of file
diff --git a/renderer/library/src/metal/metal_render_paint.mm b/renderer/library/src/metal/metal_render_paint.mm
new file mode 100644
index 0000000..cc204f9
--- /dev/null
+++ b/renderer/library/src/metal/metal_render_paint.mm
@@ -0,0 +1,26 @@
+#include "metal/metal_render_paint.hpp"
+using namespace rive;
+
+void MetalRenderPaint::style(RenderPaintStyle style) { m_PaintStyle = style; }
+
+void MetalRenderPaint::color(unsigned int value) {}
+
+void MetalRenderPaint::thickness(float value) {}
+
+void MetalRenderPaint::join(StrokeJoin value) {}
+
+void MetalRenderPaint::cap(StrokeCap value) {}
+
+void MetalRenderPaint::blendMode(BlendMode value) {}
+
+void MetalRenderPaint::linearGradient(float sx, float sy, float ex, float ey) {}
+
+void MetalRenderPaint::radialGradient(float sx, float sy, float ex, float ey) {}
+
+void MetalRenderPaint::addStop(unsigned int color, float stop) {}
+
+void MetalRenderPaint::completeGradient() {}
+
+void MetalRenderPaint::invalidateStroke() {}
+
+MetalRenderPaint::~MetalRenderPaint() {}
\ No newline at end of file
diff --git a/renderer/library/src/metal/metal_render_path.mm b/renderer/library/src/metal/metal_render_path.mm
new file mode 100644
index 0000000..8ed4953
--- /dev/null
+++ b/renderer/library/src/metal/metal_render_path.mm
@@ -0,0 +1,5 @@
+#include "metal/metal_render_path.hpp"
+
+using namespace rive;
+
+void MetalRenderPath::fillRule(FillRule value) { m_FillRule = value; }
\ No newline at end of file
diff --git a/renderer/library/src/metal/metal_renderer.mm b/renderer/library/src/metal/metal_renderer.mm
new file mode 100644
index 0000000..18651f3
--- /dev/null
+++ b/renderer/library/src/metal/metal_renderer.mm
@@ -0,0 +1,134 @@
+#include "metal/metal_renderer.hpp"
+#include "metal/metal_render_path.hpp"
+#include "metal/metal_render_paint.hpp"
+
+using namespace rive;
+MetalRenderer::~MetalRenderer() {}
+void MetalRenderer::save() {}
+void MetalRenderer::restore() {}
+void MetalRenderer::transform(const Mat2D& transform) {}
+void MetalRenderer::drawPath(RenderPath* path, RenderPaint* paint) {}
+void MetalRenderer::clipPath(RenderPath* path) {}
+
+void MetalRenderer::startFrame() {}
+void MetalRenderer::endFrame() {}
+
+RenderPaint* MetalRenderer::makeRenderPaint() { return new MetalRenderPaint(); }
+RenderPath* MetalRenderer::makeRenderPath() { return new MetalRenderPath(); }
+
+static const char kShaderSource[] =
+    "#include <metal_stdlib>\n"
+    "using namespace metal;\n"
+    "struct Vertex\n"
+    "{\n"
+    "    packed_float2 position;\n"
+    "};\n"
+    "struct VSOutput\n"
+    "{\n"
+    "    float4 pos [[position]];\n"
+    "};\n"
+    "struct FSOutput\n"
+    "{\n"
+    "    half4 frag_data [[color(0)]];\n"
+    "};\n"
+    "vertex VSOutput vertexMain(device Vertex* vertexBuffer "
+    "[[buffer(0)]], uint vertexId [[vertex_id]])\n"
+    "{\n"
+    "    VSOutput out = { float4(vertexBuffer[vertexId].position, 0, 1),};\n"
+    "    return out;\n"
+    "}\n"
+    "fragment FSOutput fragmentMain(VSOutput input [[stage_in]])\n"
+    "{\n"
+    "    FSOutput out = { half4(1, 1, 0, 1)};\n"
+    "    return out;\n"
+    "}\n";
+
+bool MetalRenderer::initialize(void* data)
+{
+	auto metalDevice = acquireDevice();
+
+	// Create shaders
+	NSError* error = nil;
+	NSString* srcStr = [[NSString alloc] initWithBytes:kShaderSource
+	                                            length:sizeof(kShaderSource)
+	                                          encoding:NSASCIIStringEncoding];
+	id<MTLLibrary> shaderLibrary = [metalDevice newLibraryWithSource:srcStr
+	                                                         options:nil
+	                                                           error:&error];
+	if (error != nil)
+	{
+		NSString* desc = [error localizedDescription];
+		NSString* reason = [error localizedFailureReason];
+		::fprintf(stderr,
+		          "%s\n%s\n\n",
+		          desc ? [desc UTF8String] : "<unknown>",
+		          reason ? [reason UTF8String] : "");
+	}
+
+	id<MTLFunction> vertexFunction =
+	    [shaderLibrary newFunctionWithName:@"vertexMain"];
+	id<MTLFunction> fragmentFunction =
+	    [shaderLibrary newFunctionWithName:@"fragmentMain"];
+
+	MTLRenderPipelineDescriptor* pipeDesc =
+	    [[MTLRenderPipelineDescriptor alloc] init];
+	// Let's assume we're rendering into BGRA8Unorm...
+	pipeDesc.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
+
+	// pipeDesc.depthAttachmentPixelFormat =
+	// MTLPixelFormatDepth32Float_Stencil8;
+	// pipeDesc.stencilAttachmentPixelFormat =
+	// MTLPixelFormatDepth32Float_Stencil8;
+
+	pipeDesc.sampleCount = 1;
+	pipeDesc.colorAttachments[0].blendingEnabled = NO;
+
+	pipeDesc.vertexFunction = vertexFunction;
+	pipeDesc.fragmentFunction = fragmentFunction;
+
+	m_Pipeline = [metalDevice newRenderPipelineStateWithDescriptor:pipeDesc
+	                                                         error:&error];
+	if (error != nil)
+	{
+		::fprintf(stderr,
+		          "Metal: Error creating pipeline state: %s\n%s\n",
+		          [[error localizedDescription] UTF8String],
+		          [[error localizedFailureReason] UTF8String]);
+		error = nil;
+	}
+
+	MTLDepthStencilDescriptor* depthDesc =
+	    [[MTLDepthStencilDescriptor alloc] init];
+	depthDesc.depthCompareFunction = MTLCompareFunctionAlways;
+	depthDesc.depthWriteEnabled = false;
+	m_DepthStencil = [metalDevice newDepthStencilStateWithDescriptor:depthDesc];
+
+	float fillScreenVertices[] = {1.0f,
+	                              1.0f,
+
+	                              1.0f,
+	                              -1.0f,
+
+	                              -1.0f,
+	                              1.0f,
+
+	                              -1.0f,
+	                              -1.0f};
+	m_FillScreenVertexBuffer =
+	    [metalDevice newBufferWithBytes:fillScreenVertices
+	                             length:sizeof(fillScreenVertices)
+	                            options:MTLResourceOptionCPUCacheModeDefault];
+	return true;
+}
+
+void MetalRenderer::fillScreen()
+{
+	auto encoder = currentCommandEncoder();
+	[encoder setRenderPipelineState:m_Pipeline];
+	[encoder setVertexBuffer:m_FillScreenVertexBuffer offset:0 atIndex:0];
+
+	[encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip
+	            vertexStart:0
+	            vertexCount:4
+	          instanceCount:1];
+}
\ No newline at end of file
diff --git a/renderer/library/src/opengl/opengl_render_paint.cpp b/renderer/library/src/opengl/opengl_render_paint.cpp
new file mode 100644
index 0000000..de9e2de
--- /dev/null
+++ b/renderer/library/src/opengl/opengl_render_paint.cpp
@@ -0,0 +1,189 @@
+#include "opengl/opengl_render_paint.hpp"
+#include "opengl/opengl_renderer.hpp"
+#include "opengl/opengl_render_path.hpp"
+
+#include "rive/shapes/paint/color.hpp"
+#include "rive/contour_stroke.hpp"
+
+using namespace rive;
+
+void fillColorBuffer(float* buffer, unsigned int value)
+{
+	buffer[0] = colorRed(value) / 255.0f;
+	buffer[1] = colorGreen(value) / 255.0f;
+	buffer[2] = colorBlue(value) / 255.0f;
+	buffer[3] = colorAlpha(value) / 255.0f;
+}
+
+void OpenGLRenderPaint::style(RenderPaintStyle style)
+{
+	m_PaintStyle = style;
+	delete m_Stroke;
+	if (m_PaintStyle == RenderPaintStyle::stroke)
+	{
+		m_Stroke = new ContourStroke();
+		m_StrokeDirty = true;
+		if (m_StrokeBuffer != 0)
+		{
+			glDeleteBuffers(1, &m_StrokeBuffer);
+		}
+		glGenBuffers(1, &m_StrokeBuffer);
+	}
+	else
+	{
+		m_Stroke = nullptr;
+		m_StrokeDirty = false;
+	}
+}
+
+void OpenGLRenderPaint::color(unsigned int value)
+{
+	fillColorBuffer(m_Color, value);
+}
+
+void OpenGLRenderPaint::thickness(float value) { m_StrokeThickness = value; }
+
+void OpenGLRenderPaint::join(StrokeJoin value) { m_StrokeJoin = value; }
+
+void OpenGLRenderPaint::cap(StrokeCap value) { m_StrokeCap = value; }
+
+void OpenGLRenderPaint::blendMode(BlendMode value) {}
+
+void OpenGLRenderPaint::linearGradient(float sx, float sy, float ex, float ey)
+{
+	if (m_Gradient == nullptr)
+	{
+		m_Gradient = new OpenGLGradient(1);
+	}
+	m_Gradient->position(sx, sy, ex, ey);
+}
+
+void OpenGLRenderPaint::radialGradient(float sx, float sy, float ex, float ey)
+{
+	if (m_Gradient == nullptr)
+	{
+		m_Gradient = new OpenGLGradient(2);
+	}
+	m_Gradient->position(sx, sy, ex, ey);
+}
+
+void OpenGLRenderPaint::addStop(unsigned int color, float stop)
+{
+	m_Gradient->addStop(color, stop);
+}
+
+void OpenGLRenderPaint::completeGradient() {}
+
+void OpenGLRenderPaint::invalidateStroke()
+{
+	if (m_Stroke != nullptr)
+	{
+		m_StrokeDirty = true;
+	}
+}
+
+OpenGLRenderPaint::~OpenGLRenderPaint()
+{
+	if (m_StrokeBuffer != 0)
+	{
+		glDeleteBuffers(1, &m_StrokeBuffer);
+	}
+	delete m_Gradient;
+	delete m_Stroke;
+}
+
+bool OpenGLRenderPaint::doesDraw() const
+{
+	return m_Color[3] > 0.0f &&
+	       (m_Gradient == nullptr || m_Gradient->m_IsVisible);
+}
+
+void OpenGLRenderPaint::draw(OpenGLRenderer* renderer,
+                             const Mat2D& transform,
+                             OpenGLRenderPath* path)
+{
+	uint32_t type = 0;
+	if (m_Gradient != nullptr)
+	{
+		type = m_Gradient->m_Type;
+		m_Gradient->bind(renderer);
+	}
+
+	glUniform1i(renderer->fillTypeUniformIndex(), type);
+	glUniform4fv(renderer->colorUniformIndex(), 1, m_Color);
+
+	if (m_Stroke != nullptr)
+	{
+		if (m_StrokeDirty)
+		{
+			static Mat2D identity;
+			m_Stroke->reset();
+			path->extrudeStroke(m_Stroke,
+			                    m_StrokeJoin,
+			                    m_StrokeCap,
+			                    m_StrokeThickness / 2.0f,
+			                    identity);
+			m_StrokeDirty = false;
+		}
+
+		const std::vector<Vec2D>& strip = m_Stroke->triangleStrip();
+		auto size = strip.size();
+		if (size == 0)
+		{
+			return;
+		}
+
+		glBindBuffer(GL_ARRAY_BUFFER, m_StrokeBuffer);
+		glBufferData(GL_ARRAY_BUFFER,
+		             size * 2 * sizeof(float),
+		             &strip[0][0],
+		             GL_DYNAMIC_DRAW);
+
+		glEnableVertexAttribArray(0);
+		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, (void*)0);
+
+		m_Stroke->resetRenderOffset();
+		path->renderStroke(m_Stroke, renderer, transform);
+	}
+	else
+	{
+		path->cover(renderer, transform);
+	}
+}
+
+OpenGLGradient::OpenGLGradient(int type) : m_Type(type) {}
+
+void OpenGLGradient::position(float sx, float sy, float ex, float ey)
+{
+	m_Colors.clear();
+	m_Stops.clear();
+	m_IsVisible = false;
+	m_Position[0] = sx;
+	m_Position[1] = sy;
+	m_Position[2] = ex;
+	m_Position[3] = ey;
+}
+
+void OpenGLGradient::bind(OpenGLRenderer* renderer)
+{
+	auto numberOfStops = m_Stops.size();
+
+	glUniform1i(renderer->stopCountUniformIndex(), numberOfStops);
+	glUniform4fv(
+	    renderer->stopColorsUniformIndex(), numberOfStops, &m_Colors[0]);
+	glUniform1fv(renderer->stopsUniformIndex(), numberOfStops, &m_Stops[0]);
+	glUniform4fv(renderer->gradientPositionUniformIndex(), 1, &m_Position[0]);
+}
+
+void OpenGLGradient::addStop(unsigned int color, float stop)
+{
+	auto index = m_Colors.size();
+	m_Colors.resize(index + 4);
+
+	fillColorBuffer(&m_Colors[index], color);
+	if (m_Colors[index + 3] > 0.0f)
+	{
+		m_IsVisible = true;
+	}
+	m_Stops.push_back(stop);
+}
\ No newline at end of file
diff --git a/renderer/library/src/opengl/opengl_render_path.cpp b/renderer/library/src/opengl/opengl_render_path.cpp
new file mode 100644
index 0000000..daf64cf
--- /dev/null
+++ b/renderer/library/src/opengl/opengl_render_path.cpp
@@ -0,0 +1,250 @@
+#include "opengl/opengl_render_path.hpp"
+#include "opengl/opengl_renderer.hpp"
+#include "opengl/opengl.h"
+#include "rive/contour_stroke.hpp"
+
+using namespace rive;
+
+OpenGLRenderPath::OpenGLRenderPath() { glGenBuffers(1, &m_ContourBuffer); }
+
+OpenGLRenderPath::~OpenGLRenderPath() { glDeleteBuffers(1, &m_ContourBuffer); }
+void OpenGLRenderPath::fillRule(FillRule value) { m_FillRule = value; }
+
+void OpenGLRenderPath::stencil(OpenGLRenderer* renderer, const Mat2D& transform)
+{
+	if (isContainer())
+	{
+		for (auto& subPath : m_SubPaths)
+		{
+			Mat2D pathTransform;
+			// Mat2D::multiply(pathTransform, transform, subPath.transform());
+			Mat2D::multiply(pathTransform, transform, subPath.transform());
+			reinterpret_cast<OpenGLRenderPath*>(subPath.path())
+			    ->stencil(renderer, pathTransform);
+		}
+		return;
+	}
+
+	// glUseProgram(renderer->program());
+	std::size_t vertexCount;
+
+	if (isDirty())
+	{
+		computeContour();
+		vertexCount = m_ContourVertices.size();
+		// We only want the indices to go from the off contour point (bounds'
+		// last point). First 4 points are bounds.
+		renderer->updateIndexBuffer(vertexCount - 3);
+
+		glBindBuffer(GL_ARRAY_BUFFER, m_ContourBuffer);
+		glBufferData(GL_ARRAY_BUFFER,
+		             vertexCount * 2 * sizeof(float),
+		             &m_ContourVertices[0][0],
+		             GL_DYNAMIC_DRAW);
+	}
+	else
+	{
+		glBindBuffer(GL_ARRAY_BUFFER, m_ContourBuffer);
+		vertexCount = m_ContourVertices.size();
+	}
+
+	// 4 vertices of bounds and one for the repeated start (repeated on close so
+	// we don't need to modulate indices and share them across all paths with
+	// different contours).
+	if (vertexCount < 5)
+	{
+		return;
+	}
+
+	auto triangleCount = vertexCount - 5;
+	// printf("VCOUNT: %i E: %i\n", vertexCount, triangleCount);
+	// printf("X: %f %f\n", transform[0], transform[1]);
+	// printf("Y: %f %f\n", transform[2], transform[3]);
+	// printf("T: %f %f\n", transform[4], transform[5]);
+
+	float m4[16] = {transform[0],
+	                transform[1],
+	                0.0,
+	                0.0,
+	                transform[2],
+	                transform[3],
+	                0.0,
+	                0.0,
+	                0.0,
+	                0.0,
+	                1.0,
+	                0.0,
+	                transform[4],
+	                transform[5],
+	                0.0,
+	                1.0};
+
+	glUniformMatrix4fv(renderer->transformUniformIndex(), 1, GL_FALSE, m4);
+
+	glEnableVertexAttribArray(0);
+	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, (void*)0);
+
+	// glDisable(GL_CULL_FACE);
+	// glDisable(GL_DEPTH_TEST);
+	// glEnable(GL_BLEND);
+	// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, renderer->indexBuffer());
+	// Index buffer offset is always after first 6 (2 triangles for bounds).
+
+	// Draw the triangulated contour (triangle fans from the bottom left of the
+	// AABB) into the stencil buffer.
+	glDrawElements(GL_TRIANGLES,
+	               triangleCount * 3,
+	               GL_UNSIGNED_SHORT,
+	               (void*)(6 * sizeof(unsigned short)));
+
+	// GLenum err;
+	// while ((err = glGetError()) != GL_NO_ERROR)
+	// {
+	// 	// Process/log the error.
+	// 	fprintf(stderr, "ERRR:: %i\n", err);
+	// }
+	// glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
+	// static unsigned short indices[3] = {0, 2, 2};
+	// glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, &indices[0]);
+}
+
+void OpenGLRenderPath::cover(OpenGLRenderer* renderer,
+                             const Mat2D& transform,
+                             const Mat2D& localTransform)
+{
+	if (isContainer())
+	{
+		for (auto& subPath : m_SubPaths)
+		{
+			const Mat2D& subPathTransform = subPath.transform();
+			Mat2D pathTransform;
+			Mat2D::multiply(pathTransform, transform, subPathTransform);
+			reinterpret_cast<OpenGLRenderPath*>(subPath.path())
+			    ->cover(renderer, pathTransform, subPathTransform);
+		}
+		return;
+	}
+
+	glBindBuffer(GL_ARRAY_BUFFER, m_ContourBuffer);
+	auto vertexCount = m_ContourVertices.size();
+
+	if (vertexCount < 5)
+	{
+		return;
+	}
+
+	{
+		float m4[16] = {transform[0],
+		                transform[1],
+		                0.0,
+		                0.0,
+		                transform[2],
+		                transform[3],
+		                0.0,
+		                0.0,
+		                0.0,
+		                0.0,
+		                1.0,
+		                0.0,
+		                transform[4],
+		                transform[5],
+		                0.0,
+		                1.0};
+
+		glUniformMatrix4fv(renderer->transformUniformIndex(), 1, GL_FALSE, m4);
+	}
+	{
+		float m4[16] = {localTransform[0],
+		                localTransform[1],
+		                0.0,
+		                0.0,
+		                localTransform[2],
+		                localTransform[3],
+		                0.0,
+		                0.0,
+		                0.0,
+		                0.0,
+		                1.0,
+		                0.0,
+		                localTransform[4],
+		                localTransform[5],
+		                0.0,
+		                1.0};
+
+		glUniformMatrix4fv(
+		    renderer->shapeTransformUniformIndex(), 1, GL_FALSE, m4);
+	}
+
+	glEnableVertexAttribArray(0);
+	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, (void*)0);
+
+	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, renderer->indexBuffer());
+
+	// Draw bounds.
+	glDrawElements(GL_TRIANGLES, 2 * 3, GL_UNSIGNED_SHORT, (void*)(0));
+}
+
+void OpenGLRenderPath::renderStroke(ContourStroke* stroke,
+                                    OpenGLRenderer* renderer,
+                                    const Mat2D& transform,
+                                    const Mat2D& localTransform)
+{
+	if (isContainer())
+	{
+		for (auto& subPath : m_SubPaths)
+		{
+			reinterpret_cast<OpenGLRenderPath*>(subPath.path())
+			    ->renderStroke(stroke, renderer, transform, localTransform);
+		}
+		return;
+	}
+
+	{
+		float m4[16] = {transform[0],
+		                transform[1],
+		                0.0,
+		                0.0,
+		                transform[2],
+		                transform[3],
+		                0.0,
+		                0.0,
+		                0.0,
+		                0.0,
+		                1.0,
+		                0.0,
+		                transform[4],
+		                transform[5],
+		                0.0,
+		                1.0};
+
+		glUniformMatrix4fv(renderer->transformUniformIndex(), 1, GL_FALSE, m4);
+	}
+	{
+		float m4[16] = {localTransform[0],
+		                localTransform[1],
+		                0.0,
+		                0.0,
+		                localTransform[2],
+		                localTransform[3],
+		                0.0,
+		                0.0,
+		                0.0,
+		                0.0,
+		                1.0,
+		                0.0,
+		                localTransform[4],
+		                localTransform[5],
+		                0.0,
+		                1.0};
+
+		glUniformMatrix4fv(
+		    renderer->shapeTransformUniformIndex(), 1, GL_FALSE, m4);
+	}
+
+	std::size_t start, end;
+	stroke->nextRenderOffset(start, end);
+
+	glDrawArrays(GL_TRIANGLE_STRIP, start, end - start);
+}
\ No newline at end of file
diff --git a/renderer/library/src/opengl/opengl_renderer.cpp b/renderer/library/src/opengl/opengl_renderer.cpp
new file mode 100644
index 0000000..e7bfbda
--- /dev/null
+++ b/renderer/library/src/opengl/opengl_renderer.cpp
@@ -0,0 +1,366 @@
+#include "opengl/opengl_renderer.hpp"
+#include "opengl/opengl_render_path.hpp"
+#include "opengl/opengl_render_paint.hpp"
+#include "opengl_shaders.cpp"
+#include <cassert>
+
+using namespace rive;
+
+GLuint createAndCompileShader(GLuint type, const char* source);
+
+OpenGLRenderer::OpenGLRenderer() {}
+OpenGLRenderer::~OpenGLRenderer()
+{
+	glDeleteProgram(m_Program);
+	glDeleteShader(m_VertexShader);
+	glDeleteShader(m_FragmentShader);
+	glDeleteBuffers(1, &m_IndexBuffer);
+	glDeleteBuffers(1, &m_BlitBuffer);
+	glDeleteVertexArrays(1, &m_VertexArray);
+}
+
+bool OpenGLRenderer::initialize(void* data)
+{
+	assert(m_VertexShader == 0 && m_FragmentShader == 0 && m_Program == 0);
+
+	m_VertexShader =
+	    createAndCompileShader(GL_VERTEX_SHADER, vertexShaderSource);
+	if (m_VertexShader == 0)
+	{
+		return false;
+	}
+
+	m_FragmentShader =
+	    createAndCompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
+	if (m_FragmentShader == 0)
+	{
+		return false;
+	}
+
+	m_Program = glCreateProgram();
+	glAttachShader(m_Program, m_VertexShader);
+	glAttachShader(m_Program, m_FragmentShader);
+	glLinkProgram(m_Program);
+	GLint isLinked = 0;
+	glGetProgramiv(m_Program, GL_LINK_STATUS, (int*)&isLinked);
+	if (isLinked == GL_FALSE)
+	{
+		GLint maxLength = 0;
+		glGetProgramiv(m_Program, GL_INFO_LOG_LENGTH, &maxLength);
+
+		std::vector<GLchar> infoLog(maxLength);
+		glGetProgramInfoLog(m_Program, maxLength, &maxLength, &infoLog[0]);
+		fprintf(stderr, "Failed to link program %s\n", &infoLog[0]);
+		return false;
+	}
+
+	// Create index buffer which we'll grow and populate as necessary.
+	glGenBuffers(1, &m_IndexBuffer);
+	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexBuffer);
+
+	// Create vertex buffer for blitting to full viewport coordinates.
+	float blitBuffer[8] = {
+	    -1.0f,
+	    1.0f,
+
+	    1.0f,
+	    1.0f,
+
+	    1.0f,
+	    -1.0f,
+
+	    -1.0f,
+	    -1.0f,
+	};
+	glGenBuffers(1, &m_BlitBuffer);
+	glBindBuffer(GL_ARRAY_BUFFER, m_BlitBuffer);
+	glBufferData(
+	    GL_ARRAY_BUFFER, 8 * sizeof(float), &blitBuffer[0], GL_STATIC_DRAW);
+
+	// Two triangles for bounds.
+	m_Indices.emplace_back(0);
+	m_Indices.emplace_back(1);
+	m_Indices.emplace_back(2);
+	m_Indices.emplace_back(2);
+	m_Indices.emplace_back(3);
+	m_Indices.emplace_back(0);
+
+	glBufferData(GL_ELEMENT_ARRAY_BUFFER,
+	             m_Indices.size() * sizeof(unsigned short),
+	             &m_Indices[0],
+	             GL_STATIC_DRAW);
+
+	glGenVertexArrays(1, &m_VertexArray);
+	glBindVertexArray(m_VertexArray);
+
+	glUseProgram(m_Program);
+
+	m_ProjectionUniformIndex = glGetUniformLocation(m_Program, "projection");
+	m_TransformUniformIndex = glGetUniformLocation(m_Program, "transform");
+
+	m_FillTypeUniformIndex = glGetUniformLocation(m_Program, "fillType");
+	m_StopCountUniformIndex = glGetUniformLocation(m_Program, "count");
+	m_StopColorsUniformIndex = glGetUniformLocation(m_Program, "colors");
+	m_StopsUniformIndex = glGetUniformLocation(m_Program, "stops");
+	m_ColorUniformIndex = glGetUniformLocation(m_Program, "color");
+	m_GradientPositionUniformIndex =
+	    glGetUniformLocation(m_Program, "position");
+	m_ShapeTransformUniformIndex =
+	    glGetUniformLocation(m_Program, "localTransform");
+
+	float projection[16] = {0.0f};
+	orthographicProjection(projection, 0.0f, 800, 800, 0.0f, 0.0f, 1.0f);
+	modelViewProjection(projection);
+
+	return true;
+}
+
+void OpenGLRenderer::drawPath(RenderPath* path, RenderPaint* paint)
+{
+	auto glPaint = static_cast<OpenGLRenderPaint*>(paint);
+	// if (glPaint->style() == RenderPaintStyle::stroke || !glPaint->doesDraw())
+
+	if (!glPaint->doesDraw())
+	{
+		return;
+	}
+	bool needsStencil = glPaint->style() == RenderPaintStyle::fill;
+
+	glColorMask(false, false, false, false);
+	// Set fill type to 0 so we don't perform any gradient fragment calcs.
+	glUniform1i(fillTypeUniformIndex(), 0);
+
+	if (isClippingDirty())
+	{
+		if (m_IsClipping)
+		{
+			// Clear previous clip.
+			glStencilMask(0xFF);
+			glClear(GL_STENCIL_BUFFER_BIT);
+
+			// TODO: instead of clearing the entire buffer, as we clip we could
+			// compute the combined clipping area set and clear that here.
+		}
+		auto clipLength = m_ClipPaths.size();
+		if (clipLength > 0)
+		{
+			m_IsClipping = true;
+			SubPath& firstClipPath = m_ClipPaths[0];
+
+			glStencilMask(0xFF);
+			glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
+
+			glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP);
+			glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP);
+			static_cast<OpenGLRenderPath*>(firstClipPath.path())
+			    ->stencil(this, firstClipPath.transform());
+
+			// Fail when not equal to 0 and replace with 0x80 (mark high bit as
+			// included in clip). Require stencil mask (write mask) of 0xFF and
+			// stencil func mask of 0x7F such that the comparison looks for 0
+			// but write 0x80.
+			glStencilMask(0xFF);
+			glStencilFunc(GL_NOTEQUAL, 0x80, 0x7F);
+			glStencilOp(GL_ZERO, GL_ZERO, GL_REPLACE);
+
+			glBindBuffer(GL_ARRAY_BUFFER, m_BlitBuffer);
+			glEnableVertexAttribArray(0);
+			glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, (void*)0);
+			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexBuffer);
+
+			float m4[16] = {1.0,
+			                0.0,
+			                0.0,
+			                0.0,
+
+			                0.0,
+			                1.0,
+			                0.0,
+			                0.0,
+
+			                0.0,
+			                0.0,
+			                1.0,
+			                0.0,
+
+			                0.0,
+			                0.0,
+			                0.0,
+			                1.0};
+
+			glUniformMatrix4fv(transformUniformIndex(), 1, GL_FALSE, m4);
+			glUniformMatrix4fv(m_ProjectionUniformIndex, 1, GL_FALSE, m4);
+
+			// Draw bounds.
+			glDrawElements(GL_TRIANGLES, 2 * 3, GL_UNSIGNED_SHORT, (void*)(0));
+
+			glUniformMatrix4fv(
+			    m_ProjectionUniformIndex, 1, GL_FALSE, m_ModelViewProjection);
+			for (int i = 1; i < clipLength; i++)
+			// for (int i = 1; i < 0; i++)
+			{
+
+				// When already clipping we want to write only to the last/lower
+				// 7 bits as our high 8th bit is used to mark clipping
+				// inclusion.
+				glStencilMask(0x7F);
+				// Pass only if that 8th bit is set. This allows us to write our
+				// new winding into the lower 7 bits.
+				glStencilFunc(GL_EQUAL, 0x80, 0x80);
+				SubPath& nextClipPath = m_ClipPaths[i];
+
+				glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP);
+				glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP);
+				static_cast<OpenGLRenderPath*>(nextClipPath.path())
+				    ->stencil(this, nextClipPath.transform());
+
+				// Fail when not equal to 0 and replace with 0x80 (mark high bit
+				// as included in clip). Require stencil mask (write mask) of
+				// 0xFF and stencil func mask of 0x7F such that the comparison
+				// looks for 0 but write 0x80.
+				glStencilMask(0xFF);
+				glStencilFunc(GL_NOTEQUAL, 0x80, 0x7F);
+				glStencilOp(GL_ZERO, GL_ZERO, GL_REPLACE);
+
+				glBindBuffer(GL_ARRAY_BUFFER, m_BlitBuffer);
+				glEnableVertexAttribArray(0);
+				glVertexAttribPointer(
+				    0, 2, GL_FLOAT, GL_FALSE, 2 * 4, (void*)0);
+
+				glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexBuffer);
+
+				glUniformMatrix4fv(transformUniformIndex(), 1, GL_FALSE, m4);
+				glUniformMatrix4fv(m_ProjectionUniformIndex, 1, GL_FALSE, m4);
+				// Draw bounds.
+				glDrawElements(
+				    GL_TRIANGLES, 2 * 3, GL_UNSIGNED_SHORT, (void*)(0));
+
+				glUniformMatrix4fv(m_ProjectionUniformIndex,
+				                   1,
+				                   GL_FALSE,
+				                   m_ModelViewProjection);
+			}
+		}
+		else
+		{
+			m_IsClipping = false;
+		}
+	}
+
+	auto glPath = static_cast<OpenGLRenderPath*>(path);
+
+	if (needsStencil)
+	{
+		// Set up stencil buffer.
+		if (m_IsClipping)
+		{
+			glStencilMask(0x7F);
+			glStencilFunc(GL_EQUAL, 0x80, 0x80);
+		}
+		else
+		{
+			glStencilMask(0xFF);
+			glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
+		}
+
+		glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP);
+		glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP);
+
+		auto xform = transform();
+		glPath->stencil(this, xform);
+
+		glColorMask(true, true, true, true);
+		glStencilFunc(GL_NOTEQUAL, 0, m_IsClipping ? 0x7F : 0xFF);
+		glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
+	}
+	else
+	{
+		if (m_IsClipping)
+		{
+			glStencilMask(0x7F);
+			glStencilFunc(GL_EQUAL, 0x80, 0x80);
+		}
+		else
+		{
+			glStencilMask(0xFF);
+			glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
+		}
+		glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_KEEP);
+		glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_KEEP);
+		glColorMask(true, true, true, true);
+		// glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
+		glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
+	}
+	glPaint->draw(this, transform(), glPath);
+
+	// glPath->cover(this, transform());
+}
+
+void OpenGLRenderer::startFrame()
+{
+	LowLevelRenderer::startFrame();
+	glUseProgram(m_Program);
+	glEnableVertexAttribArray(0);
+	glUniformMatrix4fv(
+	    m_ProjectionUniformIndex, 1, GL_FALSE, m_ModelViewProjection);
+	glEnable(GL_STENCIL_TEST);
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+}
+
+void OpenGLRenderer::endFrame() {}
+
+RenderPaint* OpenGLRenderer::makeRenderPaint()
+{
+	return new OpenGLRenderPaint();
+}
+RenderPath* OpenGLRenderer::makeRenderPath() { return new OpenGLRenderPath(); }
+
+void OpenGLRenderer::updateIndexBuffer(std::size_t contourLength)
+{
+	if (contourLength < 2)
+	{
+		return;
+	}
+	auto edgeCount = (m_Indices.size() - 6) / 3;
+	auto targetEdgeCount = contourLength - 2;
+	if (edgeCount < targetEdgeCount)
+	{
+		while (edgeCount < targetEdgeCount)
+		{
+			m_Indices.push_back(3);
+			m_Indices.push_back(edgeCount + 4);
+			m_Indices.push_back(edgeCount + 5);
+			edgeCount++;
+		}
+
+		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexBuffer);
+		glBufferData(GL_ELEMENT_ARRAY_BUFFER,
+		             m_Indices.size() * sizeof(unsigned short),
+		             &m_Indices[0],
+		             GL_STATIC_DRAW);
+	}
+}
+
+GLuint createAndCompileShader(GLuint type, const char* source)
+{
+	GLuint shader = glCreateShader(type);
+	glShaderSource(shader, 1, &source, nullptr);
+	glCompileShader(shader);
+	GLint isCompiled = 0;
+	glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
+	if (isCompiled == GL_FALSE)
+	{
+		GLint maxLength = 0;
+		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
+
+		std::vector<GLchar> infoLog(maxLength);
+		glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);
+		fprintf(stderr, "Failed to compile shader %s\n", &infoLog[0]);
+		glDeleteShader(shader);
+
+		return 0;
+	}
+
+	return shader;
+}
\ No newline at end of file
diff --git a/renderer/library/src/opengl/opengl_shaders.cpp b/renderer/library/src/opengl/opengl_shaders.cpp
new file mode 100644
index 0000000..9da09c9
--- /dev/null
+++ b/renderer/library/src/opengl/opengl_shaders.cpp
@@ -0,0 +1,93 @@
+
+const char* vertexShaderSource = R"""(
+#version 330 core
+
+layout (location = 0) in vec2 position;
+
+out vec2 pos;
+
+uniform mat4 projection;
+uniform mat4 transform;
+uniform mat4 localTransform;
+
+void main() 
+{
+    gl_Position = projection*transform*vec4(position, 0.0, 1.0);
+    pos = (localTransform*vec4(position, 0.0, 1.0)).xy;
+}
+)""";
+
+const char* fragmentShaderSource = R"""(
+#version 330 core
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+uniform vec4 color;
+uniform vec4 position;
+uniform int count;
+uniform vec4 colors[16];
+uniform float stops[16];
+uniform int fillType;
+in vec2 pos;
+out vec4 fragColor;
+
+void main()
+{
+    if (fillType == 0)
+    {
+        // solid
+        fragColor = color;//vec4(color.rgb * color.a, color.a);
+    }
+    else if (fillType == 1)
+    {
+        // linear
+
+        vec2 start = position.xy;
+        vec2 end = position.zw;
+
+        
+        vec2 toEnd = end - start;
+        float lengthSquared = toEnd.x * toEnd.x + toEnd.y * toEnd.y;
+        float f = dot(pos - start, toEnd) / lengthSquared;
+        fragColor =
+            mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
+        for (int i = 1; i < 15; ++i)
+        {
+            if (i >= count - 1)
+            {
+                break;
+            }
+            fragColor = mix(fragColor,
+                            colors[i + 1],
+                            smoothstep(stops[i], stops[i + 1], f));
+        }
+        // float alpha = fragColor.w;
+        // fragColor = vec4(fragColor.xyz * alpha, alpha);
+    }
+    else if (fillType == 2)
+    {
+        // radial
+
+        vec2 start = position.xy;
+        vec2 end = position.zw;
+        
+        float f = distance(start, pos) / distance(start, end);
+        fragColor =
+            mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
+        for (int i = 1; i < 15; ++i)
+        {
+            if (i >= count - 1)
+            {
+                break;
+            }
+            fragColor = mix(fragColor,
+                            colors[i + 1],
+                            smoothstep(stops[i], stops[i + 1], f));
+        }
+        // float alpha = fragColor.w;
+        // fragColor = vec4(fragColor.xyz * alpha, alpha);
+    }
+}
+)""";
\ No newline at end of file
diff --git a/renderer/viewer/assets/404.riv b/renderer/viewer/assets/404.riv
new file mode 100644
index 0000000..c7a33a1
--- /dev/null
+++ b/renderer/viewer/assets/404.riv
Binary files differ
diff --git a/renderer/viewer/assets/bone_deform.riv b/renderer/viewer/assets/bone_deform.riv
new file mode 100644
index 0000000..9f6e275
--- /dev/null
+++ b/renderer/viewer/assets/bone_deform.riv
Binary files differ
diff --git a/renderer/viewer/assets/car.riv b/renderer/viewer/assets/car.riv
new file mode 100644
index 0000000..81202cb
--- /dev/null
+++ b/renderer/viewer/assets/car.riv
Binary files differ
diff --git a/renderer/viewer/assets/clip.riv b/renderer/viewer/assets/clip.riv
new file mode 100644
index 0000000..eb9a735
--- /dev/null
+++ b/renderer/viewer/assets/clip.riv
Binary files differ
diff --git a/renderer/viewer/assets/clipped_circle_star_2.riv b/renderer/viewer/assets/clipped_circle_star_2.riv
new file mode 100644
index 0000000..4172640
--- /dev/null
+++ b/renderer/viewer/assets/clipped_circle_star_2.riv
Binary files differ
diff --git a/renderer/viewer/assets/control.riv b/renderer/viewer/assets/control.riv
new file mode 100644
index 0000000..3f7b044
--- /dev/null
+++ b/renderer/viewer/assets/control.riv
Binary files differ
diff --git a/renderer/viewer/assets/gradient.riv b/renderer/viewer/assets/gradient.riv
new file mode 100644
index 0000000..3e022a0
--- /dev/null
+++ b/renderer/viewer/assets/gradient.riv
Binary files differ
diff --git a/renderer/viewer/assets/juice.riv b/renderer/viewer/assets/juice.riv
new file mode 100644
index 0000000..5df55e4
--- /dev/null
+++ b/renderer/viewer/assets/juice.riv
Binary files differ
diff --git a/renderer/viewer/assets/leg_issues.riv b/renderer/viewer/assets/leg_issues.riv
new file mode 100644
index 0000000..7c76170
--- /dev/null
+++ b/renderer/viewer/assets/leg_issues.riv
Binary files differ
diff --git a/renderer/viewer/assets/marty.riv b/renderer/viewer/assets/marty.riv
new file mode 100644
index 0000000..abc309f
--- /dev/null
+++ b/renderer/viewer/assets/marty.riv
Binary files differ
diff --git a/renderer/viewer/assets/marty_v2.riv b/renderer/viewer/assets/marty_v2.riv
new file mode 100644
index 0000000..774fee6
--- /dev/null
+++ b/renderer/viewer/assets/marty_v2.riv
Binary files differ
diff --git a/renderer/viewer/assets/off_road_car.riv b/renderer/viewer/assets/off_road_car.riv
new file mode 100644
index 0000000..81202cb
--- /dev/null
+++ b/renderer/viewer/assets/off_road_car.riv
Binary files differ
diff --git a/renderer/viewer/assets/polygon_party.riv b/renderer/viewer/assets/polygon_party.riv
new file mode 100644
index 0000000..04c5148
--- /dev/null
+++ b/renderer/viewer/assets/polygon_party.riv
Binary files differ
diff --git a/renderer/viewer/assets/rotate_square.riv b/renderer/viewer/assets/rotate_square.riv
new file mode 100644
index 0000000..eba670a
--- /dev/null
+++ b/renderer/viewer/assets/rotate_square.riv
Binary files differ
diff --git a/renderer/viewer/assets/runner.riv b/renderer/viewer/assets/runner.riv
new file mode 100644
index 0000000..b6a0feb
--- /dev/null
+++ b/renderer/viewer/assets/runner.riv
Binary files differ
diff --git a/renderer/viewer/assets/runner_boy.riv b/renderer/viewer/assets/runner_boy.riv
new file mode 100644
index 0000000..f4b3780
--- /dev/null
+++ b/renderer/viewer/assets/runner_boy.riv
Binary files differ
diff --git a/renderer/viewer/assets/simple_stroke.riv b/renderer/viewer/assets/simple_stroke.riv
new file mode 100644
index 0000000..28eb071
--- /dev/null
+++ b/renderer/viewer/assets/simple_stroke.riv
Binary files differ
diff --git a/renderer/viewer/assets/simple_stroke_only.riv b/renderer/viewer/assets/simple_stroke_only.riv
new file mode 100644
index 0000000..8fc84f1
--- /dev/null
+++ b/renderer/viewer/assets/simple_stroke_only.riv
Binary files differ
diff --git a/renderer/viewer/assets/stroke_caps.riv b/renderer/viewer/assets/stroke_caps.riv
new file mode 100644
index 0000000..d9622cb
--- /dev/null
+++ b/renderer/viewer/assets/stroke_caps.riv
Binary files differ
diff --git a/renderer/viewer/assets/triangle.riv b/renderer/viewer/assets/triangle.riv
new file mode 100644
index 0000000..42c0d0e
--- /dev/null
+++ b/renderer/viewer/assets/triangle.riv
Binary files differ
diff --git a/renderer/viewer/assets/zombie_leg.riv b/renderer/viewer/assets/zombie_leg.riv
new file mode 100644
index 0000000..5df4bdf
--- /dev/null
+++ b/renderer/viewer/assets/zombie_leg.riv
Binary files differ
diff --git a/renderer/viewer/build.sh b/renderer/viewer/build.sh
new file mode 100755
index 0000000..0992331
--- /dev/null
+++ b/renderer/viewer/build.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -e
+
+# pushd ../../
+# ./build.sh $@
+# popd
+
+# pushd ../library
+# ./build.sh $@
+# popd
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]; then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+elif [ "$OPTION" = "clean" ]; then
+    echo Cleaning project ...
+    premake5 gmake && make clean
+elif [ "$OPTION" = "release" ]; then
+    premake5 --with-low-level-rendering gmake && make config=release -j7
+else
+    premake5 --with-low-level-rendering gmake && make -j7
+fi
diff --git a/renderer/viewer/build/bin/debug/rive_diligent_viewer b/renderer/viewer/build/bin/debug/rive_diligent_viewer
new file mode 100755
index 0000000..df8f839
--- /dev/null
+++ b/renderer/viewer/build/bin/debug/rive_diligent_viewer
Binary files differ
diff --git a/renderer/viewer/build/premake5.lua b/renderer/viewer/build/premake5.lua
new file mode 100644
index 0000000..79d9b46
--- /dev/null
+++ b/renderer/viewer/build/premake5.lua
@@ -0,0 +1,60 @@
+workspace "rive"
+configurations {"debug", "release"}
+
+BASE_DIR = path.getabsolute("../../../build")
+location("./")
+dofile(path.join(BASE_DIR, "premake5.lua"))
+
+BASE_DIR = path.getabsolute("../../library/build")
+location("./")
+dofile(path.join(BASE_DIR, "premake5.lua"))
+
+DEPENDENCIES_DIR = "../../dependencies/local/";
+
+project "rive_low_level_viewer"
+kind "ConsoleApp"
+language "C++"
+cppdialect "C++17"
+targetdir "bin/%{cfg.buildcfg}"
+objdir "obj/%{cfg.buildcfg}"
+includedirs {"../include", "../../../include", "../../library/include", "%{DEPENDENCIES_DIR}/include"}
+
+if os.host() == "macosx" then
+    links {"Cocoa.framework", "IOKit.framework", "CoreVideo.framework", "Metal.framework", "QuartzCore.framework",
+           "OpenGL.framework", "glfw3"}
+    defines {"RIVE_HAS_OPENGL", "RIVE_HAS_METAL"}
+    defines {"GL_SILENCE_DEPRECATION"}
+    includedirs {"%{DEPENDENCIES_DIR}/include/gl3w"}
+    files {"../src/**.mm"}
+end
+
+links {"rive", "rive_renderer"}
+libdirs {"../../../build/bin/%{cfg.buildcfg}", "../../library/build/bin/%{cfg.buildcfg}", "%{DEPENDENCIES_DIR}/lib"}
+
+files {"../src/**.cpp"}
+
+buildoptions {"-Wall", "-fno-rtti"}
+
+filter "configurations:debug"
+defines {"DEBUG"}
+symbols "On"
+
+filter "configurations:release"
+defines {"RELEASE"}
+defines {"NDEBUG"}
+optimize "On"
+
+-- Clean Function --
+newaction {
+    trigger = "clean",
+    description = "clean the build",
+    execute = function()
+        print("clean the build...")
+        os.rmdir("./bin")
+        os.rmdir("./obj")
+        os.remove("Makefile")
+        -- no wildcards in os.remove, so use shell
+        os.execute("rm *.make")
+        print("build cleaned")
+    end
+}
diff --git a/renderer/viewer/run.sh b/renderer/viewer/run.sh
new file mode 100755
index 0000000..21ddd3c
--- /dev/null
+++ b/renderer/viewer/run.sh
@@ -0,0 +1,15 @@
+
+OPTION=$1
+
+
+if [ "$OPTION" = 'help' ]; then
+    echo runs.sh - runs debug viewer
+    echo run.sh release - run release viewer
+    echo run.sh debug - run viewer with debugger
+elif [ "$OPTION" = "release" ]; then
+    ./build/bin/release/rive_low_level_viewer $2
+elif [ "$OPTION" = "debug" ]; then
+    lldb ./build/bin/debug/rive_low_level_viewer $2
+else
+    ./build/bin/debug/rive_low_level_viewer $1
+fi
diff --git a/renderer/viewer/src/gl.cpp b/renderer/viewer/src/gl.cpp
new file mode 100644
index 0000000..2919423
--- /dev/null
+++ b/renderer/viewer/src/gl.cpp
@@ -0,0 +1,17 @@
+#ifdef RIVE_HAS_OPENGL
+#include "opengl/opengl_renderer.hpp"
+namespace rive
+{
+	class ViewerGLRenderer : public OpenGLRenderer
+	{
+	public:
+		void startFrame() override
+		{
+			glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
+			glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+			OpenGLRenderer::startFrame();
+		}
+	};
+	LowLevelRenderer* makeRendererOpenGL() { return new ViewerGLRenderer(); }
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/viewer/src/metal.mm b/renderer/viewer/src/metal.mm
new file mode 100644
index 0000000..cb6ca45
--- /dev/null
+++ b/renderer/viewer/src/metal.mm
@@ -0,0 +1,79 @@
+#ifdef RIVE_HAS_METAL
+#include "metal/metal_renderer.hpp"
+
+#import <Metal/Metal.h>
+#import <QuartzCore/CAMetalLayer.h>
+#import <AppKit/NSWindow.h>
+
+/// Implemented in viewer.cpp, must be available before instancing the
+/// MetalRenderer.
+extern void* viewerNativeWindowHandle;
+
+namespace rive
+{
+	class ViewerMetalRenderer : public MetalRenderer
+	{
+	private:
+		id<CAMetalDrawable> m_FrameDrawable;
+		id<MTLCommandBuffer> m_FrameCommandBuffer;
+		id<MTLRenderCommandEncoder> m_FrameCommandEncoder;
+		id<MTLCommandQueue> m_CommandQueue;
+		CAMetalLayer* m_MetalLayer;
+
+	public:
+		id<MTLDevice> acquireDevice() override
+		{
+			const id<MTLDevice> gpu = MTLCreateSystemDefaultDevice();
+			m_MetalLayer = [CAMetalLayer layer];
+			m_MetalLayer.device = gpu;
+			m_MetalLayer.opaque = YES;
+			NSWindow* nswindow =
+			    static_cast<NSWindow*>(viewerNativeWindowHandle);
+			nswindow.contentView.layer = m_MetalLayer;
+			nswindow.contentView.wantsLayer = YES;
+			m_CommandQueue = [gpu newCommandQueue];
+
+			return gpu;
+		}
+
+		id<MTLRenderCommandEncoder> currentCommandEncoder() override
+		{
+			return m_FrameCommandEncoder;
+		}
+
+		void startFrame() override
+		{
+			m_FrameDrawable = [m_MetalLayer nextDrawable];
+			// Get the command buffer from the command queue.
+			m_FrameCommandBuffer = [m_CommandQueue commandBuffer];
+
+			// Get the texture for the next frame.
+			id<MTLTexture> texture = m_FrameDrawable.texture;
+
+			MTLRenderPassDescriptor* passDescriptor =
+			    [MTLRenderPassDescriptor renderPassDescriptor];
+			passDescriptor.colorAttachments[0].texture = texture;
+			passDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
+			passDescriptor.colorAttachments[0].storeAction =
+			    MTLStoreActionStore;
+			passDescriptor.colorAttachments[0].clearColor =
+			    MTLClearColorMake(0.0, 0.0, 0.0, 0.0);
+			// Get the encoder which we will use for all draw calls on the main
+			// buffer.
+			m_FrameCommandEncoder = [m_FrameCommandBuffer
+			    renderCommandEncoderWithDescriptor:passDescriptor];
+			fillScreen();
+		}
+
+		void endFrame() override
+		{
+			[m_FrameCommandEncoder endEncoding];
+
+			[m_FrameCommandBuffer presentDrawable:m_FrameDrawable];
+			[m_FrameCommandBuffer commit];
+		}
+	};
+
+	LowLevelRenderer* makeRendererMetal() { return new ViewerMetalRenderer(); }
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/renderer/viewer/src/viewer.cpp b/renderer/viewer/src/viewer.cpp
new file mode 100644
index 0000000..bcfcba6
--- /dev/null
+++ b/renderer/viewer/src/viewer.cpp
@@ -0,0 +1,208 @@
+#include "rive/artboard.hpp"
+#include "rive/file.hpp"
+#include "rive/layout.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "low_level/low_level_renderer.hpp"
+#include <string.h>
+
+#if __APPLE__
+#define GLFW_EXPOSE_NATIVE_COCOA
+#endif
+
+#include "graphics_api.hpp"
+
+// Make sure gl3w is included before glfw3
+// #include "GL/gl3w.h"
+#include "GLFW/glfw3.h"
+#include "GLFW/glfw3native.h"
+
+#include <stdio.h>
+
+void* viewerNativeWindowHandle = nullptr;
+
+int main(int argc, const char** argv)
+{
+	auto graphicsApi = rive::GraphicsApi::unknown;
+	if (argc >= 2)
+	{
+		// Figure out if the user requested a specific Graphics API.
+		const char* deviceName = argv[1];
+		if (strcmp(deviceName, "gl") == 0 || strcmp(deviceName, "opengl") == 0)
+		{
+			graphicsApi = rive::GraphicsApi::opengl;
+		}
+		else if (strcmp(deviceName, "mtl") == 0 ||
+		         strcmp(deviceName, "metal") == 0)
+		{
+			graphicsApi = rive::GraphicsApi::metal;
+		}
+		else if (strcmp(deviceName, "vk") == 0 ||
+		         strcmp(deviceName, "vulkan") == 0)
+		{
+			graphicsApi = rive::GraphicsApi::vulkan;
+		}
+		else if (strcmp(deviceName, "d3d11") == 0)
+		{
+			graphicsApi = rive::GraphicsApi::d3d11;
+		}
+		else if (strcmp(deviceName, "d3d12") == 0)
+		{
+			graphicsApi = rive::GraphicsApi::d3d12;
+		}
+	}
+
+	if (!glfwInit())
+	{
+		fprintf(stderr, "Failed to initialize glfw.\n");
+		return 1;
+	}
+
+	if (graphicsApi == rive::GraphicsApi::opengl)
+	{
+		glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
+		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
+		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
+		glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+		glfwWindowHint(GLFW_SAMPLES, 16);
+	}
+	else
+	{
+		glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+	}
+
+	GLFWwindow* window = glfwCreateWindow(640, 480, "Rive", nullptr, nullptr);
+	if (!window)
+	{
+		glfwTerminate();
+		fprintf(stderr, "Failed to create window.\n");
+		return -1;
+	}
+
+#ifdef __APPLE__
+	glfwMakeContextCurrent(window);
+	viewerNativeWindowHandle = static_cast<void*>(glfwGetCocoaWindow(window));
+#endif
+
+	// If no specific API requested, big the default one for the platform.
+	rive::LowLevelRenderer* renderer =
+	    graphicsApi == rive::GraphicsApi::unknown
+	        ? rive::GraphicsApi::makeRenderer()
+	        : rive::GraphicsApi::makeRenderer(graphicsApi);
+
+	if (renderer == nullptr || !renderer->initialize())
+	{
+		return 1;
+	}
+
+	char windowTitle[128];
+	snprintf(windowTitle,
+	         128,
+	         "Rive Low Level Renderer (%s)",
+	         rive::GraphicsApi::name(renderer->type()));
+	glfwSetWindowTitle(window, windowTitle);
+
+	// Load a rive file
+	uint8_t* fileBytes = nullptr;
+	unsigned int fileBytesLength = 0;
+
+	// std::string filename = "assets/polygon_party.riv";
+	// std::string filename = "assets/triangle.riv";
+	// std::string filename = "assets/juice.riv";
+	// std::string filename = "assets/clip.riv";
+	// std::string filename = "assets/clipped_circle_star_2.riv";
+	// std::string filename = "assets/marty.riv";
+	std::string filename = "assets/runner.riv";
+	// std::string filename = "assets/rotate_square.riv";
+	// std::string filename = "assets/bone_deform.riv";
+	// std::string filename = "assets/off_road_car.riv";
+	// std::string filename = "assets/simple_stroke.riv";
+	// std::string filename = "assets/leg_issues.riv";
+	// std::string filename = "assets/control.riv";
+	// std::string filename = "assets/stroke_caps.riv";
+	FILE* fp = fopen(filename.c_str(), "r");
+	fseek(fp, 0, SEEK_END);
+	fileBytesLength = ftell(fp);
+	fseek(fp, 0, SEEK_SET);
+	delete[] fileBytes;
+	fileBytes = new uint8_t[fileBytesLength];
+	if (fread(fileBytes, 1, fileBytesLength, fp) != fileBytesLength)
+	{
+		delete[] fileBytes;
+		fprintf(stderr, "failed to read all of %s\n", filename.c_str());
+		return 1;
+	}
+	auto reader = rive::BinaryReader(fileBytes, fileBytesLength);
+	rive::File* file = nullptr;
+	auto result = rive::File::import(reader, &file);
+	if (result != rive::ImportResult::success)
+	{
+		delete[] fileBytes;
+		fprintf(stderr, "failed to import file\n");
+		return 1;
+	}
+
+	rive::Artboard* artboard = file->artboard();
+	artboard->advance(0.0f);
+
+	rive::LinearAnimationInstance* animationInstance = nullptr;
+	int animationIndex = 0;
+	rive::LinearAnimation* animation = // nullptr;
+	    animationIndex >= 0 && animationIndex < artboard->animationCount()
+	        ? artboard->animation(animationIndex)
+	        : nullptr;
+	if (animation != nullptr)
+	{
+		animationInstance = new rive::LinearAnimationInstance(animation);
+	}
+
+	double lastTime = glfwGetTime();
+	int lastWidth = 0, lastHeight = 0;
+	float projection[16] = {0.0f};
+	while (!glfwWindowShouldClose(window))
+	{
+		int width = 0, height = 0;
+		glfwGetFramebufferSize(window, &width, &height);
+
+		if (lastWidth != width || lastHeight != height)
+		{
+			lastWidth = width;
+			lastHeight = height;
+			renderer->orthographicProjection(
+			    projection, 0.0f, width, height, 0.0f, 0.0f, 1.0f);
+			renderer->modelViewProjection(projection);
+		}
+
+		double time = glfwGetTime();
+		float elapsed = (float)(time - lastTime);
+		lastTime = time;
+
+		renderer->startFrame();
+
+		if (artboard != nullptr)
+		{
+			if (animationInstance != nullptr)
+			{
+				animationInstance->advance(elapsed * 0.25f);
+				animationInstance->apply(artboard);
+			}
+			artboard->advance(elapsed);
+			renderer->save();
+			renderer->align(rive::Fit::contain,
+			                rive::Alignment::center,
+			                rive::AABB(0, 0, width, height),
+			                artboard->bounds());
+			artboard->draw(renderer);
+			renderer->restore();
+		}
+
+		renderer->endFrame();
+
+#if __APPLE__
+		glfwSwapBuffers(window);
+#endif
+		glfwPollEvents();
+	}
+
+	glfwTerminate();
+	return 0;
+}
\ No newline at end of file
diff --git a/skia/renderer/include/skia_renderer.hpp b/skia/renderer/include/skia_renderer.hpp
index 27bdf86..002d768 100644
--- a/skia/renderer/include/skia_renderer.hpp
+++ b/skia/renderer/include/skia_renderer.hpp
@@ -89,6 +89,7 @@
 		void radialGradient(float sx, float sy, float ex, float ey) override;
 		void addStop(unsigned int color, float stop) override;
 		void completeGradient() override;
+		void invalidateStroke() override {}
 	};
 
 	class SkiaRenderer : public Renderer
diff --git a/src/artboard.cpp b/src/artboard.cpp
index 8840a03..340dc7a 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -512,11 +512,12 @@
 	return m_StateMachines[index];
 }
 
-Artboard* Artboard::instance() const
+Artboard* Artboard::instance(Artboard* instanceObject) const
 {
-	auto artboardClone = clone()->as<Artboard>();
+	// Must be a fresh instance.
+	assert(instanceObject->m_Objects.empty());
 
-	artboardClone->m_Objects.push_back(artboardClone);
+	instanceObject->m_Objects.push_back(instanceObject);
 
 	// Skip first object (artboard).
 	auto itr = m_Objects.begin();
@@ -524,26 +525,32 @@
 	{
 		auto object = *itr;
 
-		artboardClone->m_Objects.push_back(object == nullptr ? nullptr
-		                                                     : object->clone());
+		instanceObject->m_Objects.push_back(
+		    object == nullptr ? nullptr : object->clone());
 	}
 
 	for (auto animation : m_Animations)
 	{
-		artboardClone->m_Animations.push_back(animation);
+		instanceObject->m_Animations.push_back(animation);
 	}
 	for (auto stateMachine : m_StateMachines)
 	{
-		artboardClone->m_StateMachines.push_back(stateMachine);
+		instanceObject->m_StateMachines.push_back(stateMachine);
 	}
 
-	if (artboardClone->initialize() != StatusCode::Ok)
+	if (instanceObject->initialize() != StatusCode::Ok)
 	{
-		delete artboardClone;
-		artboardClone = nullptr;
+		delete instanceObject;
+		instanceObject = nullptr;
 	}
 
-	artboardClone->m_IsInstance = true;
+	instanceObject->m_IsInstance = true;
 
-	return artboardClone;
+	return instanceObject;
+}
+
+Artboard* Artboard::instance() const
+{
+	auto artboardClone = clone()->as<Artboard>();
+	return instance(artboardClone);
 }
\ No newline at end of file
diff --git a/src/contour_render_path.cpp b/src/contour_render_path.cpp
new file mode 100644
index 0000000..568c826
--- /dev/null
+++ b/src/contour_render_path.cpp
@@ -0,0 +1,93 @@
+#ifdef LOW_LEVEL_RENDERING
+#include "rive/contour_render_path.hpp"
+#include "rive/contour_stroke.hpp"
+
+using namespace rive;
+
+PathCommand::PathCommand(PathCommandType type) : m_Type(type) {}
+PathCommand::PathCommand(PathCommandType type, float x, float y) :
+    m_Type(type), m_Point(x, y)
+{
+}
+PathCommand::PathCommand(PathCommandType type,
+                         float outX,
+                         float outY,
+                         float inX,
+                         float inY,
+                         float x,
+                         float y) :
+    m_Type(type), m_OutPoint(outX, outY), m_InPoint(inX, inY), m_Point(x, y)
+{
+}
+
+ContourSubPath::ContourSubPath(RenderPath* path, const Mat2D& transform) :
+    m_Path(path), m_Transform(transform)
+{
+}
+
+RenderPath* ContourSubPath::path() const { return m_Path; }
+const Mat2D& ContourSubPath::transform() const { return m_Transform; }
+
+void ContourRenderPath::addRenderPath(RenderPath* path, const Mat2D& transform)
+{
+	m_SubPaths.emplace_back(ContourSubPath(path, transform));
+}
+
+void ContourRenderPath::reset()
+{
+	m_IsClosed = false;
+	m_SubPaths.clear();
+	m_ContourVertices.clear();
+	m_Commands.clear();
+	m_IsDirty = true;
+}
+
+void ContourRenderPath::moveTo(float x, float y)
+{
+	m_Commands.emplace_back(PathCommand(PathCommandType::move, x, y));
+}
+
+void ContourRenderPath::lineTo(float x, float y)
+{
+	m_Commands.emplace_back(PathCommand(PathCommandType::line, x, y));
+}
+
+void ContourRenderPath::cubicTo(
+    float ox, float oy, float ix, float iy, float x, float y)
+{
+	m_Commands.emplace_back(
+	    PathCommand(PathCommandType::cubic, ox, oy, ix, iy, x, y));
+}
+void ContourRenderPath::close()
+{
+	m_Commands.emplace_back(PathCommand(PathCommandType::close));
+	m_IsClosed = true;
+}
+
+bool ContourRenderPath::isContainer() const { return !m_SubPaths.empty(); }
+
+void ContourRenderPath::extrudeStroke(ContourStroke* stroke,
+                                      StrokeJoin join,
+                                      StrokeCap cap,
+                                      float strokeWidth,
+                                      const Mat2D& transform)
+{
+	if (isContainer())
+	{
+		for (auto& subPath : m_SubPaths)
+		{
+			static_cast<ContourRenderPath*>(subPath.path())
+			    ->extrudeStroke(
+			        stroke, join, cap, strokeWidth, subPath.transform());
+		}
+		return;
+	}
+
+	if (isDirty())
+	{
+		computeContour();
+	}
+
+	stroke->extrude(this, m_IsClosed, join, cap, strokeWidth, transform);
+}
+#endif
\ No newline at end of file
diff --git a/src/contour_render_path_recursive.cpp b/src/contour_render_path_recursive.cpp
new file mode 100644
index 0000000..ce11b6b
--- /dev/null
+++ b/src/contour_render_path_recursive.cpp
@@ -0,0 +1,181 @@
+#if defined(LOW_LEVEL_RENDERING) && defined(CONTOUR_RECURSIVE)
+
+#include "rive/contour_render_path.hpp"
+#include "rive/math/cubic_utilities.hpp"
+#include <cassert>
+
+using namespace rive;
+
+// TODO when we add strokes, add ranges in the contour that need to be stroked
+// as contiguous lines.
+
+// struct StrokeRange
+// {
+// 	unsigned int start;
+// 	unsigned int end;
+// };
+
+class RecursiveCubicSegmenter
+{
+private:
+	Vec2D m_Pen, m_PenDown;
+	bool m_IsPenDown = false;
+	std::vector<Vec2D>* m_Contour;
+	// std::vector<StrokeRange> m_StrokeRanges;
+
+	AABB m_Bounds;
+	float m_Threshold, m_ThresholdSquared;
+
+public:
+	RecursiveCubicSegmenter(std::vector<Vec2D>* contour, float threshold) :
+	    m_Contour(contour),
+	    m_Bounds(AABB::forExpansion()),
+	    m_Threshold(threshold),
+	    m_ThresholdSquared(threshold * threshold)
+	{
+	}
+
+	const Vec2D& pen() { return m_Pen; }
+	bool isPenDown() { return m_IsPenDown; }
+
+	void addVertex(const Vec2D& vertex)
+	{
+		m_Contour->emplace_back(vertex);
+		AABB::expandTo(m_Bounds, vertex);
+	}
+
+	const AABB& bounds() const { return m_Bounds; }
+
+	inline void penUp()
+	{
+		if (!m_IsPenDown)
+		{
+			return;
+		}
+		m_IsPenDown = false;
+	}
+
+	inline void penDown()
+	{
+		if (m_IsPenDown)
+		{
+			return;
+		}
+		m_IsPenDown = true;
+		Vec2D::copy(m_PenDown, m_Pen);
+		addVertex(m_PenDown);
+	}
+
+	inline void close()
+	{
+		if (!m_IsPenDown)
+		{
+			return;
+		}
+		Vec2D::copy(m_Pen, m_PenDown);
+		m_IsPenDown = false;
+
+		// TODO: Can we optimize and not dupe this point if it's the last point
+		// already in the list? For example: a procedural triangle closes itself
+		// with a lineTo the first point.
+		addVertex(m_PenDown);
+	}
+
+	inline void pen(const Vec2D& position) { Vec2D::copy(m_Pen, position); }
+
+	void segmentCubic(const Vec2D& from,
+	                  const Vec2D& fromOut,
+	                  const Vec2D& toIn,
+	                  const Vec2D& to,
+	                  float t1,
+	                  float t2)
+	{
+		if (CubicUtilities::shouldSplitCubic(
+		        from, fromOut, toIn, to, m_Threshold))
+		{
+			float halfT = (t1 + t2) / 2.0f;
+
+			Vec2D hull[6];
+			CubicUtilities::computeHull(from, fromOut, toIn, to, 0.5f, hull);
+
+			segmentCubic(from, hull[0], hull[3], hull[5], t1, halfT);
+
+			segmentCubic(hull[5], hull[4], hull[2], to, halfT, t2);
+		}
+		else
+		{
+			if (Vec2D::distanceSquared(from, to) > m_ThresholdSquared)
+			{
+				addVertex(Vec2D(CubicUtilities::cubicAt(
+				                    t2, from[0], fromOut[0], toIn[0], to[0]),
+				                CubicUtilities::cubicAt(
+				                    t2, from[1], fromOut[1], toIn[1], to[1])));
+			}
+		}
+	}
+};
+
+void ContourRenderPath::computeContour()
+{
+	m_IsDirty = false;
+	assert(m_ContourVertices.empty());
+	RecursiveCubicSegmenter segmenter(&m_ContourVertices, m_ContourThreshold);
+
+	// First four vertices are the bounds.
+	m_ContourVertices.emplace_back(Vec2D());
+	m_ContourVertices.emplace_back(Vec2D());
+	m_ContourVertices.emplace_back(Vec2D());
+	m_ContourVertices.emplace_back(Vec2D());
+
+	for (rive::PathCommand& command : m_Commands)
+	{
+		switch (command.type())
+		{
+			case PathCommandType::move:
+				segmenter.penUp();
+				segmenter.pen(command.point());
+				break;
+			case PathCommandType::line:
+				segmenter.penDown();
+				segmenter.pen(command.point());
+				segmenter.addVertex(command.point());
+				break;
+			case PathCommandType::cubic:
+				segmenter.penDown();
+				segmenter.segmentCubic(segmenter.pen(),
+				                       command.outPoint(),
+				                       command.inPoint(),
+				                       command.point(),
+				                       0.0f,
+				                       1.0f);
+				// segmenter.addVertex(command.point());
+				segmenter.pen(command.point());
+				break;
+			case PathCommandType::close:
+				segmenter.close();
+				break;
+		}
+	}
+	// TODO: when we stroke we may want to differentiate whether or not the path
+	// actually closed.
+	segmenter.close();
+
+	// TODO: consider if there's a case with no points.
+	AABB::copy(m_ContourBounds, segmenter.bounds());
+	Vec2D& first = m_ContourVertices[0];
+	first[0] = m_ContourBounds.minX;
+	first[1] = m_ContourBounds.minY;
+
+	Vec2D& second = m_ContourVertices[1];
+	second[0] = m_ContourBounds.maxX;
+	second[1] = m_ContourBounds.minY;
+
+	Vec2D& third = m_ContourVertices[2];
+	third[0] = m_ContourBounds.maxX;
+	third[1] = m_ContourBounds.maxY;
+
+	Vec2D& fourth = m_ContourVertices[3];
+	fourth[0] = m_ContourBounds.minX;
+	fourth[1] = m_ContourBounds.maxY;
+}
+#endif
\ No newline at end of file
diff --git a/src/contour_stroke.cpp b/src/contour_stroke.cpp
new file mode 100644
index 0000000..161699c
--- /dev/null
+++ b/src/contour_stroke.cpp
@@ -0,0 +1,387 @@
+#ifdef LOW_LEVEL_RENDERING
+#include "rive/contour_stroke.hpp"
+#include "rive/contour_render_path.hpp"
+#include "rive/math/vec2d.hpp"
+
+using namespace rive;
+
+static const int subdivisionArcLength = 4.0f;
+
+void ContourStroke::reset()
+{
+	m_TriangleStrip.clear();
+	m_Offsets.clear();
+}
+
+void ContourStroke::resetRenderOffset() { m_RenderOffset = 0; }
+
+void ContourStroke::nextRenderOffset(std::size_t& start, std::size_t& end)
+{
+	assert(m_RenderOffset < m_Offsets.size());
+	start = m_RenderOffset == 0 ? 0 : m_Offsets[m_RenderOffset - 1];
+	end = m_Offsets[m_RenderOffset++];
+}
+
+void ContourStroke::extrude(const ContourRenderPath* renderPath,
+                            bool isClosed,
+                            StrokeJoin join,
+                            StrokeCap cap,
+                            float strokeWidth,
+                            const Mat2D& transform)
+{
+	// TODO: if transform is identity, no need to copy and transform
+	// contourPoints->points.
+
+	const std::vector<Vec2D>& contourPoints = renderPath->contourVertices();
+	std::vector<Vec2D> points(contourPoints);
+	auto pointCount = points.size();
+	if (pointCount < 6)
+	{
+		return;
+	}
+	for (int i = 4; i < pointCount; i++)
+	{
+		Vec2D& point = points[i];
+		Vec2D::transform(point, point, transform);
+	}
+	auto startOffset = m_TriangleStrip.size();
+	Vec2D lastPoint = points[4];
+	Vec2D lastDiff;
+	Vec2D::subtract(lastDiff, points[5], lastPoint);
+	float lastLength = Vec2D::length(lastDiff);
+	Vec2D lastDiffNormalized;
+	Vec2D::scale(lastDiffNormalized, lastDiff, 1.0f / lastLength);
+
+	Vec2D lastA, lastB;
+	Vec2D perpendicularStrokeDiff = Vec2D(lastDiffNormalized[1] * -strokeWidth,
+	                                      lastDiffNormalized[0] * strokeWidth);
+	Vec2D::add(lastA, lastPoint, perpendicularStrokeDiff);
+	Vec2D::subtract(lastB, lastPoint, perpendicularStrokeDiff);
+
+	if (!isClosed)
+	{
+		switch (cap)
+		{
+			case StrokeCap::square:
+			{
+				Vec2D squareA, squareB;
+				Vec2D strokeDiff = Vec2D(lastDiffNormalized[0] * strokeWidth,
+				                         lastDiffNormalized[1] * strokeWidth);
+				Vec2D::subtract(squareA, lastA, strokeDiff);
+				Vec2D::subtract(squareB, lastB, strokeDiff);
+				m_TriangleStrip.push_back(squareA);
+				m_TriangleStrip.push_back(squareB);
+				break;
+			}
+			case StrokeCap::round:
+			{
+				Vec2D capDirection =
+				    Vec2D(-lastDiffNormalized[1], lastDiffNormalized[0]);
+				float arcLength = std::abs(M_PI * strokeWidth);
+				int steps = (int)std::ceil(arcLength / subdivisionArcLength);
+				float angleTo = std::atan2(capDirection[1], capDirection[0]);
+				float inc = M_PI / steps;
+				float angle = angleTo;
+				// make sure to draw the full cap due triangle strip
+				for (int j = 0; j <= steps; j++)
+				{
+					m_TriangleStrip.push_back(lastPoint);
+					m_TriangleStrip.push_back(
+					    Vec2D(lastPoint[0] + std::cos(angle) * strokeWidth,
+					          lastPoint[1] + std::sin(angle) * strokeWidth));
+					angle += inc;
+				}
+				break;
+			}
+			default:
+				break;
+		}
+	}
+	m_TriangleStrip.push_back(lastA);
+	m_TriangleStrip.push_back(lastB);
+
+	pointCount -= isClosed ? 6 : 5;
+	std::size_t adjustedPointCount = isClosed ? pointCount + 1 : pointCount;
+
+	for (std::size_t i = 1; i < adjustedPointCount; i++)
+	{
+		const Vec2D& point = points[(i % pointCount) + 4];
+		Vec2D diff, diffNormalized, next;
+		float length;
+		if (i < adjustedPointCount - 1 || isClosed)
+		{
+			Vec2D::subtract(
+			    diff, (next = points[((i + 1) % pointCount) + 4]), point);
+			length = Vec2D::length(diff);
+			Vec2D::scale(diffNormalized, diff, 1.0f / length);
+		}
+		else
+		{
+			diff = lastDiff;
+			next = point;
+			length = lastLength;
+			diffNormalized = lastDiffNormalized;
+		}
+
+		// perpendicular dx
+		float pdx0 = -lastDiffNormalized[1];
+		float pdy0 = lastDiffNormalized[0];
+		float pdx1 = -diffNormalized[1];
+		float pdy1 = diffNormalized[0];
+
+		// Compute bisector without a normalization by averaging perpendicular
+		// diffs.
+		Vec2D bisector((pdx0 + pdx1) * 0.5f, (pdy0 + pdy1) * 0.5f);
+		float cross = diff[0] * lastDiff[1] - lastDiff[0] * diff[1];
+
+		float dot = Vec2D::dot(bisector, bisector);
+
+		float lengthLimit = std::min(length, lastLength);
+		bool bevelInner = false;
+		bool bevel = join == StrokeJoin::miter ? dot < 0.1f : dot < 0.999f;
+
+		// Scale bisector to match stroke size.
+		if (dot > 0.000001f)
+		{
+			float scale = 1.0f / dot * strokeWidth;
+			float limit = lengthLimit / strokeWidth;
+			if (dot * limit * limit < 1.0f)
+			{
+				bevelInner = true;
+			}
+			bisector[0] *= scale;
+			bisector[1] *= scale;
+		}
+		else
+		{
+			bisector[0] *= strokeWidth;
+			bisector[1] *= strokeWidth;
+		}
+
+		if (!bevel)
+		{
+			Vec2D c, d;
+			Vec2D::add(c, point, bisector);
+			Vec2D::subtract(d, point, bisector);
+
+			if (!bevelInner)
+			{
+				// Normal mitered edge.
+				m_TriangleStrip.push_back(c);
+				m_TriangleStrip.push_back(d);
+			}
+			else if (cross <= 0)
+			{
+				// Overlap the inner (in this case right) edge (sometimes called
+				// miter inner).
+				Vec2D c1, c2;
+				Vec2D::add(c1,
+				           point,
+				           Vec2D(lastDiffNormalized[1] * -strokeWidth,
+				                 lastDiffNormalized[0] * strokeWidth));
+				Vec2D::add(c2,
+				           point,
+				           Vec2D(diffNormalized[1] * -strokeWidth,
+				                 diffNormalized[0] * strokeWidth));
+
+				m_TriangleStrip.push_back(c1);
+				m_TriangleStrip.push_back(d);
+				m_TriangleStrip.push_back(c2);
+				m_TriangleStrip.push_back(d);
+			}
+			else
+			{
+				// Overlap the inner (in this case left) edge (sometimes called
+				// miter inner).
+				Vec2D d1, d2;
+				Vec2D::subtract(d1,
+				                point,
+				                Vec2D(lastDiffNormalized[1] * -strokeWidth,
+				                      lastDiffNormalized[0] * strokeWidth));
+				Vec2D::subtract(d2,
+				                point,
+				                Vec2D(diffNormalized[1] * -strokeWidth,
+				                      diffNormalized[0] * strokeWidth));
+
+				m_TriangleStrip.push_back(c);
+				m_TriangleStrip.push_back(d1);
+				m_TriangleStrip.push_back(c);
+				m_TriangleStrip.push_back(d2);
+			}
+		}
+		else
+		{
+			Vec2D ldPStroke = Vec2D(lastDiffNormalized[1] * -strokeWidth,
+			                        lastDiffNormalized[0] * strokeWidth);
+			Vec2D dPStroke = Vec2D(diffNormalized[1] * -strokeWidth,
+			                       diffNormalized[0] * strokeWidth);
+			if (cross <= 0)
+			{
+				// Bevel the outer (left in this case) edge.
+				Vec2D a1;
+				Vec2D a2;
+
+				if (bevelInner)
+				{
+					Vec2D::add(a1, point, ldPStroke);
+					Vec2D::add(a2, point, dPStroke);
+				}
+				else
+				{
+					Vec2D::add(a1, point, bisector);
+					a2 = a1;
+				}
+
+				Vec2D b;
+				Vec2D::subtract(b, point, ldPStroke);
+				Vec2D bn;
+				Vec2D::subtract(bn, point, dPStroke);
+
+				m_TriangleStrip.push_back(a1);
+				m_TriangleStrip.push_back(b);
+				if (join == StrokeJoin::round)
+				{
+					const Vec2D& pivot = bevelInner ? point : a1;
+					Vec2D toPrev;
+					Vec2D::subtract(toPrev, bn, point);
+					Vec2D toNext;
+					Vec2D::subtract(toNext, b, point);
+					float angleFrom = std::atan2(toPrev[1], toPrev[0]);
+					float angleTo = std::atan2(toNext[1], toNext[0]);
+					if (angleTo > angleFrom)
+					{
+						angleTo -= M_2_PI;
+					}
+					float range = angleTo - angleFrom;
+					float arcLength = std::abs(range * strokeWidth);
+					int steps = std::ceil(arcLength / subdivisionArcLength);
+
+					float inc = range / steps;
+					float angle = angleTo - inc;
+					for (int j = 0; j < steps - 1; j++)
+					{
+						m_TriangleStrip.push_back(pivot);
+						m_TriangleStrip.emplace_back(
+						    Vec2D(point[0] + std::cos(angle) * strokeWidth,
+						          point[1] + std::sin(angle) * strokeWidth));
+
+						angle -= inc;
+					}
+				}
+				m_TriangleStrip.push_back(a2);
+				m_TriangleStrip.push_back(bn);
+			}
+			else
+			{
+				// Bevel the outer (right in this case) edge.
+				Vec2D b1;
+				Vec2D b2;
+				if (bevelInner)
+				{
+					Vec2D::subtract(b1, point, ldPStroke);
+					Vec2D::subtract(b2, point, dPStroke);
+				}
+				else
+				{
+					Vec2D::subtract(b1, point, bisector);
+					b2 = b1;
+				}
+
+				Vec2D a;
+				Vec2D::add(a, point, ldPStroke);
+				Vec2D an;
+
+				Vec2D::add(an, point, dPStroke);
+
+				m_TriangleStrip.push_back(a);
+				m_TriangleStrip.push_back(b1);
+
+				if (join == StrokeJoin::round)
+				{
+					const Vec2D& pivot = bevelInner ? point : b1;
+					Vec2D toPrev;
+					Vec2D::subtract(toPrev, a, point);
+					Vec2D toNext;
+					Vec2D::subtract(toNext, an, point);
+					float angleFrom = std::atan2(toPrev[1], toPrev[0]);
+					float angleTo = std::atan2(toNext[1], toNext[0]);
+					if (angleTo > angleFrom)
+					{
+						angleTo -= M_2_PI;
+					}
+
+					float range = angleTo - angleFrom;
+					float arcLength = std::abs(range * strokeWidth);
+					int steps = std::ceil(arcLength / subdivisionArcLength);
+					float inc = range / steps;
+
+					float angle = angleFrom + inc;
+					for (int j = 0; j < steps - 1; j++)
+					{
+						m_TriangleStrip.emplace_back(
+						    Vec2D(point[0] + std::cos(angle) * strokeWidth,
+						          point[1] + std::sin(angle) * strokeWidth));
+						m_TriangleStrip.push_back(pivot);
+						angle += inc;
+					}
+				}
+				m_TriangleStrip.push_back(an);
+				m_TriangleStrip.push_back(b2);
+			}
+		}
+
+		lastPoint = point;
+		lastDiff = diff;
+		lastDiffNormalized = diffNormalized;
+	}
+
+	if (isClosed)
+	{
+		auto last = m_TriangleStrip.size() - 1;
+		m_TriangleStrip[startOffset] = m_TriangleStrip[last - 1];
+		m_TriangleStrip[startOffset + 1] = m_TriangleStrip[last];
+	}
+	else
+	{
+		switch (cap)
+		{
+			case StrokeCap::square:
+			{
+				auto l = m_TriangleStrip.size();
+				Vec2D squareA, squareB;
+				Vec2D strokeDiff = Vec2D(lastDiffNormalized[0] * strokeWidth,
+				                         lastDiffNormalized[1] * strokeWidth);
+				Vec2D::add(squareA, m_TriangleStrip[l - 2], strokeDiff);
+				Vec2D::add(squareB, m_TriangleStrip[l - 1], strokeDiff);
+				m_TriangleStrip.push_back(squareA);
+				m_TriangleStrip.push_back(squareB);
+				break;
+			}
+			case StrokeCap::round:
+			{
+				Vec2D capDirection =
+				    Vec2D(-lastDiffNormalized[1], lastDiffNormalized[0]);
+				float arcLength = std::abs(M_PI * strokeWidth);
+				int steps = (int)std::ceil(arcLength / subdivisionArcLength);
+				float angleTo = std::atan2(capDirection[1], capDirection[0]);
+				float inc = M_PI / steps;
+				float angle = angleTo;
+				// make sure to draw the full cap due triangle strip
+				for (int j = 0; j <= steps; j++)
+				{
+					m_TriangleStrip.push_back(lastPoint);
+					m_TriangleStrip.push_back(
+					    Vec2D(lastPoint[0] + std::cos(angle) * strokeWidth,
+					          lastPoint[1] + std::sin(angle) * strokeWidth));
+					angle -= inc;
+				}
+				break;
+			}
+			default:
+				break;
+		}
+	}
+
+	m_Offsets.push_back(m_TriangleStrip.size());
+}
+#endif
diff --git a/src/math/aabb.cpp b/src/math/aabb.cpp
index d480a61..ab06c4e 100644
--- a/src/math/aabb.cpp
+++ b/src/math/aabb.cpp
@@ -105,3 +105,38 @@
 	out[2] = std::fmax(p1[0], std::fmax(p2[0], std::fmax(p3[0], p4[0])));
 	out[3] = std::fmax(p1[1], std::fmax(p2[1], std::fmax(p3[1], p4[1])));
 }
+
+void AABB::expandTo(AABB& out, const Vec2D& point)
+{
+	float x = point[0];
+	float y = point[1];
+	expandTo(out, x, y);
+}
+
+void AABB::expandTo(AABB& out, float x, float y)
+{
+	if (x < out.minX)
+	{
+		out.minX = x;
+	}
+	if (x > out.maxX)
+	{
+		out.maxX = x;
+	}
+	if (y < out.minY)
+	{
+		out.minY = y;
+	}
+	if (y > out.maxY)
+	{
+		out.maxY = y;
+	}
+}
+
+void AABB::copy(AABB& out, const AABB& a)
+{
+	out[0] = a[0];
+	out[1] = a[1];
+	out[2] = a[2];
+	out[3] = a[3];
+}
\ No newline at end of file
diff --git a/src/math/mat2d.cpp b/src/math/mat2d.cpp
index 410f1fc..2db040b 100644
--- a/src/math/mat2d.cpp
+++ b/src/math/mat2d.cpp
@@ -132,4 +132,7 @@
 	result[1] *= sx;
 	result[2] *= sy;
 	result[3] *= sy;
-}
\ No newline at end of file
+}
+
+static Mat2D s_Transform;
+const Mat2D& Mat2D::identity() { return s_Transform; }
\ No newline at end of file
diff --git a/src/shapes/metrics_path.cpp b/src/shapes/metrics_path.cpp
index a16f89e..91008e6 100644
--- a/src/shapes/metrics_path.cpp
+++ b/src/shapes/metrics_path.cpp
@@ -1,5 +1,6 @@
 #include "rive/shapes/metrics_path.hpp"
 #include "rive/renderer.hpp"
+#include "rive/math/cubic_utilities.hpp"
 #include <math.h>
 
 using namespace rive;
@@ -57,42 +58,9 @@
 
 void MetricsPath::close() {}
 
-static void computeHull(const Vec2D& from,
-                        const Vec2D& fromOut,
-                        const Vec2D& toIn,
-                        const Vec2D& to,
-                        float t,
-                        Vec2D* hull)
-{
-	Vec2D::lerp(hull[0], from, fromOut, t);
-	Vec2D::lerp(hull[1], fromOut, toIn, t);
-	Vec2D::lerp(hull[2], toIn, to, t);
-
-	Vec2D::lerp(hull[3], hull[0], hull[1], t);
-	Vec2D::lerp(hull[4], hull[1], hull[2], t);
-
-	Vec2D::lerp(hull[5], hull[3], hull[4], t);
-}
-
 static const float minSegmentLength = 0.05f;
 static const float distTooFar = 1.0f;
 
-static bool tooFar(const Vec2D& a, const Vec2D& b)
-{
-	return std::max(std::abs(a[0] - b[0]), std::abs(a[1] - b[1])) > distTooFar;
-}
-
-static bool shouldSplitCubic(const Vec2D& from,
-                             const Vec2D& fromOut,
-                             const Vec2D& toIn,
-                             const Vec2D& to)
-{
-	Vec2D oneThird, twoThird;
-	Vec2D::lerp(oneThird, from, to, 1.0f / 3.0f);
-	Vec2D::lerp(twoThird, from, to, 2.0f / 3.0f);
-	return tooFar(fromOut, oneThird) || tooFar(toIn, twoThird);
-}
-
 static float segmentCubic(const Vec2D& from,
                           const Vec2D& fromOut,
                           const Vec2D& toIn,
@@ -103,12 +71,12 @@
                           std::vector<CubicSegment>& segments)
 {
 
-	if (shouldSplitCubic(from, fromOut, toIn, to))
+	if (CubicUtilities::shouldSplitCubic(from, fromOut, toIn, to, distTooFar))
 	{
 		float halfT = (t1 + t2) / 2.0f;
 
 		Vec2D hull[6];
-		computeHull(from, fromOut, toIn, to, 0.5f, hull);
+		CubicUtilities::computeHull(from, fromOut, toIn, to, 0.5f, hull);
 
 		runningLength = segmentCubic(from,
 		                             hull[0],
@@ -404,7 +372,8 @@
 			if (startT == 0.0f)
 			{
 				// Start is 0, so split at end and keep the left side.
-				computeHull(from, fromOut, toIn, to, endT, hull);
+				CubicUtilities::computeHull(
+				    from, fromOut, toIn, to, endT, hull);
 				if (moveTo)
 				{
 					result->moveTo(from[0], from[1]);
@@ -419,7 +388,8 @@
 			else
 			{
 				// Split at start since it's non 0.
-				computeHull(from, fromOut, toIn, to, startT, hull);
+				CubicUtilities::computeHull(
+				    from, fromOut, toIn, to, startT, hull);
 				if (moveTo)
 				{
 					// Move to first point on the right side.
@@ -440,12 +410,13 @@
 				{
 					// End is not 1, so split again and cubic to the left side
 					// of the split and remap endT to the new curve range
-					computeHull(hull[5],
-					            hull[4],
-					            hull[2],
-					            to,
-					            (endT - startT) / (1.0f - startT),
-					            hull);
+					CubicUtilities::computeHull(hull[5],
+					                            hull[4],
+					                            hull[2],
+					                            to,
+					                            (endT - startT) /
+					                                (1.0f - startT),
+					                            hull);
 
 					result->cubicTo(hull[0][0],
 					                hull[0][1],
diff --git a/src/shapes/paint/stroke.cpp b/src/shapes/paint/stroke.cpp
index c8a8805..5cd1645 100644
--- a/src/shapes/paint/stroke.cpp
+++ b/src/shapes/paint/stroke.cpp
@@ -61,10 +61,17 @@
 
 void Stroke::addStrokeEffect(StrokeEffect* effect) { m_Effect = effect; }
 
-void Stroke::invalidateEffects()
+void Stroke::invalidate()
 {
 	if (m_Effect != nullptr)
 	{
 		m_Effect->invalidateEffect();
 	}
+	invalidateRendering();
+}
+
+void Stroke::invalidateRendering()
+{
+	assert(m_RenderPaint != nullptr);
+	m_RenderPaint->invalidateStroke();
 }
\ No newline at end of file
diff --git a/src/shapes/paint/trim_path.cpp b/src/shapes/paint/trim_path.cpp
index b62cd65..cc15a83 100644
--- a/src/shapes/paint/trim_path.cpp
+++ b/src/shapes/paint/trim_path.cpp
@@ -112,7 +112,10 @@
 void TrimPath::invalidateEffect()
 {
 	m_RenderPath = nullptr;
-	parent()->as<Stroke>()->parent()->addDirt(ComponentDirt::Paint);
+	Stroke* stroke = parent()->as<Stroke>();
+	stroke->parent()->addDirt(ComponentDirt::Paint);
+	// Drive this up so the rendering layer can invalidate too.
+	stroke->invalidateRendering();
 }
 
 void TrimPath::startChanged() { invalidateEffect(); }
diff --git a/src/shapes/path.cpp b/src/shapes/path.cpp
index e201c98..5a805ce 100644
--- a/src/shapes/path.cpp
+++ b/src/shapes/path.cpp
@@ -278,6 +278,8 @@
 	if (m_Shape != nullptr)
 	{
 		m_Shape->pathChanged();
+		// We invalidate stroke if the path points change.
+		m_Shape->invalidateStroke();
 	}
 }
 
@@ -287,6 +289,11 @@
 	{
 		m_Shape->pathChanged();
 	}
+	if (hasDirt(value, ComponentDirt::Transform) && m_Shape != nullptr)
+	{
+		// We invalidate stroke if a local path transform changes.
+		m_Shape->invalidateStroke();
+	}
 }
 
 void Path::update(ComponentDirt value)
@@ -298,13 +305,6 @@
 	{
 		buildPath(*m_CommandPath, isPathClosed(), m_Vertices);
 	}
-	// if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr)
-	// {
-	// 	// Make sure the path composer has an opportunity to rebuild the path
-	// 	// (this is why the composer depends on the shape and all its paths,
-	// 	// ascertaning it updates after both)
-	// 	m_Shape->pathChanged();
-	// }
 }
 
 #ifdef ENABLE_QUERY_FLAT_VERTICES
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index f92ba15..ff959e4 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -29,11 +29,7 @@
 	}
 }
 
-void Shape::pathChanged()
-{
-	m_PathComposer.addDirt(ComponentDirt::Path, true);
-	invalidateStrokeEffects();
-}
+void Shape::pathChanged() { m_PathComposer.addDirt(ComponentDirt::Path, true); }
 
 void Shape::draw(Renderer* renderer)
 {
diff --git a/src/shapes/shape_paint_container.cpp b/src/shapes/shape_paint_container.cpp
index 514282a..a527cde 100644
--- a/src/shapes/shape_paint_container.cpp
+++ b/src/shapes/shape_paint_container.cpp
@@ -37,13 +37,13 @@
 	return space;
 }
 
-void ShapePaintContainer::invalidateStrokeEffects()
+void ShapePaintContainer::invalidateStroke()
 {
 	for (auto paint : m_ShapePaints)
 	{
 		if (paint->is<Stroke>())
 		{
-			paint->as<Stroke>()->invalidateEffects();
+			paint->as<Stroke>()->invalidate();
 		}
 	}
 }
diff --git a/test/no_op_renderer.hpp b/test/no_op_renderer.hpp
index f1716ac..07622cd 100644
--- a/test/no_op_renderer.hpp
+++ b/test/no_op_renderer.hpp
@@ -19,6 +19,7 @@
 		void radialGradient(float sx, float sy, float ex, float ey) override {}
 		void addStop(unsigned int color, float stop) override {}
 		void completeGradient() override {}
+		void invalidateStroke() override {}
 	};
 
 	enum class NoOpPathCommandType