Improving stroking for closed paths.
diff --git a/include/contour_render_path.hpp b/include/contour_render_path.hpp
index 9911f99..ee0a35b 100644
--- a/include/contour_render_path.hpp
+++ b/include/contour_render_path.hpp
@@ -66,6 +66,7 @@
 		const Mat2D& transform() const;
 	};
 
+	class ContourStroke;
 	///
 	/// Segments curves into line segments and computes the bounds of the
 	/// segmented curve.
@@ -79,6 +80,7 @@
 		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(); }
@@ -86,6 +88,7 @@
 		{
 			return m_ContourVertices;
 		}
+		bool isClosed() const { return m_IsClosed; }
 
 		bool isContainer() const;
 		void addRenderPath(RenderPath* path, const Mat2D& transform) override;
@@ -99,6 +102,11 @@
 
 		void computeContour();
 		bool isDirty() const { return m_IsDirty; }
+
+		void extrudeStroke(ContourStroke* stroke,
+		                   StrokeJoin join,
+		                   StrokeCap cap,
+		                   float strokeWidth);
 	};
 } // namespace rive
 #endif
\ No newline at end of file
diff --git a/include/contour_stroke.hpp b/include/contour_stroke.hpp
index 118974a..65f30d6 100644
--- a/include/contour_stroke.hpp
+++ b/include/contour_stroke.hpp
@@ -17,8 +17,19 @@
 	{
 	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,
diff --git a/renderer/library/include/opengl/opengl_render_paint.hpp b/renderer/library/include/opengl/opengl_render_paint.hpp
index 1d017e4..bc2e3a1 100644
--- a/renderer/library/include/opengl/opengl_render_paint.hpp
+++ b/renderer/library/include/opengl/opengl_render_paint.hpp
@@ -3,12 +3,14 @@
 
 #include "renderer.hpp"
 #include <vector>
+#include "opengl/opengl.h"
 
 namespace rive
 {
 	class OpenGLRenderer;
 	class OpenGLRenderPaint;
 	class OpenGLRenderPath;
+	class ContourStroke;
 
 	class OpenGLGradient
 	{
@@ -34,6 +36,11 @@
 		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;
 
 	public:
 		void style(RenderPaintStyle style) override;
diff --git a/renderer/library/include/opengl/opengl_render_path.hpp b/renderer/library/include/opengl/opengl_render_path.hpp
index afd968f..fa4242c 100644
--- a/renderer/library/include/opengl/opengl_render_path.hpp
+++ b/renderer/library/include/opengl/opengl_render_path.hpp
@@ -24,6 +24,10 @@
 		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/src/opengl/opengl_render_paint.cpp b/renderer/library/src/opengl/opengl_render_paint.cpp
index f4d3260..32a2302 100644
--- a/renderer/library/src/opengl/opengl_render_paint.cpp
+++ b/renderer/library/src/opengl/opengl_render_paint.cpp
@@ -2,6 +2,7 @@
 #include "shapes/paint/color.hpp"
 #include "opengl/opengl_renderer.hpp"
 #include "opengl/opengl_render_path.hpp"
+#include "contour_stroke.hpp"
 
 using namespace rive;
 
@@ -13,18 +14,35 @@
 	buffer[3] = colorAlpha(value) / 255.0f;
 }
 
-void OpenGLRenderPaint::style(RenderPaintStyle style) { m_PaintStyle = style; }
+void OpenGLRenderPaint::style(RenderPaintStyle style)
+{
+	m_PaintStyle = style;
+	delete m_Stroke;
+	if (m_PaintStyle == RenderPaintStyle::stroke)
+	{
+		m_Stroke = new ContourStroke();
+		if (m_StrokeBuffer != 0)
+		{
+			glDeleteBuffers(1, &m_StrokeBuffer);
+		}
+		glGenBuffers(1, &m_StrokeBuffer);
+	}
+	else
+	{
+		m_Stroke = nullptr;
+	}
+}
 
 void OpenGLRenderPaint::color(unsigned int value)
 {
 	fillColorBuffer(m_Color, value);
 }
 
-void OpenGLRenderPaint::thickness(float value) {}
+void OpenGLRenderPaint::thickness(float value) { m_StrokeThickness = value; }
 
-void OpenGLRenderPaint::join(StrokeJoin value) {}
+void OpenGLRenderPaint::join(StrokeJoin value) { m_StrokeJoin = value; }
 
-void OpenGLRenderPaint::cap(StrokeCap value) {}
+void OpenGLRenderPaint::cap(StrokeCap value) { m_StrokeCap = value; }
 
 void OpenGLRenderPaint::blendMode(BlendMode value) {}
 
@@ -53,7 +71,15 @@
 
 void OpenGLRenderPaint::completeGradient() {}
 
-OpenGLRenderPaint::~OpenGLRenderPaint() { delete m_Gradient; }
+OpenGLRenderPaint::~OpenGLRenderPaint()
+{
+	if (m_StrokeBuffer != 0)
+	{
+		glDeleteBuffers(1, &m_StrokeBuffer);
+	}
+	delete m_Gradient;
+	delete m_Stroke;
+}
 
 bool OpenGLRenderPaint::doesDraw() const
 {
@@ -76,7 +102,35 @@
 	glUniform1i(renderer->fillTypeUniformIndex(), type);
 	glUniform4fv(renderer->colorUniformIndex(), 1, m_Color);
 
-	path->cover(renderer, transform);
+	if (m_Stroke != nullptr)
+	{
+		m_Stroke->reset();
+		path->extrudeStroke(
+		    m_Stroke, m_StrokeJoin, m_StrokeCap, m_StrokeThickness / 2.0f);
+
+		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) {}
diff --git a/renderer/library/src/opengl/opengl_render_path.cpp b/renderer/library/src/opengl/opengl_render_path.cpp
index 1c13c01..6e72e68 100644
--- a/renderer/library/src/opengl/opengl_render_path.cpp
+++ b/renderer/library/src/opengl/opengl_render_path.cpp
@@ -1,6 +1,7 @@
 #include "opengl/opengl_render_path.hpp"
 #include "opengl/opengl_renderer.hpp"
 #include "opengl/opengl.h"
+#include "contour_stroke.hpp"
 
 using namespace rive;
 
@@ -182,4 +183,71 @@
 
 	// 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)
+		{
+			const Mat2D& subPathTransform = subPath.transform();
+			Mat2D pathTransform;
+			Mat2D::multiply(pathTransform, transform, subPathTransform);
+			reinterpret_cast<OpenGLRenderPath*>(subPath.path())
+			    ->renderStroke(
+			        stroke, renderer, pathTransform, subPathTransform);
+		}
+		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
index a722fc0..83a89ca 100644
--- a/renderer/library/src/opengl/opengl_renderer.cpp
+++ b/renderer/library/src/opengl/opengl_renderer.cpp
@@ -118,10 +118,13 @@
 void OpenGLRenderer::drawPath(RenderPath* path, RenderPaint* paint)
 {
 	auto glPaint = static_cast<OpenGLRenderPaint*>(paint);
-	if (glPaint->style() == RenderPaintStyle::stroke || !glPaint->doesDraw())
+	// 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.
@@ -246,28 +249,49 @@
 
 	auto glPath = static_cast<OpenGLRenderPath*>(path);
 
-	// Set up stencil buffer.
-	if (m_IsClipping)
+	if (needsStencil)
 	{
-		glStencilMask(0x7F);
-		glStencilFunc(GL_EQUAL, 0x80, 0x80);
+		// 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);
+
+		glPath->stencil(this, transform());
+
+		glColorMask(true, true, true, true);
+		glStencilFunc(GL_NOTEQUAL, 0, m_IsClipping ? 0x7F : 0xFF);
+		glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
 	}
 	else
 	{
-		glStencilMask(0xFF);
-		glStencilFunc(GL_ALWAYS, 0x0, 0xFF);
+		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);
 	}
-
-	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, m_IsClipping ? 0x7F : 0xFF);
-	glStencilOp(GL_ZERO, GL_ZERO, GL_ZERO);
-
 	glPaint->draw(this, transform(), glPath);
+
 	// glPath->cover(this, transform());
 }
 
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/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/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/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/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/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/src/gl.cpp b/renderer/viewer/src/gl.cpp
index e2339d0..2919423 100644
--- a/renderer/viewer/src/gl.cpp
+++ b/renderer/viewer/src/gl.cpp
@@ -7,7 +7,7 @@
 	public:
 		void startFrame() override
 		{
-			glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
+			glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
 			glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
 			OpenGLRenderer::startFrame();
 		}
diff --git a/renderer/viewer/src/viewer.cpp b/renderer/viewer/src/viewer.cpp
index 829c986..f4804a5 100644
--- a/renderer/viewer/src/viewer.cpp
+++ b/renderer/viewer/src/viewer.cpp
@@ -110,7 +110,11 @@
 	// 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/marty.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";
 	FILE* fp = fopen(filename.c_str(), "r");
 	fseek(fp, 0, SEEK_END);
 	fileBytesLength = ftell(fp);
diff --git a/src/contour_render_path.cpp b/src/contour_render_path.cpp
index cfe7a01..7211492 100644
--- a/src/contour_render_path.cpp
+++ b/src/contour_render_path.cpp
@@ -1,5 +1,6 @@
 #ifdef LOW_LEVEL_RENDERING
 #include "contour_render_path.hpp"
+#include "contour_stroke.hpp"
 
 using namespace rive;
 
@@ -34,6 +35,7 @@
 
 void ContourRenderPath::reset()
 {
+	m_IsClosed = false;
 	m_SubPaths.clear();
 	m_ContourVertices.clear();
 	m_Commands.clear();
@@ -59,8 +61,31 @@
 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)
+{
+	if (isContainer())
+	{
+		for (auto& subPath : m_SubPaths)
+		{
+			static_cast<ContourRenderPath*>(subPath.path())
+			    ->extrudeStroke(stroke, join, cap, strokeWidth);
+		}
+		return;
+	}
+
+	if (isDirty())
+	{
+		computeContour();
+	}
+
+	stroke->extrude(this, m_IsClosed, join, cap, strokeWidth);
+}
 #endif
\ No newline at end of file
diff --git a/src/contour_stroke.cpp b/src/contour_stroke.cpp
index 2c0a4d8..a0e70eb 100644
--- a/src/contour_stroke.cpp
+++ b/src/contour_stroke.cpp
@@ -6,6 +6,22 @@
 using namespace rive;
 
 static const int subdivisionArcLength = 4.0f;
+bool first = true;
+
+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,
@@ -13,17 +29,16 @@
                             StrokeCap cap,
                             float strokeWidth)
 {
-	m_TriangleStrip.clear();
-
 	const std::vector<Vec2D>& points = renderPath->contourVertices();
 	auto pointCount = points.size();
-	if (pointCount < 2)
+	if (pointCount < 6)
 	{
 		return;
 	}
-	Vec2D lastPoint = points[0];
+	auto startOffset = m_TriangleStrip.size();
+	Vec2D lastPoint = points[4];
 	Vec2D lastDiff;
-	Vec2D::subtract(lastDiff, points[1], lastPoint);
+	Vec2D::subtract(lastDiff, points[5], lastPoint);
 	float lastLength = Vec2D::length(lastDiff);
 	Vec2D lastDiffNormalized;
 	Vec2D::scale(lastDiffNormalized, lastDiff, 1.0f / lastLength);
@@ -76,16 +91,18 @@
 	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];
+		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]), point);
+			Vec2D::subtract(
+			    diff, (next = points[((i + 1) % pointCount) + 4]), point);
 			length = Vec2D::length(diff);
 			Vec2D::scale(diffNormalized, diff, 1.0f / length);
 		}
@@ -258,6 +275,7 @@
 				else
 				{
 					Vec2D::subtract(b1, point, bisector);
+					b2 = b1;
 				}
 
 				Vec2D a;
@@ -307,5 +325,30 @@
 		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];
+	}
+
+	m_Offsets.push_back(m_TriangleStrip.size());
+
+	if (first)
+	{
+		printf("CLOSED: %i %i\n", isClosed, points.size() - 4);
+		for (auto pt : points)
+		{
+			printf("P: %f %f\n", pt[0], pt[1]);
+		}
+		printf("---\n");
+		first = false;
+
+		for (auto v : m_TriangleStrip)
+		{
+			printf("%f %f\n", v[0], v[1]);
+		}
+	}
 }
-#endif
\ No newline at end of file
+#endif