Getting GL rendering to work.
diff --git a/.gitignore b/.gitignore
index 7e9ac38..b1c96d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,4 @@
 /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/include/math/mat2d.hpp b/include/math/mat2d.hpp
index cf6db22..81920eb 100644
--- a/include/math/mat2d.hpp
+++ b/include/math/mat2d.hpp
@@ -2,6 +2,7 @@
 #define _RIVE_MAT2D_HPP_
 
 #include <cstddef>
+#include <stdio.h>
 
 namespace rive
 {
@@ -47,6 +48,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)
@@ -61,5 +69,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/renderer/library/include/low_level/low_level_renderer.hpp b/renderer/library/include/low_level/low_level_renderer.hpp
index 0d339de..6f5dbfb 100644
--- a/renderer/library/include/low_level/low_level_renderer.hpp
+++ b/renderer/library/include/low_level/low_level_renderer.hpp
@@ -27,7 +27,7 @@
 	/// OpenGL, Vulkan, D3D, etc.
 	class LowLevelRenderer : public Renderer
 	{
-	private:
+	protected:
 		float m_ModelViewProjection[16] = {0.0f};
 		std::list<RenderState> m_Stack;
 
diff --git a/renderer/library/include/opengl/opengl.h b/renderer/library/include/opengl/opengl.h
index 79a1327..629bc12 100644
--- a/renderer/library/include/opengl/opengl.h
+++ b/renderer/library/include/opengl/opengl.h
@@ -8,7 +8,7 @@
 #include <OpenGLES/ES2/gl.h>
 #include <OpenGLES/ES2/glext.h>
 #else
-#include <OpenGL/gl.h>
+#include <OpenGL/gl3.h>
 #include <OpenGL/glu.h>
 #include <OpenGL/glext.h>
 #endif
diff --git a/renderer/library/include/opengl/opengl_render_path.hpp b/renderer/library/include/opengl/opengl_render_path.hpp
index 0ceb001..c704fce 100644
--- a/renderer/library/include/opengl/opengl_render_path.hpp
+++ b/renderer/library/include/opengl/opengl_render_path.hpp
@@ -2,6 +2,7 @@
 #define _RIVE_OPENGL_RENDER_PATH_HPP_
 
 #include "contour_render_path.hpp"
+#include "opengl.h"
 
 namespace rive
 {
diff --git a/renderer/library/include/opengl/opengl_renderer.hpp b/renderer/library/include/opengl/opengl_renderer.hpp
index 3f16727..2304d3d 100644
--- a/renderer/library/include/opengl/opengl_renderer.hpp
+++ b/renderer/library/include/opengl/opengl_renderer.hpp
@@ -15,6 +15,8 @@
 		GLuint m_VertexShader = 0, m_FragmentShader = 0;
 		GLuint m_Program = 0;
 		GLuint m_IndexBuffer = 0;
+		GLint m_ProjectionUniformIndex = -1;
+		GLint m_TransformUniformIndex = -1;
 		std::vector<unsigned short> m_Indices;
 
 	public:
@@ -34,6 +36,10 @@
 		bool initialize(void* data) override;
 
 		void updateIndexBuffer(std::size_t contourLength);
+
+		GLint transformUniformIndex() const { return m_TransformUniformIndex; }
+		GLuint program() const { return m_Program; }
+		virtual const char* shaderHeader() const { return nullptr; };
 	};
 
 } // namespace rive
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 9783827..fdfad07 100644
--- a/renderer/library/src/low_level_renderer/low_level_renderer.cpp
+++ b/renderer/library/src/low_level_renderer/low_level_renderer.cpp
@@ -49,6 +49,7 @@
 
 void LowLevelRenderer::transform(const Mat2D& transform)
 {
-	Mat2D::copy(m_Stack.back().transform, transform);
+	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
diff --git a/renderer/library/src/opengl/opengl_render_path.cpp b/renderer/library/src/opengl/opengl_render_path.cpp
index 04eb48e..1286f6b 100644
--- a/renderer/library/src/opengl/opengl_render_path.cpp
+++ b/renderer/library/src/opengl/opengl_render_path.cpp
@@ -1,22 +1,97 @@
 #include "opengl/opengl_render_path.hpp"
+#include "opengl/opengl_renderer.hpp"
+#include "opengl/opengl.h"
 
 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 subPathTransform;
+			Mat2D::multiply(subPathTransform, transform, subPath.transform());
+			reinterpret_cast<OpenGLRenderPath*>(subPath.path())
+			    ->stencil(renderer, subPathTransform);
+		}
+		return;
+	}
+
+	glUseProgram(renderer->program());
+	std::size_t vertexCount;
+
 	if (isDirty())
 	{
 		computeContour();
+		vertexCount = m_ContourVertices.size();
+		renderer->updateIndexBuffer(vertexCount);
 
 		glBindBuffer(GL_ARRAY_BUFFER, m_ContourBuffer);
-		auto length = m_ContourVertices.size();
 		glBufferData(GL_ARRAY_BUFFER,
-		             length * sizeof(float),
-		             &m_ContourVertices[0],
+		             vertexCount * 2 * sizeof(float),
+		             &m_ContourVertices[0][0],
 		             GL_DYNAMIC_DRAW);
 	}
