Adding clipping support.
diff --git a/renderer/library/include/low_level/low_level_renderer.hpp b/renderer/library/include/low_level/low_level_renderer.hpp
index 6f5dbfb..9c377a4 100644
--- a/renderer/library/include/low_level/low_level_renderer.hpp
+++ b/renderer/library/include/low_level/low_level_renderer.hpp
@@ -6,36 +6,55 @@
 #include "math/mat2d.hpp"
 #include <stdint.h>
 #include <list>
+#include <vector>
 
 namespace rive
 {
-	struct SubPath
+	class SubPath
 	{
-		RenderPath* path;
-		Mat2D transform;
+	private:
+		RenderPath* m_Path;
+		Mat2D m_Transform;
+
+	public:
+		SubPath(RenderPath* path, const Mat2D& transform);
+
+		RenderPath* path();
+		const Mat2D& transform();
 	};
 
 	struct RenderState
 	{
 		Mat2D transform;
-		std::list<SubPath> clipPaths;
+		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() = 0;
+		virtual void startFrame();
 		virtual void endFrame() = 0;
 
 		virtual RenderPaint* makeRenderPaint() = 0;
@@ -57,6 +76,7 @@
 		void restore() override;
 		void transform(const Mat2D& transform) override;
 		const Mat2D& transform();
+		void clipPath(RenderPath* path) override;
 	};
 
 } // namespace rive
diff --git a/renderer/library/include/opengl/opengl_renderer.hpp b/renderer/library/include/opengl/opengl_renderer.hpp
index 149c0fc..cbb538c 100644
--- a/renderer/library/include/opengl/opengl_renderer.hpp
+++ b/renderer/library/include/opengl/opengl_renderer.hpp
@@ -25,6 +25,8 @@
 		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.
@@ -36,7 +38,6 @@
 		OpenGLRenderer();
 		~OpenGLRenderer();
 		void drawPath(RenderPath* path, RenderPaint* paint) override;
-		void clipPath(RenderPath* path) override;
 
 		void startFrame() override;
 		void endFrame() override;
diff --git a/renderer/library/src/low_level_renderer/low_level_renderer.cpp b/renderer/library/src/low_level_renderer/low_level_renderer.cpp
index fdfad07..d64bf05 100644
--- a/renderer/library/src/low_level_renderer/low_level_renderer.cpp
+++ b/renderer/library/src/low_level_renderer/low_level_renderer.cpp
@@ -1,8 +1,17 @@
 #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])
@@ -44,7 +53,13 @@
 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)
@@ -52,4 +67,50 @@
 	Mat2D& stackMat = m_Stack.back().transform;
 	Mat2D::multiply(stackMat, stackMat, transform);
 }
-const Mat2D& LowLevelRenderer::transform() { return m_Stack.back().transform; }
\ No newline at end of file
+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/opengl/opengl_renderer.cpp b/renderer/library/src/opengl/opengl_renderer.cpp
index 30f0e08..a722fc0 100644
--- a/renderer/library/src/opengl/opengl_renderer.cpp
+++ b/renderer/library/src/opengl/opengl_renderer.cpp
@@ -15,6 +15,7 @@
 	glDeleteShader(m_VertexShader);
 	glDeleteShader(m_FragmentShader);
 	glDeleteBuffers(1, &m_IndexBuffer);
+	glDeleteBuffers(1, &m_BlitBuffer);
 	glDeleteVertexArrays(1, &m_VertexArray);
 }
 
@@ -57,6 +58,25 @@
 	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);
@@ -103,29 +123,157 @@
 		return;
 	}
 
+	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);
 
 	// Set up stencil buffer.
-	glStencilMask(0xFF);
-	glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
-	glColorMask(false, false, false, false);
+	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);
 
 	glPath->stencil(this, transform());
 
 	glColorMask(true, true, true, true);
-	glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
+	glStencilFunc(GL_NOTEQUAL, 0, m_IsClipping ? 0x7F : 0xFF);
 	glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
 
 	glPaint->draw(this, transform(), glPath);
 	// glPath->cover(this, transform());
 }
 
-void OpenGLRenderer::clipPath(RenderPath* path) {}
-
 void OpenGLRenderer::startFrame()
 {
+	LowLevelRenderer::startFrame();
 	glUseProgram(m_Program);
 	glEnableVertexAttribArray(0);
 	glUniformMatrix4fv(
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/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/src/viewer.cpp b/renderer/viewer/src/viewer.cpp
index 86dca8d..829c986 100644
--- a/renderer/viewer/src/viewer.cpp
+++ b/renderer/viewer/src/viewer.cpp
@@ -107,7 +107,10 @@
 
 	// std::string filename = "assets/polygon_party.riv";
 	// std::string filename = "assets/triangle.riv";
-	std::string filename = "assets/juice.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";
 	FILE* fp = fopen(filename.c_str(), "r");
 	fseek(fp, 0, SEEK_END);
 	fileBytesLength = ftell(fp);