diff --git a/demo/main.cpp b/demo/main.cpp
index 64d444ed02c2f9c059ffa39740cd551ccff0dfa5..067388dbbd7b75da63568df5dc3917b30763ade5 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -397,7 +397,7 @@ int main(int argc, const char* argv[]) {
 
     menu->add("start", [&app]() { app.change_state("game"); });
 	menu->add("options", [&app]() { app.change_state("game"); });
-	menu->add("quit", [&app]() { app.change_state("game"); });
+	menu->add("quit", [&app]() { app.running(false); });
 
 	// game state
     app.add_state<GameScene>("game");
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 0ff4692f0cd80b5a8b782c34a9f24e5d8c9bf87e..d9b16e98062c62e3014be1004de48c79a0408ab9 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -25,6 +25,7 @@ target_sources(${PROJECT_NAME}
 	gui/widget.cpp
 	gui/widgets.cpp
 	gui/containers.cpp
+	math/triangulation.cpp
 )
 
 # spdlog for cleaner logging
diff --git a/fggl/gfx/CMakeLists.txt b/fggl/gfx/CMakeLists.txt
index 3b9118fc9fd7b62119fefabe66c37c9c536b1d32..40d3befc17a449174ad5c9f2c205e468550a51bf 100644
--- a/fggl/gfx/CMakeLists.txt
+++ b/fggl/gfx/CMakeLists.txt
@@ -9,4 +9,5 @@ target_sources(fggl
 
 # OpenGL backend
 add_subdirectory(ogl)
+add_subdirectory(ogl4)
 
diff --git a/fggl/gfx/ogl/CMakeLists.txt b/fggl/gfx/ogl/CMakeLists.txt
index 62e85335f3a3502230deef2f3ec847ea4144df4b..3eb9f94d1bfcdd66840c1ab2bdafc4a4b40bea3e 100644
--- a/fggl/gfx/ogl/CMakeLists.txt
+++ b/fggl/gfx/ogl/CMakeLists.txt
@@ -5,6 +5,7 @@ target_sources(fggl
 	backend.cpp
 	shader.cpp
 	renderer.cpp
+	types.cpp
 )
 
 # OpenGL Backend
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index baf0a00f95315a7fd9442bb07ed83d0ece87e1f7..af9e019ad96d63917880cefd79e31ae4aa3f072a 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -160,10 +160,12 @@ namespace fggl::gfx {
 		m_cache = std::make_unique<ShaderCache>(storage);
 
 		// setup 2D rendering system
-		m_token2D = setupVertex2D();
 		ShaderConfig shader2DConfig = ShaderFromName("shader2D");
 		m_cache->load(shader2DConfig);
 
+		// rendering helpers
+		m_canvasRenderer = std::make_unique<ogl4::CanvasRenderer>();
+		m_modelRenderer = std::make_unique<ogl4::StaticModelRenderer>();
 	};
 
 	void OpenGL4Backend::clear() {
@@ -171,85 +173,13 @@ namespace fggl::gfx {
 		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 	}
 
-	inline static void add_mesh_triangle(data::Mesh2D& mesh, const std::vector<data::Vertex2D>& verts) {
-		assert( verts.size() == 3);
-		for( const auto& vert : verts ) {
-			auto idx = mesh.add_vertex(vert);
-			mesh.add_index(idx);
-		}
-	}
-
-	static void generateMesh(const gfx::Paint &paint, Mesh2D &mesh) {
-		for (const auto &cmd : paint.cmds()) {
-			auto path = cmd.path;
-
-			std::vector<data::Vertex2D> verts;
-			math::vec3 colour{1.0F, 1.0F, 1.0F};
-			auto idx = 0;
-			auto colourIdx = 0;
-
-			for (auto &type : path.m_types) {
-				if (type == PathType::PATH) {
-					verts.push_back({.position = path.m_points[idx++], .colour = colour});
-				} else if (type == PathType::MOVE) {
-					// polygon finished
-					if (verts.size() < 3) {
-						// empty, point, or line
-						// TODO deal with whatever I'm meant to do with this...
-					} else if (verts.size() == 3) {
-						// triangle
-						add_mesh_triangle(mesh, verts);
-					} else {
-						// polygon
-						math::fanTriangulation(verts, mesh);
-					}
-
-					verts.clear();
-					verts.push_back({.position = path.m_points[idx++], .colour = colour});
-				} else if (type == PathType::COLOUR) {
-					colour = path.m_colours[colourIdx++];
-				} else {
-					// unsupported type
-				}
-			}
-
-			if (!verts.empty()) {
-				math::fanTriangulation(verts, mesh);
-			}
-		}
-	}
-
 	void OpenGL4Backend::draw2D(const gfx::Paint &paint) {
-		// generate the mesh from a paint command list
-		data::Mesh2D mesh;
-		generateMesh(paint, mesh);
+		if ( !m_canvasRenderer ) {
+			return;
+		}
 
-		// render the resulting mesh
 		auto shader2D = m_cache->get("shader2D");
-
-		glUseProgram(shader2D);
-
-		auto projMat = glm::ortho(0.0f, 1920.0f, 0.0f, 1080.f);
-		glUniformMatrix4fv(glGetUniformLocation(shader2D, "projection"), 1, GL_FALSE,
-						   glm::value_ptr(projMat));
-
-		glBindVertexArray(m_token2D.vao);
-
-		glBindBuffer(GL_ARRAY_BUFFER, m_token2D.buffs[0]);
-		glBufferData(GL_ARRAY_BUFFER, mesh.vertexList.size() * sizeof(Vertex2D),
-					 mesh.vertexList.data(), GL_DYNAMIC_DRAW);
-
-		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_token2D.buffs[1]);
-		glBufferData(GL_ELEMENT_ARRAY_BUFFER,
-					 mesh.indexList.size() * sizeof(uint32_t), mesh.indexList.data(),
-					 GL_DYNAMIC_DRAW);
-
-		glDrawElements(GL_TRIANGLES, mesh.indexList.size(), GL_UNSIGNED_INT,
-					   (GLvoid *) 0);
-
-		glBindVertexArray(0);
-		glUseProgram(0);
-		// glDisable(GL_PRIMITIVE_RESTART);
+		m_canvasRenderer->render(shader2D, paint);
 	}
 
 	void OpenGL4Backend::resize(int width, int height) {
diff --git a/fggl/gfx/ogl/types.cpp b/fggl/gfx/ogl/types.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7bb829337afaf915e818a209979988968632f96e
--- /dev/null
+++ b/fggl/gfx/ogl/types.cpp
@@ -0,0 +1,135 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "fggl/gfx/ogl/types.hpp"
+#include <cassert>
+
+//
+// special defines:
+//
+// FGGL_GL_I_BOUND will take responsibility away from fggl for ensuring buffers are bound before use.
+// FGGL_GL_PARANOID will try to ensure that openGL state is managed correctly in method calls, but will be slower
+
+namespace fggl::gfx::ogl {
+
+	template<> const BuffAttrF attr_type<float>::attr = BuffAttrF::FLOAT;
+	template<> const BuffAttrF attr_type<math::vec2>::attr = BuffAttrF::FLOAT;
+	template<> const BuffAttrF attr_type<math::vec3>::attr = BuffAttrF::FLOAT;
+	template<> const BuffAttrF attr_type<math::vec4>::attr = BuffAttrF::FLOAT;
+	template<> const GLint attr_type<float>::size = 1;
+	template<> const GLint attr_type<math::vec2>::size = 2;
+	template<> const GLint attr_type<math::vec3>::size = 3;
+	template<> const GLint attr_type<math::vec4>::size = 4;
+
+	VertexArray::VertexArray() {
+		glGenVertexArrays(1, &m_obj);
+	}
+
+	VertexArray::~VertexArray() {
+		release();
+	}
+
+	VertexArray::VertexArray(VertexArray &&other) noexcept : m_obj(other.m_obj) {
+		other.m_obj = 0;
+	}
+
+	VertexArray &VertexArray::operator=(VertexArray &&other) {
+		if ( this != &other ){
+			release();
+			std::swap(m_obj, other.m_obj);
+		}
+		return *this;
+	}
+
+	void VertexArray::release() {
+		glDeleteVertexArrays(1, &m_obj);
+		m_obj = 0;
+	}
+
+	void VertexArray::setAttribute(const ArrayBuffer& buff, GLuint idx, AttributeF& attr) {
+		assert( 0 <= idx && idx < GL_MAX_VERTEX_ATTRIBS);
+		assert( 1 <= attr.elmCount && attr.elmCount <= 4);
+		assert( buff.isValid() );
+
+		#ifndef FGGL_GL_I_BOUND
+			bind();
+			GLuint boundVertexArray = 0;
+			bind_buffer(&boundVertexArray, buff);
+		#endif
+
+		glEnableVertexAttribArray(idx);
+		glVertexAttribPointer( idx, attr.elmCount, (GLenum)attr.attrType, GL_FALSE, attr.stride, (void*)attr.offset);
+
+		#ifndef FGGL_GL_I_BOUND
+			unbind_buffer(&boundVertexArray, buff);
+		#endif
+	}
+
+	void VertexArray::setAttribute(const ArrayBuffer& buff, GLuint idx, AttributeI& attr, bool normalized) {
+		assert( 0 <= idx && idx < GL_MAX_VERTEX_ATTRIBS);
+		assert( 1 <= attr.elmCount && attr.elmCount <= 4);
+		assert( buff.isValid() );
+
+		#ifndef FGGL_GL_I_BOUND
+			GLuint boundVertexArray = 0;
+			bind_buffer(&boundVertexArray, buff);
+		#endif
+
+		glVertexAttribPointer(idx, attr.elmCount, (GLenum)attr.attrType, (GLboolean)normalized, attr.stride, (void*)attr.offset);
+
+		#ifndef FGGL_GL_I_BOUND
+			unbind_buffer(&boundVertexArray, buff);
+		#endif
+	}
+
+	void VertexArray::setAttributeI(const ArrayBuffer& buff, GLuint idx, AttributeI& attr) {
+		assert( 0 <= idx && idx < GL_MAX_VERTEX_ATTRIBS);
+		assert( 1 <= attr.elmCount && attr.elmCount <= 4);
+		assert( buff.isValid() );
+
+		#ifndef FGGL_GL_I_BOUND
+			GLuint boundVertexArray = 0;
+			bind_buffer(&boundVertexArray, buff);
+		#endif
+
+		glVertexAttribIPointer( idx, attr.elmCount, (GLenum)attr.attrType, attr.stride, (void*)attr.offset);
+
+		#ifndef FGGL_GL_I_BOUND
+			unbind_buffer(&boundVertexArray, buff);
+		#endif
+	}
+
+	void VertexArray::drawElements(const ElementBuffer &buff, Primative drawType, std::size_t size) {
+		bind();
+
+		#ifndef FGGL_I_BOUND
+			GLuint boundElementArray = 0;
+			bind_buffer(&boundElementArray, buff);
+		#endif
+
+		glDrawElements( (GLenum)drawType, (GLsizei)size, GL_UNSIGNED_INT, nullptr );
+
+		#ifndef FGGL_I_BOUND
+			unbind_buffer(&boundElementArray, buff);
+		#endif
+	}
+
+
+} // namespace fggl::gfx::ogl
\ No newline at end of file
diff --git a/fggl/gfx/ogl4/CMakeLists.txt b/fggl/gfx/ogl4/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b66df9424352f3ff782db0bf27e3622ee99fdb16
--- /dev/null
+++ b/fggl/gfx/ogl4/CMakeLists.txt
@@ -0,0 +1,6 @@
+
+# Sources
+target_sources(fggl
+    PRIVATE
+		canvas.cpp
+)
diff --git a/fggl/gfx/ogl4/canvas.cpp b/fggl/gfx/ogl4/canvas.cpp
index 66f166f95554e24bc7c89959e25e41911757928b..36dcf09cccdca69e9c040b3e6fcfc6ab68bbf73d 100644
--- a/fggl/gfx/ogl4/canvas.cpp
+++ b/fggl/gfx/ogl4/canvas.cpp
@@ -22,18 +22,108 @@
 #include "fggl/gfx/ogl4/canvas.hpp"
 
 #include "fggl/data/model.hpp"
+#include "fggl/math/types.hpp"
+
+#include <glm/gtc/type_ptr.hpp>
+#include "fggl/math/triangulation.hpp"
 
 #define FGGL_OPENGL_CORRECTNESS
 
 namespace fggl::gfx::ogl4 {
 
+	inline static void add_mesh_triangle(data::Mesh2D& mesh, const std::vector<data::Vertex2D>& verts) {
+		assert( verts.size() == 3);
+		for( const auto& vert : verts ) {
+			auto idx = mesh.add_vertex(vert);
+			mesh.add_index(idx);
+		}
+	}
+
 	static void convert_to_mesh(const gfx::Paint& paint, data::Mesh2D& mesh) {
+		for (const auto &cmd : paint.cmds()) {
+			auto path = cmd.path;
+
+			std::vector<data::Vertex2D> verts;
+			math::vec3 colour{1.0F, 1.0F, 1.0F};
+			auto idx = 0;
+			auto colourIdx = 0;
+
+			for (auto &type : path.m_types) {
+				if (type == PathType::PATH) {
+					verts.push_back({.position = path.m_points[idx++], .colour = colour});
+				} else if (type == PathType::MOVE) {
+					// polygon finished
+					if (verts.size() < 3) {
+						// empty, point, or line
+						// TODO deal with whatever I'm meant to do with this...
+					} else if (verts.size() == 3) {
+						// triangle
+						add_mesh_triangle(mesh, verts);
+					} else {
+						// polygon
+						math::fan_triangulation(verts, mesh);
+					}
+
+					verts.clear();
+					verts.push_back({.position = path.m_points[idx++], .colour = colour});
+				} else if (type == PathType::COLOUR) {
+					colour = path.m_colours[colourIdx++];
+				} else {
+					// unsupported type
+				}
+			}
+
+			if (!verts.empty()) {
+				math::fan_triangulation(verts, mesh);
+			}
+		}
 	}
 
-	void CanvasRenderer::render(const gfx::Paint &paint) {
+	CanvasRenderer::CanvasRenderer() {
+		m_vao.bind();
+
+		#ifdef FGGL_GL_I_BOUND
+			// user will handle binding themselves usually, so attribute won't bind itself.
+			// which means it's our problem right now...
+			GLuint originalVertexList;
+			ogl::bind_buffer( &originalVertexList, m_vertexList );
+		#endif
+
+		// define our attributes
+		auto posAttr = ogl::attribute<data::Vertex2D, math::vec2>(offsetof(data::Vertex2D, position));
+		auto colAttr = ogl::attribute<data::Vertex2D, math::vec3>(offsetof(data::Vertex2D, colour));
+
+		// bind the attributes to the vao
+		m_vao.setAttribute(m_vertexList, 0, posAttr);
+		m_vao.setAttribute(m_vertexList, 1, colAttr);
+
+		#ifdef FGGL_GL_I_BOUND
+			// cool, rebind whatever happened before, or not
+			ogl::unbind_buffer( &originalVertexList, m_vertexList );
+		#endif
+
+		glBindVertexArray(0);
+	}
+
+	void CanvasRenderer::render(GLuint shader, const gfx::Paint &paint) {
 		data::Mesh2D mesh;
 		convert_to_mesh(paint, mesh);
 
+		// update data
+		m_vao.bind();
+		m_vertexList.replace(mesh.vertexList.size(), mesh.vertexList.data());
+		m_indexList.replace(mesh.indexList.size(), mesh.indexList.data());
+
+		// draw
+
+		// FIXME: this should be abstracted into the shader class
+		glUseProgram( shader );
+		auto projMat = glm::ortho(0.0f, 1920.0f, 1080.0f, 0.f);
+		glUniformMatrix4fv(glGetUniformLocation( shader, "projection"), 1, GL_FALSE,
+						   glm::value_ptr(projMat));
+
+		m_vao.drawElements(m_indexList, ogl::Primative::TRIANGLE, mesh.indexList.size());
+		glUseProgram( 0 );
 	}
 
 }
\ No newline at end of file
diff --git a/fggl/math/triangulation.cpp b/fggl/math/triangulation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..88d3d19c0c8d373dbc76a19fd8565d25003b0eb1
--- /dev/null
+++ b/fggl/math/triangulation.cpp
@@ -0,0 +1,48 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "fggl/math/triangulation.hpp"
+
+namespace fggl::math {
+
+	/**
+	 * Fast Triangulation for convex polygons.
+	 */
+	void fan_triangulation(const PolygonVertex& polygon, data::Mesh2D &mesh) {
+		assert(polygon.size() >= 3);
+
+		// add the first two points to the mesh
+		auto firstIdx = mesh.add_vertex(polygon[0]);
+		auto prevIdx = mesh.add_vertex(polygon[1]);
+
+		// deal with the indices
+		const auto nTris = polygon.size() - 2;
+		for (auto i = 0; i < nTris; i++) {
+			mesh.add_index(firstIdx);
+			mesh.add_index(prevIdx);
+
+			auto currIdx = mesh.add_vertex(polygon[i + 2]);
+			mesh.add_index(currIdx);
+			prevIdx = currIdx;
+		}
+	}
+
+} // namespace fggl::math
+
diff --git a/fggl/scenes/menu.cpp b/fggl/scenes/menu.cpp
index 386dbf67feab1a9be8eb7a0d0516e12f9204c9d4..26b800aff6309723e12afa993ce86223f7cd945a 100644
--- a/fggl/scenes/menu.cpp
+++ b/fggl/scenes/menu.cpp
@@ -28,7 +28,8 @@ namespace fggl::scenes {
 			// in canvas space
 			math::vec2 projected;
 			projected.x = math::rescale_ndc(m_cursorPos.x, 0, 1920.f);
-			projected.y = math::rescale_ndc(m_cursorPos.y, 1080.0f, 0);
+			//projected.y = math::rescale_ndc(m_cursorPos.y, 1080.0f, 0);
+			projected.y = math::rescale_ndc(m_cursorPos.y, 0, 1080.0f);
 
 			auto* hoverWidget = m_canvas.getChildAt(projected);
 			if ( hoverWidget != m_hover ){
diff --git a/include/fggl/gfx/ogl/renderer.hpp b/include/fggl/gfx/ogl/renderer.hpp
index 1f8a60f8136a331986b45ee856cbfa98cf1c2c78..24c0ab1824c2ae3acbf198a7090b917354192950 100644
--- a/include/fggl/gfx/ogl/renderer.hpp
+++ b/include/fggl/gfx/ogl/renderer.hpp
@@ -49,16 +49,18 @@ namespace fggl::gfx {
 			explicit OpenGL4Backend(const Window &owner);
 			~OpenGL4Backend() override = default;
 
+			// copy bad
+			OpenGL4Backend(const OpenGL4Backend&) = delete;
+			OpenGL4Backend& operator=(const OpenGL4Backend&) = delete;
+
 			void clear() override;
 			void resize(int width, int height) override;
 
 			void draw2D(const Paint &paint) override;
 
 		private:
-			ogl4::StaticModelRenderer m_modelRenderer;
-			ogl4::CanvasRenderer m_canvasRenderer;
-
-			GlRenderToken m_token2D;
+			std::unique_ptr<ogl4::StaticModelRenderer> m_modelRenderer;
+			std::unique_ptr<ogl4::CanvasRenderer> m_canvasRenderer;
 			std::unique_ptr<ShaderCache> m_cache;
 	};
 
diff --git a/include/fggl/gfx/ogl/types.hpp b/include/fggl/gfx/ogl/types.hpp
index 75917b153024e8bec784898743a7b1afa87ccca2..45d8cf617189a4a66fe1d4630a3bfc59aee58664 100644
--- a/include/fggl/gfx/ogl/types.hpp
+++ b/include/fggl/gfx/ogl/types.hpp
@@ -19,14 +19,16 @@
  */
 
 //
-// Created by webpigeon on 17/04/22.
+// uses RAII, with reference to https://www.khronos.org/opengl/wiki/Common_Mistakes#RAII_and_hidden_destructor_calls
 //
 
 #ifndef FGGL_GFX_OGL_TYPES_HPP
 #define FGGL_GFX_OGL_TYPES_HPP
 
 #include <cstdint>
+#include <cassert>
 #include <string_view>
+#include <iostream>
 
 #include "fggl/gfx/ogl/common.hpp"
 #include "fggl/math/types.hpp"
@@ -61,30 +63,30 @@ namespace fggl::gfx::ogl {
 			Location uniform(const std::string& name) const;
 
 			// primatives
-			void setUniform(Location name, GLfloat value);
-			void setUniform(Location name, GLint value);
-			void setUniform(Location name, GLuint value);
+			void setUniformF(Location name, GLfloat value);
+			void setUniformI(Location name, GLint value);
+			void setUniformU(Location name, GLuint value);
 
 			// vector versions (float)
-			void setUniform(Location name, math::vec2f value);
-			void setUniform(Location name, math::vec3f value);
-			void setUniform(Location name, math::vec4f value);
-			void setUniform(Location name, const math::vec2f* value, GLsizei size);
-			void setUniform(Location name, const math::vec3f* value, GLsizei size);
-			void setUniform(Location name, const math::vec4f* value, GLsizei size);
+			void setUniformF(Location name, math::vec2f value);
+			void setUniformF(Location name, math::vec3f value);
+			void setUniformF(Location name, math::vec4f value);
+			void setUniformF(Location name, const math::vec2f* value, GLsizei size);
+			void setUniformF(Location name, const math::vec3f* value, GLsizei size);
+			void setUniformF(Location name, const math::vec4f* value, GLsizei size);
 
 			// vector versions (int)
-			void setUniform(Location name, math::vec2i value);
-			void setUniform(Location name, math::vec3i value);
-			void setUniform(Location name, math::vec4i value);
-			void setUniform(Location name, const math::vec2i* value, GLsizei size);
-			void setUniform(Location name, const math::vec3i* value, GLsizei size);
-			void setUniform(Location name, const math::vec4i* value, GLsizei size);
+			void setUniformI(Location name, math::vec2i value);
+			void setUniformI(Location name, math::vec3i value);
+			void setUniformI(Location name, math::vec4i value);
+			void setUniformI(Location name, const math::vec2i* value, GLsizei size);
+			void setUniformI(Location name, const math::vec3i* value, GLsizei size);
+			void setUniformI(Location name, const math::vec4i* value, GLsizei size);
 
 			// matrix versions
-			void setUniform(Location name, const math::mat2*, GLsizei count);
-			void setUniform(Location name, const math::mat3*, GLsizei count);
-			void setUniform(Location name, const math::mat4*, GLsizei count);
+			void setUniformMtx(Location name, const math::mat2*, GLsizei count);
+			void setUniformMtx(Location name, const math::mat3*, GLsizei count);
+			void setUniformMtx(Location name, const math::mat4*, GLsizei count);
 	};
 
 	enum class BuffAttrF {
@@ -147,45 +149,176 @@ namespace fggl::gfx::ogl {
 		STREAM_COPY = GL_STREAM_COPY,
 	};
 
+	template<BufType T>
 	class Buffer {
 		private:
 			GLuint m_obj = 0;
-			void release();
+			GLsizeiptr m_capacity = 0;
+
+			/**
+			 * Free an underlying buffer.
+			 *
+			 * If this buffer object is already empty, this is a no-op (and the OpenGL spec guarntees that freeing buffer
+			 * 0 has no effect.
+			 */
+			void release() {
+				glDeleteBuffers(1, &m_obj);
+				m_obj = 0;
+			}
 
 		public:
+			/**
+			 * Create a new (unfilled) buffer object.
+			 */
+			Buffer() {
+				glGenBuffers(1, &m_obj);
+			}
+
+			~Buffer() {
+				release();
+			}
+
 			// copy constructor bad
 			Buffer(const Buffer&) = delete;
 			Buffer& operator=(const Buffer&) = delete;
 
-			Buffer(Buffer&& other);
-			Buffer& operator=(Shader&& other);
+			Buffer(Buffer&& other) : m_obj(other.m_obj), m_capacity(other.m_capacity) {
+				other.obj_ = 0;
+				other.m_capacity = 0;
+			}
+
+			Buffer& operator=(Buffer&& other) {
+				if ( this != &other) {
+					release();
+					std::swap( m_obj, other.m_obj );
+					std::swap( m_capacity, other.m_capacity );
+				}
+			}
+
+			void bind() const {
+				assert( m_obj != 0 );
+				glBindBuffer( (GLenum)T, m_obj );
+			}
+
+			inline bool isValid() const {
+				return m_obj != 0;
+			};
+
+			void write(GLsizeiptr size, const GLvoid* data, BufUsage usage) {
+				bind();
+				glBufferData( (GLenum)T, size, data, (GLenum)usage);
+			}
+
+			void update(GLintptr offset, GLsizeiptr size, const void* data) {
+
+			}
+
+			template<typename D>
+			void replace(std::size_t size, D* data) {
+				bind();
+				GLsizeiptr sizePtr = size * sizeof(D);
+				if ( sizePtr > m_capacity) {
+					glBufferData( (GLenum)T, sizePtr, data, GL_STREAM_DRAW );
+					m_capacity = sizePtr;
+				} else {
+					glBufferSubData((GLenum)T, 0, sizePtr, data);
+				}
+				glBindBuffer( (GLenum)T, 0 );
+			}
 
-			void bind();
+	};
+
+	// common buffer types
+	using ArrayBuffer = Buffer<BufType::ARRAY>;
+	using ElementBuffer = Buffer<BufType::ELEMENT_ARRAY>;
+
+	struct AttributeF {
+		BuffAttrF attrType;
+		GLint elmCount;
+		GLsizei stride;
+		std::size_t offset;
+	};
 
-			void write(BufType target, GLsizeiptr size, const GLvoid* data, BufUsage usage);
-			void update(BufType target, GLintptr offset, GLsizeiptr size, const void* data);
+	struct AttributeI {
+		BuffAttrI attrType;
+		GLint elmCount;
+		GLsizei stride;
+		std::size_t offset;
+	};
 
-			// NOTE: size must be between 1 - 4
-			void setAttribute(GLuint idx, GLint size, BuffAttrF attr, bool normalized, GLsizei stride, std::size_t offset);
-			void setAttribute(GLuint idx, GLint size, BuffAttrI attr, GLsizei stride, std::size_t offset);
-			void setAttributeI(GLuint idx, GLint size, BuffAttrI attr, GLsizei stride, std::size_t offset);
+	// type intrincs to make interface nicer
+	template<typename T>
+	struct attr_type{
+		const static BuffAttrF attr;
+		const static GLint size;
 	};
 
+	template<typename V, typename T>
+	AttributeF attribute(std::size_t offset) {
+		return AttributeF{
+			.attrType = attr_type<T>::attr,
+			.elmCount = attr_type<T>::size,
+			.stride = sizeof(V),
+			.offset = offset
+		};
+	}
+
+	/**
+	 * Represents an OpenGL vertex array object (VAO).
+	 *
+	 * This contains most information about an OpenGL state for supplying vertex data to the GPU. There is some basic
+	 * debug checking to stop silly stuff happening, but this will be disabled in release mode.
+	 */
 	class VertexArray {
 		private:
-			GLuint obj_ = 0;
-			void Release();
+			GLuint m_obj = 0;
+			void release();
 
 		public:
-			VertexArray(const Buffer&) = delete;
+			VertexArray();
+			~VertexArray();
+
+			// copy constructors bad
+			VertexArray(const VertexArray&) = delete;
 			VertexArray& operator=(const VertexArray) = delete;
 
-			VertexArray(VertexArray&& other);
+			// move constructors might be ok
+			VertexArray(VertexArray&& other) noexcept;
 			VertexArray& operator=(VertexArray&& other);
 
-			void bind();
+			inline void bind() const {
+				assert( m_obj != 0);
+				glBindVertexArray( m_obj );
+			}
+
+			void setAttribute(const ArrayBuffer& buffer, GLuint idx, AttributeF& attr);
+			void setAttribute(const ArrayBuffer& buffer, GLuint idx, AttributeI& attr, bool normalized);
+			void setAttributeI(const ArrayBuffer& buffer, GLuint idx, AttributeI& attr);
+
+			void drawElements(const ElementBuffer& buff, Primative drawType, std::size_t size);
 	};
 
+	// paranoid functions
+	void bind_vertex_array(GLuint& marker);
+	void unbind_vertex_array(GLuint& marker);
+
+	template<BufType T>
+	void bind_buffer(GLuint* marker, const Buffer<T>& buff) {
+		#ifdef FGGL_GL_PARANOID
+			assert( marker != nullptr );
+			glGetIntegerv( (GLenum)T, (GLint*) marker );
+		#endif
+		buff.bind();
+	}
+
+	template<BufType T>
+	void unbind_buffer(GLuint* marker, const Buffer<T>& buff) {
+		#ifdef GL_FGGL_PARANOID
+			assert( marker != nullptr );
+			glBindVertexArray(marker);
+		#endif
+	}
+
 } // namespace fggl::gfx::ogl
 
 #endif //FGGL_GFX_OGL_TYPES_HPP
diff --git a/include/fggl/gfx/ogl4/canvas.hpp b/include/fggl/gfx/ogl4/canvas.hpp
index 20330687dd265879a6d7300d5bf170709826978c..8ace79c04bc4a96e06bedc4a762899b63ebf82ac 100644
--- a/include/fggl/gfx/ogl4/canvas.hpp
+++ b/include/fggl/gfx/ogl4/canvas.hpp
@@ -5,17 +5,20 @@
 #ifndef FGGL_GFX_OGL4_CANVAS_HPP
 #define FGGL_GFX_OGL4_CANVAS_HPP
 
-#include <fggl/gfx/paint.hpp>
-
+#include "fggl/gfx/paint.hpp"
+#include "fggl/gfx/ogl/types.hpp"
 
 namespace fggl::gfx::ogl4 {
 
 	class CanvasRenderer {
 		public:
-			CanvasRenderer() = default;
-			void render(const gfx::Paint& paint);
+			CanvasRenderer();
+			void render(GLuint shader, const gfx::Paint& paint);
 
 		private:
+			ogl::VertexArray m_vao;
+			ogl::Buffer<ogl::BufType::ARRAY> m_vertexList;
+			ogl::Buffer<ogl::BufType::ELEMENT_ARRAY> m_indexList;
 	};
 
 } // namespace fggl::gfx::ogl4
diff --git a/include/fggl/math/triangulation.hpp b/include/fggl/math/triangulation.hpp
index 11578c88cccddc80d94b1f8e80a1a92a0cbd0396..a62860a22268cffd75ed46bed843965c8e20adf2 100644
--- a/include/fggl/math/triangulation.hpp
+++ b/include/fggl/math/triangulation.hpp
@@ -1,5 +1,5 @@
-#ifndef FGGL_MATH_TRIS_H
-#define FGGL_MATH_TRIS_H
+#ifndef FGGL_MATH_TRIANGULATION_HPP
+#define FGGL_MATH_TRIANGULATION_HPP
 
 #include <fggl/math/types.hpp>
 #include <fggl/data/model.hpp>
@@ -48,7 +48,7 @@ namespace fggl::math {
 	 *
 	 * see https://math.stackexchange.com/a/1745427
 	 */
-	bool isConvex(const Polygon &polygon) {
+	static bool isConvex(const Polygon &polygon) {
 		if (polygon.size() < 3) {
 			return false;
 		}
@@ -119,24 +119,7 @@ namespace fggl::math {
 	/**
 	 * Fast Triangulation for convex polygons.
 	 */
-	void fanTriangulation(const PolygonVertex& polygon, data::Mesh2D &mesh) {
-		assert(polygon.size() >= 3);
-
-		// add the first two points to the mesh
-		auto firstIdx = mesh.add_vertex(polygon[0]);
-		auto prevIdx = mesh.add_vertex(polygon[1]);
-
-		// deal with the indices
-		const auto nTris = polygon.size() - 2;
-		for (auto i = 0; i < nTris; i++) {
-			mesh.add_index(firstIdx);
-			mesh.add_index(prevIdx);
-
-			auto currIdx = mesh.add_vertex(polygon[i + 2]);
-			mesh.add_index(currIdx);
-			prevIdx = currIdx;
-		}
-	}
+	void fan_triangulation(const PolygonVertex& polygon, data::Mesh2D &mesh);
 
 } // namespace fggl::util