+	else
+	{
+		glBindBuffer(GL_ARRAY_BUFFER, m_ContourBuffer);
+		vertexCount = m_ContourVertices.size();
+	}
+
+	if (vertexCount < 2)
+	{
+		return;
+	}
+
+	auto triangleCount = vertexCount - 2;
+	// 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());
+	glDrawElements(
+	    GL_TRIANGLES, triangleCount * 3, GL_UNSIGNED_SHORT, (void*)(0));
+	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]);
 }
\ 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 0b894f6..9313548 100644
--- a/renderer/library/src/opengl/opengl_renderer.cpp
+++ b/renderer/library/src/opengl/opengl_renderer.cpp
@@ -19,12 +19,14 @@
 
 bool OpenGLRenderer::initialize(void* data)
 {
+	fprintf(stderr, "init opengl\n");
 	assert(m_VertexShader == 0 && m_FragmentShader == 0 && m_Program == 0);
 
 	m_VertexShader =
 	    createAndCompileShader(GL_VERTEX_SHADER, vertexShaderSource);
 	if (m_VertexShader == 0)
 	{
+		fprintf(stderr, "init opengl no 1");
 		return false;
 	}
 
@@ -32,6 +34,7 @@
 	    createAndCompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
 	if (m_FragmentShader == 0)
 	{
+		fprintf(stderr, "init opengl no 2");
 		return false;
 	}
 
@@ -55,6 +58,21 @@
 	// Create index buffer which we'll grow and populate as necessary.
 	glGenBuffers(1, &m_IndexBuffer);
 
+	// TODO: CLEANUP
+	GLuint vao;
+	glGenVertexArrays(1, &vao);
+	glBindVertexArray(vao);
+	printf("VAO: %i\n", vao);
+	glUseProgram(m_Program);
+
+	m_ProjectionUniformIndex = glGetUniformLocation(m_Program, "projection");
+	m_TransformUniformIndex = glGetUniformLocation(m_Program, "transform");
+	GLint position = glGetAttribLocation(m_Program, "position");
+	fprintf(stderr, "POSITION: %i\n", position);
+	float projection[16] = {0.0f};
+	orthographicProjection(projection, 0.0f, 800, 800, 0.0f, 0.0f, 1.0f);
+	modelViewProjection(projection);
+
 	return true;
 }
 
@@ -65,6 +83,7 @@
 	{
 		return;
 	}
+
 	auto glPath = static_cast<OpenGLRenderPath*>(path);
 	glPath->stencil(this, transform());
 }
