diff --git a/.gitignore b/.gitignore
index 33de3094fdfe2a4c76695591c56185f62f2fb18b..cd44b8d8673cd84ae3bb4568083ebf783e8dc3db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 build/
 cmake-build-debug/
+cmake-build-debug-coverage/
 .idea/
diff --git a/demo/imgui.ini b/demo/imgui.ini
new file mode 100644
index 0000000000000000000000000000000000000000..4a5c20148e89683813a21aaba10843ac98bbf579
--- /dev/null
+++ b/demo/imgui.ini
@@ -0,0 +1,5 @@
+[Window][Debug##Default]
+Pos=60,60
+Size=400,400
+Collapsed=0
+
diff --git a/demo/main.cpp b/demo/main.cpp
index 0ce8dfd01d5a7c538797eec85792f8d4b0015329..cece5848aa81f22adf6ab4e95f41324658e5b3f9 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -6,6 +6,7 @@
 #include <fggl/gfx/renderer.hpp>
 #include <fggl/gfx/shader.hpp>
 #include <fggl/data/procedural.hpp>
+#include <fggl/ecs/ecs.hpp>
 #include <fggl/debug/debug.h>
 #include <fggl/data/storage.hpp>
 
@@ -30,15 +31,20 @@ int main(int argc, char* argv[]) {
 	config.name = "unlit";
 	config.vertex = "unlit_vert.glsl";
 	config.fragment = "unlit_frag.glsl";
-	cache.load(config);
+	auto shader = cache.load(config);
 
-	// generate meshes
-	std::vector<fggl::data::Model> scene;
+	// create ECS
+	fggl::ecs::ECS ecs;
+	ecs.registerComponent<fggl::gfx::MeshToken>();
 
-/*	fggl::data::Model model;
-	fggl::data::Mesh cube = fggl::data::make_cube();
-	model.append(cube);
-	scene.push_back( model );*/
+	// create an entity
+	auto entity = ecs.createEntity();
+
+	// in a supprise to no one it's a triangle
+	auto mesh = fggl::data::make_triangle();
+	auto token = meshRenderer.upload(mesh);
+	token.pipeline = shader;
+	ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
 
 	while( !win.closeRequested() ) {
 		ctx.pollEvents();
@@ -48,7 +54,7 @@ int main(int argc, char* argv[]) {
 
 		// render step
 		ogl.clear();
-		meshRenderer.render(scene);
+		meshRenderer.render(win, ecs);
 
 		debug.draw();
 		win.swap();
diff --git a/fggl/data/model.hpp b/fggl/data/model.hpp
index 6e7a7a75a52454ae03d84a5b46111908547e8e93..ed67945573124eba2bebcbf9667168e7612eacbe 100644
--- a/fggl/data/model.hpp
+++ b/fggl/data/model.hpp
@@ -24,7 +24,7 @@ namespace fggl::data {
 			 * be added to the indicies list.
 			 */
 			inline void push(Vertex vert) {
-				int idx = indexOf(vert);
+				auto idx = indexOf(vert);
 				if ( idx == -1 )
 					pushVertex(vert);
 				else
@@ -53,10 +53,18 @@ namespace fggl::data {
 			 */
 			int indexOf(Vertex vert);
 
+			inline std::vector<Vertex>& vertexList() {
+				return m_verts;
+			}
+
 			inline std::size_t vertexCount() {
 				return m_verts.size();
 			}
 
+			inline std::vector<uint32_t>& indexList() {
+				return m_index;
+			}
+
 			inline std::size_t indexCount() {
 				return m_index.size();
 			}
@@ -79,10 +87,6 @@ namespace fggl::data {
 				m_meshes.push_back( mesh );
 			}
 
-			inline const std::vector<Mesh>& meshes() const {
-				return m_meshes;
-			}
-
 		private:
 			std::vector<Mesh> m_meshes;
 	};
diff --git a/fggl/data/procedural.cpp b/fggl/data/procedural.cpp
index 8cf57d31ab7b0d185746d2054ff5e51f2816c408..b762adc46169336db75ad77a429adca573d72271 100644
--- a/fggl/data/procedural.cpp
+++ b/fggl/data/procedural.cpp
@@ -4,7 +4,32 @@
 
 using namespace fggl::data;
 
-constexpr float verts[8 * 3] = {
+fggl::data::Mesh fggl::data::make_triangle() {
+    constexpr fggl::math::vec3 pos[]{
+            {-0.5f, -0.5f, 0.0f},
+            {0.5f,  -0.5f, 0.0f},
+            {0.0f,  0.5f,  0.0f}
+    };
+
+    // add points
+    fggl::data::Mesh mesh;
+    for (auto po : pos) {
+        Vertex vert{};
+        vert.posititon = po;
+        vert.colour = fggl::math::vec3(1.0f, 1.0f, 1.0f);
+        mesh.push(vert);
+    }
+
+    // mesh
+    for (int i = 0; i < 3; ++i) {
+        mesh.pushIndex(i);
+    }
+
+    return mesh;
+}
+
+/*
+constexpr float cubeVertex[8 * 3] = {
 	0, 0, 0,
 	0, 1, 0,
 	1, 1, 0,
@@ -15,7 +40,7 @@ constexpr float verts[8 * 3] = {
 	0, 0, 0,
 };
 
-constexpr int quads[6 * 4] = {
+constexpr int cubeIndexes[6 * 4] = {
 	7, 6, 5, 4,
 	0, 1, 2, 3,
 	6, 7, 3, 2,
@@ -77,8 +102,8 @@ void computeNormals(Mesh& mesh) {
 
 Mesh fggl::data::make_cube() {
 
-	int nVerts = sizeof(verts) / sizeof(verts[0]) / 3;
-	int nQuads = sizeof(quads) / sizeof(quads[0]) / 4;
+	int nVerts = sizeof(cubeVertex) / sizeof(cubeVertex[0]) / 3;
+	int nQuads = sizeof(cubeIndexes) / sizeof(cubeIndexes[0]) / 4;
 
 	std::vector< unsigned int > vertIndex;
 
@@ -87,16 +112,16 @@ Mesh fggl::data::make_cube() {
 	// stick the vertex data into the mesh
 	for ( int i=0; i < nVerts; i++ ) {
 		fggl::data::Vertex vert;
-		vert.posititon = fggl::math::vec3( verts[i], verts[i + 1], verts[i+2] );
+		vert.posititon = fggl::math::vec3( cubeVertex[i], cubeVertex[i + 1], cubeVertex[i+2] );
 
-		int idx = cube.pushVertex(vert);
+		auto idx = cube.pushVertex(vert);
 		vertIndex.push_back(idx);
 	}
 
 	// triagulate the quads
-	quadsToTris(cube, vertIndex, quads, nQuads);
+	quadsToTris(cube, vertIndex, cubeIndexes, nQuads);
 	computeNormals(cube);
 
 	return cube;
-}
+}*/
 
diff --git a/fggl/data/procedural.hpp b/fggl/data/procedural.hpp
index 2c9ddf7cd07737f14f4acf0a1db51fa938a8aa1e..32b0424904011b1d406e707f6b47ff9243f9632f 100644
--- a/fggl/data/procedural.hpp
+++ b/fggl/data/procedural.hpp
@@ -3,5 +3,7 @@
 
 namespace fggl::data {
 
-	Mesh make_cube();
+	//Mesh make_cube();
+
+	Mesh make_triangle();
 }
diff --git a/fggl/ecs/ecs.hpp b/fggl/ecs/ecs.hpp
index 077cd12c9c74d3f332abce22c6e10aa0719d1bb7..e62b6836bd46a38264915f159714ec08cacae18e 100644
--- a/fggl/ecs/ecs.hpp
+++ b/fggl/ecs/ecs.hpp
@@ -163,7 +163,7 @@ namespace fggl::ecs {
 			void removeComponent(const entity_t& entityId);
 
 			template<class C>
-			bool hasComponent(const entity_t& entityId) {
+			bool hasComponent(const entity_t& entityId) const {
 				const component_type_t componentID = Component<C>::typeID();
 				const auto* arch = m_entityArchtypes.at(entityId).archetype;
 				if ( arch == nullptr ) {
@@ -192,8 +192,27 @@ namespace fggl::ecs {
 				return nullptr;
 			}
 
+			template<class C>
+			const C* getComponent(const entity_t& entityId) const {
+                assert( hasComponent<C>( entityId ) );
+                const auto type = Component<C>::typeID();
+
+                const ComponentBase* const newComp = m_componentMap.at(type);
+                const auto record = m_entityArchtypes.at(entityId);
+                const auto* arch = record.archetype;
+
+                // JWR: linear search... seems a little suspect, they're ordered after all
+                for ( std::size_t i=0; i < arch->type.size(); ++i ) {
+                    if ( arch->type[i] == type ) {
+                        return reinterpret_cast<C*>(& (arch->data[i][ record.index * newComp->size() ]) );
+                    }
+                }
+
+                return nullptr;
+			}
+
 			template<class... Cs>
-			std::vector<entity_t> getEntityWith() {
+			std::vector<entity_t> getEntityWith() const {
 				// construct the key
 				archToken_t key;
 				(key.push_back( Component<Cs>::typeID() ), ...);
diff --git a/fggl/gfx/ogl.cpp b/fggl/gfx/ogl.cpp
index f28395627eb6b717b26e03c0fe6127406f57392f..8e84ccaf885899f954122c208393ab2abc97e632 100644
--- a/fggl/gfx/ogl.cpp
+++ b/fggl/gfx/ogl.cpp
@@ -4,7 +4,7 @@
 
 using namespace fggl::gfx;
 
-Graphics::Graphics(Window& window) : m_window(window) {
+GlGraphics::GlGraphics(Window& window) : m_window(window) {
 	window.activate();
 	GLenum err = glewInit();
 	if ( GLEW_OK != err ) {
@@ -12,10 +12,10 @@ Graphics::Graphics(Window& window) : m_window(window) {
 	}
 }
 
-Graphics::~Graphics() {
+GlGraphics::~GlGraphics() {
 }
 
-void Graphics::clear() {
+void GlGraphics::clear() {
 	m_window.activate();
 
 	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
diff --git a/fggl/gfx/ogl.hpp b/fggl/gfx/ogl.hpp
index 58c417efef2609618a5f23c94159aa0309726d96..0cc71387046c28988a34759825d3d140ffd49cf5 100644
--- a/fggl/gfx/ogl.hpp
+++ b/fggl/gfx/ogl.hpp
@@ -12,10 +12,10 @@
  */
 namespace fggl::gfx {
 
-	class Graphics {
+	class GlGraphics {
 		public:
-			Graphics(Window& window);
-			~Graphics();
+			GlGraphics(Window& window);
+			~GlGraphics();
 
 			void clear();
 
diff --git a/fggl/gfx/renderer.cpp b/fggl/gfx/renderer.cpp
index 96be4dca864a029372bb385010bdf67f15cd1803..9d90911ed4e02b4d17ec0e90c631f96ef49538fa 100644
--- a/fggl/gfx/renderer.cpp
+++ b/fggl/gfx/renderer.cpp
@@ -1,21 +1,85 @@
 #include <fggl/gfx/renderer.hpp>
 #include <fggl/gfx/rendering.hpp>
 
+#include <fggl/data/model.hpp>
+
+/**
+ * Future optimisations:
+ *   recommended approach is to group stuff in to as few vao as possible - this will do one vao per mesh, aka bad.
+ *   Add support for instanced rendering (particles)
+ *   Look at packing vertex data in better ways (with profiling)
+ *   Support shader specialisation (ie, dynamic/streamed data)
+ *   Follow best recommendations for Vertex attributes (ie normals not using floats, bytes for colour)
+ *
+ * Future features:
+ *   Add support for models with weights (for animations)
+ *   OpenGL ES for the FOSS thinkpad users who can't run anything even remotely modern
+ */
+ 
+
 using namespace fggl::gfx;
 
-static void gl_render_mesh(GLuint pid, GLuint vao, GLuint idxc) {
-	glUseProgram(pid);
-	glBindVertexArray(vao);
-	glDrawElements(GL_TRIANGLES, idxc, GL_UNSIGNED_INT, (void*)0);
+template<typename T>
+static GLuint createArrayBuffer(std::vector<T>& vertexData) {
+	GLuint buffId;
+	glGenBuffers(1, &buffId);
+	glBindBuffer(GL_ARRAY_BUFFER, buffId);
+	glBufferData(GL_ARRAY_BUFFER,
+			vertexData.size() * sizeof(T),
+			vertexData.data(),
+			GL_STATIC_DRAW);
+	return buffId;	
+}
+
+static GLuint createIndexBuffer(std::vector<uint32_t>& indexData) {
+	GLuint buffId;
+	glGenBuffers(1, &buffId);
+	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffId);
+	glBufferData(GL_ELEMENT_ARRAY_BUFFER,
+			indexData.size() * sizeof(uint32_t),
+			indexData.data(),
+			GL_STATIC_DRAW);
+	return buffId;
+}
+
+GlRenderToken MeshRenderer::upload(fggl::data::Mesh& mesh) {
+	GlRenderToken token;
+	glGenVertexArrays(1, &token.vao);
+	glBindVertexArray(token.vao);
+
+	token.buffs[0] = createArrayBuffer<fggl::data::Vertex>( mesh.vertexList() );
+	glEnableVertexAttribArray(0);
+	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(fggl::data::Vertex), reinterpret_cast<void*>(offsetof(fggl::data::Vertex, posititon)));
+	glEnableVertexAttribArray(1);
+	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(fggl::data::Vertex), reinterpret_cast<void*>(offsetof(fggl::data::Vertex, normal)));
+
+	token.buffs[1] = createIndexBuffer( mesh.indexList() );
+	token.idxOffset = 0;
+	token.idxSize = mesh.indexCount();
 	glBindVertexArray(0);
+
+	return token;
 }
 
-void MeshRenderer::render(const std::vector<data::Model> &models) {
-	for ( auto& model : models ) {
-		for ( auto& mesh : model.meshes() ) {
-			//TODO gl reneder mesh
-			//gl_render_mesh();
-		}
-	}
+void MeshRenderer::render(const Window& window, const fggl::ecs::ECS& ecs) {
+    auto entities = ecs.getEntityWith<GlRenderToken>();
+
+    // nothing to render, pointless doing anything else
+    if ( entities.size() == 0) return;
+
+    // make sure the correct rendering context is active
+    window.activate();
+
+    // TODO better performance if grouped by vao first
+    // TODO the nvidia performance presentation said I shouldn't use uniforms for large data
+    for ( auto& entity : entities ) {
+	    const auto& mesh = ecs.getComponent<GlRenderToken>(entity);
+
+	    glBindVertexArray( mesh->vao );
+	    glDrawElements( GL_TRIANGLES, mesh->idxSize, GL_UNSIGNED_INT, reinterpret_cast<void*>(mesh->idxOffset) );
+    }
+
+    glBindVertexArray(0);
+
 }
 
diff --git a/fggl/gfx/renderer.hpp b/fggl/gfx/renderer.hpp
index 69868bde6a705cbbcd1d2f517b3d7a7a1990ffdb..c8fcb6063832489efc13c16b551b57c3376f6c02 100644
--- a/fggl/gfx/renderer.hpp
+++ b/fggl/gfx/renderer.hpp
@@ -4,14 +4,34 @@
 #include <vector>
 
 #include <fggl/data/model.hpp>
+#include <fggl/ecs/ecs.hpp>
+#include <fggl/gfx/ogl.hpp>
 
 namespace fggl::gfx {
 
-	class MeshRenderer {
+	struct GlRenderToken {
+		GLuint vao;
+		GLuint buffs[2];
+		GLuint idxOffset;
+		GLuint idxSize;
+		GLuint pipeline;
+	};
+
+	class GlMeshRenderer {
 		public:
-			void render(const std::vector<data::Model>& models);
+			using token_t = GlRenderToken;
+
+			token_t upload(fggl::data::Mesh& mesh);
+
+			// are VAO safe across opengl contexts? :S
+			void render(const Window& window, const fggl::ecs::ECS& ecs);
 	};
 
+	// specialisation hooks
+	using Graphics = GlGraphics;
+	using MeshRenderer = GlMeshRenderer;
+	using MeshToken = GlRenderToken;
+
 };
 
 #endif
diff --git a/fggl/gfx/shader.cpp b/fggl/gfx/shader.cpp
index 3876f1f921f4addc93634c46a7846105561a70c0..7aa478cf1f5d852705943f800a1d48737d3d9f6b 100644
--- a/fggl/gfx/shader.cpp
+++ b/fggl/gfx/shader.cpp
@@ -129,7 +129,7 @@ GLuint ShaderCache::load(const ShaderConfig& config) {
 
 		// get the error
 		char infoLog[512];
-		glGetProgramInfoLog(pid, 512, NULL, infoLog);
+		glGetProgramInfoLog(pid, 512, nullptr, infoLog);
 		std::cerr << infoLog << std::endl;
 
 		// cleanup
@@ -177,7 +177,7 @@ bool ShaderCache::cacheLoad(GLuint pid, const BinaryCache* cache) {
 template<>
 bool fggl::data::fggl_deserialize(std::filesystem::path &data, fggl::gfx::BinaryCache *out) {
 	auto f = std::fopen( data.c_str(), "r");
-	if ( f == NULL ) {
+	if ( f == nullptr ) {
 		std::cerr << "fp was null..." << std::endl;
 		return false;
 	}
@@ -229,7 +229,7 @@ bool fggl::data::fggl_serialize(std::filesystem::path &data, const fggl::gfx::Bi
 
 	// try and write
 	auto f = std::fopen( data.c_str(), "w");
-	if ( f == NULL ){
+	if ( f == nullptr ){
 		return false;
 	}
 
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index 180444cb4f7195df93d6d9678c0edb9b5d6d878f..dd27fba658d325ad6324e2d24b240dbcf4d0336e 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -31,7 +31,7 @@ void Context::pollEvents() {
 }
 
 Window::Window() : m_window(nullptr), m_input(nullptr) {
-	m_window = glfwCreateWindow(1920, 1080, "main", NULL, NULL);
+	m_window = glfwCreateWindow(1920, 1080, "main", nullptr, nullptr);
 	glfwSetWindowUserPointer(m_window, this);
 }
 
@@ -42,7 +42,7 @@ Window::~Window() {
 	}
 }
 
-void Window::activate() {
+void Window::activate() const {
 	assert( m_window != nullptr );
 	glfwMakeContextCurrent(m_window);
 }
diff --git a/fggl/gfx/window.hpp b/fggl/gfx/window.hpp
index 66bf9e1e227d6141a6d79c27aec80c327f14043d..0afc6f1255d146cb92fe95bba54763ac77a89465 100644
--- a/fggl/gfx/window.hpp
+++ b/fggl/gfx/window.hpp
@@ -45,10 +45,11 @@ namespace fggl::gfx {
 			~Window();
 
 			// window <-> opengl stuff
-			void activate();
+			void activate() const;
 			void swap();
 
 			// window manager stuff
+			[[nodiscard]]
 			inline bool closeRequested() const {
 				assert( m_window != nullptr );
 				return glfwWindowShouldClose(m_window);
@@ -59,9 +60,10 @@ namespace fggl::gfx {
 				glfwSetWindowTitle( m_window, title.c_str() );
 			}
 
+			[[nodiscard]]
 			inline bool fullscreen() const {
 				assert( m_window != nullptr );
-				return glfwGetWindowMonitor( m_window ) != NULL;
+				return glfwGetWindowMonitor( m_window ) != nullptr;
 			}
 
 			inline void fullscreen(bool state) {
@@ -74,16 +76,18 @@ namespace fggl::gfx {
 							mode->width, mode->height,
 							mode->refreshRate);
 				} else {
-					glfwSetWindowMonitor( m_window, NULL,
+					glfwSetWindowMonitor( m_window, nullptr,
 							0, 0,
 							800, 600, 0);
 				}
 			}
 
+			[[nodiscard]]
 			inline bool checkHint(WindowHint hint) const {
 				return check_hint(hint);
 			}
 
+			[[nodiscard]]
 			inline bool checkHint(MutWindowHint hint) const {
 				return check_hint(hint);
 			}
@@ -115,6 +119,7 @@ namespace fggl::gfx {
 				glfwSetWindowAttrib( m_window, hint, state );
 			}
 
+			[[nodiscard]]
 			inline bool check_hint(int hint) const {
 				assert( m_window != nullptr );
 				return glfwGetWindowAttrib( m_window, hint) == GLFW_TRUE;
diff --git a/fggl/math/types.hpp b/fggl/math/types.hpp
index ae53de8bd59bfb5412788cb8a2d2796f45ec526e..d3c5b6e28192ebe02308fbd1c435d96af9d3eb8a 100644
--- a/fggl/math/types.hpp
+++ b/fggl/math/types.hpp
@@ -21,18 +21,21 @@ namespace fggl::math {
 
 	struct Transform {
 
-		Transform() : m_local(1.0f), m_origin(0.0f), m_rotation() {
+		Transform() : m_local(1.0f), m_origin(0.0f), m_model(), m_rotation() {
 		}
 
 		// local reference vectors
+		[[nodiscard]]
 		inline vec3 up() const {
 			return vec4( UP, 1.0 ) * m_local;
 		}
 
+		[[nodiscard]]
 		inline vec3 forward() const {
 			return vec4( FORWARD, 1.0 ) * m_local;
 		}
 
+		[[nodiscard]]
 		inline vec3 right() const {
 			return vec4( RIGHT, 1.0 ) * m_local;
 		}
@@ -46,7 +49,8 @@ namespace fggl::math {
 			m_origin = pos;
 			update();
 		}
-		
+
+		[[nodiscard]]
 		inline vec3 origin() const {
 			return m_origin;
 		}
@@ -56,6 +60,7 @@ namespace fggl::math {
 			update();
 		}
 
+		[[nodiscard]]
 		inline glm::vec3 euler() const {
 			return glm::eulerAngles(m_rotation);
 		}
diff --git a/tools/RenderDoc.cap b/tools/RenderDoc.cap
new file mode 100644
index 0000000000000000000000000000000000000000..acbc557f47b79b92361847a36e01ea9707eae4d9
--- /dev/null
+++ b/tools/RenderDoc.cap
@@ -0,0 +1,27 @@
+{
+    "rdocCaptureSettings": 1,
+    "settings": {
+        "autoStart": false,
+        "commandLine": "",
+        "environment": [
+        ],
+        "executable": "/home/webpigeon/Documents/gamedev/projects-cmake/build/demo/FgglDemo",
+        "inject": false,
+        "numQueuedFrames": 0,
+        "options": {
+            "allowFullscreen": true,
+            "allowVSync": true,
+            "apiValidation": false,
+            "captureAllCmdLists": false,
+            "captureCallstacks": false,
+            "captureCallstacksOnlyDraws": false,
+            "debugOutputMute": true,
+            "delayForDebugger": 0,
+            "hookIntoChildren": false,
+            "refAllResources": false,
+            "verifyBufferAccess": false
+        },
+        "queuedFrameCap": 0,
+        "workingDir": ""
+    }
+}