@@ -73,8 +92,12 @@
 
 void OpenGLRenderer::startFrame()
 {
-	glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
-	glClear(GL_COLOR_BUFFER_BIT);
+	// glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
+	// glClear(GL_COLOR_BUFFER_BIT);
+	glUseProgram(m_Program);
+	glEnableVertexAttribArray(0);
+	glUniformMatrix4fv(
+	    m_ProjectionUniformIndex, 1, GL_FALSE, m_ModelViewProjection);
 }
 
 void OpenGLRenderer::endFrame() {}
@@ -95,6 +118,7 @@
 	auto targetEdgeCount = contourLength - 2;
 	if (edgeCount < targetEdgeCount)
 	{
+
 		while (edgeCount < targetEdgeCount)
 		{
 			m_Indices.push_back(0);
diff --git a/renderer/library/src/opengl/opengl_shaders.cpp b/renderer/library/src/opengl/opengl_shaders.cpp
index 39d57c7..5e593f4 100644
--- a/renderer/library/src/opengl/opengl_shaders.cpp
+++ b/renderer/library/src/opengl/opengl_shaders.cpp
@@ -1,10 +1,14 @@
 
 const char* vertexShaderSource = R"""(
-attribute vec2 position;
+#version 330 core
+
+layout (location = 0) in vec2 position;
+
+out vec2 pos;
+
 uniform mat4 projection;
 uniform mat4 transform;
 uniform mat4 localTransform;
-varying vec2 pos;
 
 void main(void) 
 {
@@ -14,6 +18,8 @@
 )""";
 
 const char* fragmentShaderSource = R"""(
+#version 330 core
+
 #ifdef GL_ES
 precision highp float;
 #endif
@@ -25,53 +31,57 @@
 uniform vec4 colors[16];
 uniform float stops[16];
 uniform int fillType;
-varying vec2 pos;
+in vec2 pos;
+out vec4 fragColor;
+
 void main()
 {
-    if (fillType == 0)
-    {
-        // solid
-        gl_FragColor = vec4(color.rgb * color.a, color.a);
-    }
-    else if (fillType == 1)
-    {
-        // linear
-        vec2 toEnd = end - start;
-        float lengthSquared = toEnd.x * toEnd.x + toEnd.y * toEnd.y;
-        float f = dot(pos - start, toEnd) / lengthSquared;
-        gl_FragColor =
-            mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
-        for (int i = 1; i < 15; ++i)
-        {
-            if (i >= count - 1)
-            {
-                break;
-            }
-            gl_FragColor = mix(gl_FragColor,
-                            colors[i + 1],
-                            smoothstep(stops[i], stops[i + 1], f));
-        }
-        float alpha = gl_FragColor.w;
-        gl_FragColor = vec4(gl_FragColor.xyz * alpha, alpha);
-    }
-    else if (fillType == 2)
-    {
-        // radial
-        float f = distance(start, pos) / distance(start, end);
-        gl_FragColor =
-            mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
-        for (int i = 1; i < 15; ++i)
-        {
-            if (i >= count - 1)
-            {
-                break;
-            }
-            gl_FragColor = mix(gl_FragColor,
-                            colors[i + 1],
-                            smoothstep(stops[i], stops[i + 1], f));
-        }
-        float alpha = gl_FragColor.w;
-        gl_FragColor = vec4(gl_FragColor.xyz * alpha, alpha);
-    }
+    fragColor = vec4(0.3, 0.3, 0.3, 0.3);
+
+    // if (fillType == 0)
+    // {
+    //     // solid
+    //     gl_FragColor = vec4(color.rgb * color.a, color.a);
+    // }
+    // else if (fillType == 1)
+    // {
+    //     // linear
+    //     vec2 toEnd = end - start;
+    //     float lengthSquared = toEnd.x * toEnd.x + toEnd.y * toEnd.y;
+    //     float f = dot(pos - start, toEnd) / lengthSquared;
+    //     gl_FragColor =
+    //         mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
+    //     for (int i = 1; i < 15; ++i)
+    //     {
+    //         if (i >= count - 1)
+    //         {
+    //             break;
+    //         }
+    //         gl_FragColor = mix(gl_FragColor,
+    //                         colors[i + 1],
+    //                         smoothstep(stops[i], stops[i + 1], f));
+    //     }
+    //     float alpha = gl_FragColor.w;
+    //     gl_FragColor = vec4(gl_FragColor.xyz * alpha, alpha);
+    // }
+    // else if (fillType == 2)
+    // {
+    //     // radial
+    //     float f = distance(start, pos) / distance(start, end);
+    //     gl_FragColor =
+    //         mix(colors[0], colors[1], smoothstep(stops[0], stops[1], f));
+    //     for (int i = 1; i < 15; ++i)
+    //     {
+    //         if (i >= count - 1)
+    //         {
+    //             break;
+    //         }
+    //         gl_FragColor = mix(gl_FragColor,
+    //                         colors[i + 1],
+    //                         smoothstep(stops[i], stops[i + 1], f));
+    //     }
+    //     float alpha = gl_FragColor.w;
+    //     gl_FragColor = vec4(gl_FragColor.xyz * alpha, alpha);
+    // }
 }
 )""";
\ No newline at end of file
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/src/viewer.cpp b/renderer/viewer/src/viewer.cpp
index d5610f7..dcca39c 100644
--- a/renderer/viewer/src/viewer.cpp
+++ b/renderer/viewer/src/viewer.cpp
@@ -12,7 +12,7 @@
 #include "graphics_api.hpp"
 
 // Make sure gl3w is included before glfw3
-#include "GL/gl3w.h"
+// #include "GL/gl3w.h"
 #include "GLFW/glfw3.h"
 #include "GLFW/glfw3native.h"
 
@@ -60,9 +60,9 @@
 	if (graphicsApi == rive::GraphicsApi::opengl)
 	{
 		glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
-		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
-		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
-		// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
+		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
+		glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 	}
 	else
 	{
@@ -107,6 +107,7 @@
 	unsigned int fileBytesLength = 0;
 
 	std::string filename = "assets/polygon_party.riv";
+	// std::string filename = "assets/triangle.riv";
 	FILE* fp = fopen(filename.c_str(), "r");
 	fseek(fp, 0, SEEK_END);
 	fileBytesLength = ftell(fp);
@@ -150,6 +151,7 @@
 	{
 		int width = 0, height = 0;
 		glfwGetFramebufferSize(window, &width, &height);
+
 		if (lastWidth != width || lastHeight != height)
 		{
 			lastWidth = width;
diff --git a/src/contour_render_path_recursive.cpp b/src/contour_render_path_recursive.cpp
index c70ce45..560e512 100644
--- a/src/contour_render_path_recursive.cpp
+++ b/src/contour_render_path_recursive.cpp
@@ -62,6 +62,7 @@
 		}
 		m_IsPenDown = true;
 		Vec2D::copy(m_PenDown, m_Pen);
+		addVertex(m_PenDown);
 	}
 
 	inline void close()
@@ -72,6 +73,10 @@
 		}
 		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);
 	}
 
@@ -103,7 +108,7 @@
 				addVertex(Vec2D(CubicUtilities::cubicAt(
 				                    t2, from[0], fromOut[0], toIn[0], to[0]),
 				                CubicUtilities::cubicAt(
-				                    t2, from[1], fromOut[1], toIn[1], to[0])));
+				                    t2, from[1], fromOut[1], toIn[1], to[1])));
 			}
 		}
 	}
@@ -136,6 +141,7 @@
 				                       command.point(),
 				                       0.0f,
 				                       1.0f);
+				// segmenter.addVertex(command.point());
 				segmenter.pen(command.point());
 				break;
 			case PathCommandType::close:
@@ -149,7 +155,6 @@
 
 	// TODO: consider if there's a case with no points.
 	Vec2D& first = m_ContourVertices[0];
-
 	AABB::copy(m_ContourBounds, segmenter.bounds());
 	first[0] = m_ContourBounds.minX;
 	first[1] = m_ContourBounds.minY;