diff --git a/build.sh b/build.sh
index 3f75f29674e9eb5bf125c48d4ac7b4f292f39241..9530b5d70bc35277b9659b6d2f95f93f9e674b4b 100755
--- a/build.sh
+++ b/build.sh
@@ -43,6 +43,8 @@ popd
 # additional stuff
 #
 
+EXE="gdb $EXE"
+
 # gamemoderun
 if [ -x "$(command -v gamemoderun)" ]; then
 	EXE="gamemoderun $EXE"
diff --git a/demo/data/shader2D_frag.glsl b/demo/data/shader2D_frag.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..6d4fafa8f51abb0ef8ee365a8598e70f12bcb876
--- /dev/null
+++ b/demo/data/shader2D_frag.glsl
@@ -0,0 +1,9 @@
+#version 330 core
+
+in vec3 colour;
+out vec4 FragColor;
+
+void main()
+{
+    FragColor = vec4(colour, 1.0f);
+} 
diff --git a/demo/data/shader2D_vert.glsl b/demo/data/shader2D_vert.glsl
new file mode 100644
index 0000000000000000000000000000000000000000..223a72b07d65b8b5eae73a207bf661b0ebba5436
--- /dev/null
+++ b/demo/data/shader2D_vert.glsl
@@ -0,0 +1,13 @@
+#version 330 core
+layout (location = 0) in vec2 aPos;
+layout (location = 1) in vec3 aColour;
+
+uniform mat4 projection;
+
+out vec3 colour;
+
+void main()
+{
+    gl_Position = projection * vec4(aPos, 0.0f, 1.0f);
+    colour = aColour;
+}
diff --git a/demo/main.cpp b/demo/main.cpp
index 281b687a6dfa8dcda8bf13c47b28ee7fe8ac5125..857a63d35247ae8dc672e17a088cba4462f9758b 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -5,6 +5,8 @@
 #include <utility>
 
 #include <fggl/app.hpp>
+#include <fggl/scenes/menu.hpp>
+
 #include <fggl/gfx/window.hpp>
 #include <fggl/gfx/camera.hpp>
 #include <fggl/input/camera_input.h>
@@ -139,10 +141,10 @@ private:
 
 };
 
-class GameScene : public fggl::scenes::Scene, public fggl::AppState {
+class GameScene : public fggl::AppState {
 public:
     explicit GameScene(fggl::App& app) : fggl::AppState(app), m_world(nullptr), m_sceneTime(nullptr), m_inputs(nullptr) { };
-    ~GameScene() override = default;
+    //~GameScene() override = default;
 
     void activate() override {
         auto& locator = fggl::util::ServiceLocator::instance();
@@ -158,7 +160,7 @@ public:
         setup();
     }
 
-    void setup() override {
+    void setup() {
         auto types = m_world->types();
 
         // create camera using strings
@@ -269,11 +271,6 @@ public:
     }
 
     void deactivate() override {
-        cleanup();
-    }
-
-    void cleanup() override {
-
     }
 
     void update() override {
@@ -286,7 +283,7 @@ public:
         }
     }
 
-    void render() override {
+    void render(fggl::gfx::Paint& paint) override {
         debugInspector();
         fggl::gfx::renderMeshes(glModule, *m_world, m_sceneTime->delta());
     }
@@ -379,19 +376,14 @@ int main(int argc, const char* argv[]) {
 
     // -- should not be our problem - this is a broken api
     auto window = windowing.createWindow("Demo Game");
+    window->make_graphics<fggl::gfx::OpenGL4>();
     window->fullscreen( true );
-    // --
-
-    auto& graphics = app.use<fggl::gfx::ecsOpenGLModule>(window, storage);
-
-	// -- should not be out problem - this is a broken api
-	fggl::gfx::loadPipeline(glModule, "unlit", false);
-	fggl::gfx::loadPipeline(glModule, "phong", false);
-	fggl::gfx::loadPipeline(glModule, "normals", false);
-    // --
+    app.setWindow( std::move(window) );
 
     // and now our states
-    app.add_state<MenuScene>("menu");
+    auto menu = app.add_state<fggl::scenes::BasicMenu>("menu");
+
+    // game state
     app.add_state<GameScene>("game");
 
 
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index dc239001cedd25ee7096dcbf5c2fce4f41b0872e..20a631910e3a52191e8db22e71278ee7f104f7a1 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -17,6 +17,7 @@ target_sources(${PROJECT_NAME}
     ecs3/fast/Container.cpp
     ecs3/prototype/world.cpp
     scenes/Scene.cpp
+    scenes/menu.cpp
     ecs3/module/module.cpp
     input/camera_input.cpp
     data/heightmap.cpp
diff --git a/fggl/app.cpp b/fggl/app.cpp
index 918c3d7814151f798eff232853e05f7f69fbc3ee..11124cd096e7a25a60bae8b9c99afdff4107eba7 100644
--- a/fggl/app.cpp
+++ b/fggl/app.cpp
@@ -2,6 +2,8 @@
 #include <cstdlib>
 #include <memory>
 
+#include <spdlog/spdlog.h>
+
 #include <fggl/app.hpp>
 #include <fggl/ecs3/types.hpp>
 #include <fggl/ecs3/module/module.h>
@@ -16,8 +18,7 @@ namespace fggl {
         m_running(true),
         m_types(std::make_unique<ecs3::TypeRegistry>()),
         m_modules(std::make_unique<ecs3::ModuleManager>(*m_types)),
-        m_states() {
-    }
+        m_states() {}
 
     int App::run(int argc, const char** argv) {
 
@@ -29,9 +30,25 @@ namespace fggl {
 
         while ( m_running ) {
             auto& state = m_states.active();
-
+            
             state.update();
-            state.render();
+
+            // window rendering to frame buffer
+            if ( m_window != nullptr ) {
+                m_modules->onFrameStart();
+                m_window->frameStart();
+
+                // get draw instructions
+                fggl::gfx::Paint paint;
+                state.render(paint);
+
+                // execute draw instructions
+                auto& graphics = m_window->graphics();
+                graphics.draw2D( paint );
+
+                m_window->frameEnd();
+                m_modules->onFrameEnd();
+            }
         }
 
         {
diff --git a/fggl/debug/debug.cpp b/fggl/debug/debug.cpp
index 94b2571a41bf8bedc7495fc2e5f14f8a94f467e3..debd1e434eb5ef9baf2f5a70fbf7aea277f59d89 100644
--- a/fggl/debug/debug.cpp
+++ b/fggl/debug/debug.cpp
@@ -7,7 +7,7 @@
 using fggl::gfx::Window;
 using fggl::debug::DebugUI;
 
-DebugUI::DebugUI(std::shared_ptr<Window>& win) : m_visible(false) {
+DebugUI::DebugUI(std::shared_ptr<fggl::gfx::GlfwWindow>& win) : m_visible(false) {
 	IMGUI_CHECKVERSION();
 	ImGui::CreateContext();
 
diff --git a/fggl/ecs3/module/module.cpp b/fggl/ecs3/module/module.cpp
index 9494c917d4ca1c77efc1d26f2bfe8205a932b50b..d1fd59c33a49a37fb135116b1ed5c47aab3b6d2c 100644
--- a/fggl/ecs3/module/module.cpp
+++ b/fggl/ecs3/module/module.cpp
@@ -3,3 +3,12 @@
 //
 
 #include <fggl/ecs3/module/module.h>
+
+namespace fggl::ecs3 {
+
+    void Module::onFrameStart() {
+    }
+
+    void Module::onFrameEnd() {
+    }
+}
diff --git a/fggl/gfx/ogl/backend.cpp b/fggl/gfx/ogl/backend.cpp
index 96a9c97988357c78d69cbf0dc7dffa4a6e67e440..99b06318b7ea7f2f730170bfa09bf6e5bbca9d1f 100644
--- a/fggl/gfx/ogl/backend.cpp
+++ b/fggl/gfx/ogl/backend.cpp
@@ -5,9 +5,9 @@
 
 using namespace fggl::gfx;
 
-GlGraphics::GlGraphics(const std::shared_ptr<Window> window) : m_window(window) {
+GlGraphics::GlGraphics(const Window& window) {
     spdlog::debug("[OGL] attaching window context");
-	window->activate();
+	window.activate();
 	GLenum err = glewInit();
 	if ( GLEW_OK != err ) {
 		throw std::runtime_error("couldn't init glew");
@@ -22,8 +22,6 @@ GlGraphics::~GlGraphics() {
 }
 
 void GlGraphics::clear() {
-	m_window->activate();
-
 	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
 }
diff --git a/fggl/gfx/ogl/renderer.cpp b/fggl/gfx/ogl/renderer.cpp
index f35a1a87a061fdb960769f23ed51b6fb98e325a4..b063dd395dc327a7f13d4f80bc16752cd0bd672e 100644
--- a/fggl/gfx/ogl/renderer.cpp
+++ b/fggl/gfx/ogl/renderer.cpp
@@ -2,12 +2,19 @@
 #include <spdlog/spdlog.h>
 
 #include <fggl/gfx/ogl/renderer.hpp>
+#include <fggl/gfx/paint.hpp>
+
+#include <fggl/math/triangulation.hpp>
 #include <fggl/data/model.hpp>
+#include <fggl/util/service.h>
 
 #include <fggl/gfx/camera.hpp>
+
+#include <glm/ext/matrix_clip_space.hpp>
 #include <glm/ext/matrix_transform.hpp>
 #include <glm/glm.hpp>
 #include <glm/gtc/type_ptr.hpp>
+#include <memory>
 
 
 /**
@@ -22,9 +29,126 @@
  *   Add support for models with weights (for animations)
  *   OpenGL ES for the FOSS thinkpad users who can't run anything even remotely modern
  */
- 
 namespace fggl::gfx {
 
+    using data::Vertex2D;
+    using data::Mesh2D;
+
+    GlRenderToken setupVertex2D() {
+        GlRenderToken token{};
+        glGenVertexArrays(1, &token.vao);
+        glBindVertexArray(token.vao);
+
+	    glGenBuffers(2, token.buffs);
+
+	    glBindBuffer(GL_ARRAY_BUFFER, token.buffs[0]);
+        glEnableVertexAttribArray(0);
+        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), reinterpret_cast<void*>(offsetof(Vertex2D, position)));
+        glEnableVertexAttribArray(1);
+        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), reinterpret_cast<void*>(offsetof(Vertex2D, colour)));
+
+        glBindVertexArray(0);
+        return token;
+    }
+
+    OpenGL4Backend::OpenGL4Backend() : fggl::gfx::Graphics() {
+        GLenum err = glewInit();
+        if ( GLEW_OK != err ) {
+            throw std::runtime_error("couldn't init glew");
+        }
+        glViewport(0, 0, 2560, 1440);
+
+        m_token2D = setupVertex2D();
+
+        auto& locator = util::ServiceLocator::instance();
+
+        auto storage = locator.get<data::Storage>();
+        m_cache = std::make_unique<ShaderCache>(storage);
+
+        ShaderConfig shader2DConfig = ShaderFromName( "shader2D" );
+        m_cache->load( shader2DConfig );
+    };
+
+    void OpenGL4Backend::clear() {
+        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+        glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
+    }
+
+    static void generateMesh( const gfx::Paint& paint, Mesh2D& mesh ) {
+        for ( 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
+                    } else if ( verts.size() == 3 ) {
+                        // triangle
+                    } 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 );
+
+        // 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);
+    }
+
 template<typename T>
 static GLuint createArrayBuffer(std::vector<T>& vertexData) {
 	GLuint buffId;
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index 6a45c5cc1c04cc2227c82766fa834cff060bd1ea..199ec90eeaeb5fdb8d1be7ca3238ba91c468e634 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -5,6 +5,7 @@
 #include <string>
 #include <stdexcept>
 #include <utility>
+#include <GLFW/glfw3.h>
 #include <spdlog/spdlog.h>
 
 using namespace fggl::gfx;
@@ -64,13 +65,13 @@ static void framebuffer_resize(GLFWwindow* window, int width, int height) {
 	glfwMakeContextCurrent( window );
 	glViewport(0, 0, width, height);
 
-	auto fgglWindow = reinterpret_cast<Window*>(glfwGetWindowUserPointer( window ));
+	auto fgglWindow = reinterpret_cast<GlfwWindow*>(glfwGetWindowUserPointer( window ));
 	fgglWindow->framesize( width, height );
 }
 
 static void fggl_input_cursor(GLFWwindow* window, double x, double y) {
 	auto& input = GlfwInputManager::instance();
-	auto fgglWin = (Window*)glfwGetWindowUserPointer(window);
+	auto fgglWin = (GlfwWindow*)glfwGetWindowUserPointer(window);
 
 	#ifndef FGGL_INPUT_SCREEN_COORDS
 		// convert to nice ranges...
@@ -186,7 +187,7 @@ void GlfwContext::pollEvents() {
 	fggl_joystick_poll();
 }
 
-Window::Window() : m_window(nullptr), m_framesize() {
+GlfwWindow::GlfwWindow() : m_window(nullptr), m_framesize() {
     spdlog::debug("[glfw] creating window");
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
@@ -223,19 +224,27 @@ Window::Window() : m_window(nullptr), m_framesize() {
     spdlog::debug("[glfw] window creation complete");
 }
 
-Window::~Window() {
+GlfwWindow::~GlfwWindow() {
 	if ( m_window != nullptr ) {
 		glfwDestroyWindow(m_window);
 		m_window = nullptr;
 	}
 }
 
-void Window::activate() const {
-	assert( m_window != nullptr );
-	glfwMakeContextCurrent(m_window);
+void GlfwWindow::frameStart() {
+    assert( m_window != nullptr );
+    assert( m_graphics != nullptr );
+
+    glfwMakeContextCurrent(m_window);
+    m_graphics->clear();
 }
 
-void Window::swap() {
-	assert( m_window != nullptr );
+void GlfwWindow::frameEnd() {
 	glfwSwapBuffers(m_window);
 }
+
+void GlfwWindow::activate() const {
+	assert( m_window != nullptr );
+	glfwMakeContextCurrent(m_window);
+}
+
diff --git a/fggl/scenes/menu.cpp b/fggl/scenes/menu.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d99dc3c0fb3921e97ec390b5c6ef5d425d3be6d1
--- /dev/null
+++ b/fggl/scenes/menu.cpp
@@ -0,0 +1,199 @@
+#include <fggl/scenes/menu.hpp>
+
+namespace fggl::scenes {
+
+    static void buttonBorder( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size ) {
+        // outer box
+        path.colour( {1.0f, 0.0f, 0.0f} );
+        path.pathTo( { pos.x + size.x, pos.y } );
+        path.pathTo( { pos.x + size.x, pos.y + size.y } );
+        path.pathTo( { pos.x, pos.y + size.y } );
+        path.close();
+
+        // inner box
+        math::vec2 innerTop { pos.x + 5, pos.y + 5 };
+        math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
+
+        path.colour( {1.0f, 1.0f, 0.0f} );
+        path.moveTo( { innerTop.x, innerTop.y } );
+        path.pathTo( { innerBottom.x, innerTop.y } );
+        path.pathTo( { innerBottom.x, innerBottom.y } );
+        path.pathTo( { innerTop.x, innerBottom.y } );
+        path.pathTo( { innerTop.x, innerTop.y } );
+    }
+
+    static void makeBox( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 bottomRight ) {
+        path.moveTo( { topLeft.x, topLeft.y } );
+        path.pathTo( { bottomRight.x, topLeft.y } );
+        path.pathTo( { bottomRight.x, bottomRight.y } );
+        path.pathTo( { topLeft.x, bottomRight.y } );
+        path.pathTo( { topLeft.x, topLeft.y } );
+    }
+
+    static void makeProgress( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
+        const auto bottomRight { topLeft + size };
+
+        // background
+        path.colour( {0.5f, 0.5f, 0.5f} );
+        makeBox( path, topLeft, bottomRight );
+
+        // fill
+        math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
+        math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
+
+        // figure out how wide the bar should be
+        float barWidth = (innerBottom.x - innerTop.x) * value;
+        float trueBottom = innerBottom.x;
+        innerBottom.x = innerTop.x + barWidth;
+
+        // draw the bar
+        path.colour( {0.8f, 0.0f, 0.0f} );
+        makeBox( path, innerTop, innerBottom );
+
+        // part of the bar that's not filled in
+        math::vec2 emptyTop { innerBottom.x, innerTop.y };
+        math::vec2 emptyBottom { trueBottom, innerBottom.y };
+        path.colour( {0.4f, 0.0f, 0.0f} );
+        makeBox( path, emptyTop, emptyBottom );
+
+    }
+
+    static void makeSlider( gfx::Path2D& path, glm::vec2 topLeft, glm::vec2 size, float value ) {
+
+        makeProgress( path, topLeft, size, value );
+
+        // dimensions
+        const auto bottomRight { topLeft + size };
+        const math::vec2 innerTop { topLeft.x + 5, topLeft.y + 5 };
+        const math::vec2 innerBottom { bottomRight.x - 5, bottomRight.y - 5 };
+
+        // selector bar
+        float trackWidth = innerBottom.x - innerTop.x;
+        float selectorValue = trackWidth * value;
+        float selectorWidth = 6;
+
+        math::vec2 selectorTop { innerTop.x + selectorValue - ( selectorWidth/2), topLeft.y };
+        math::vec2 selectorBottom { selectorTop.x + selectorWidth, bottomRight.y };
+        path.colour( {1.0f, 1.0f, 1.0f} );
+        makeBox( path, selectorTop, selectorBottom );
+    }
+
+
+
+    static void makeButton( gfx::Path2D& path, glm::vec2 pos, glm::vec2 size, bool active, bool pressed) {
+        // locations
+        math::vec2 outerTop { pos };
+        math::vec2 outerBottom { pos + size };
+        math::vec2 innerTop { pos.x + 5, pos.y + 5 };
+        math::vec2 innerBottom { pos.x + size.x - 5, pos.y + size.y - 5 };
+
+        math::vec3 baseColour{ 0.5f, 0.5f, 0.5f };
+
+        if ( active ) {
+            baseColour *= 1.2f;
+        }
+
+        if ( pressed ) {
+            baseColour *= 0.8f;
+        }
+
+        math::vec3 lightColour{ baseColour * 1.2f };
+        math::vec3 darkColour{ baseColour * 0.8f };
+        if ( pressed ) {
+            // flip light and dark for selected buttons
+            auto tmp = darkColour;
+            darkColour = lightColour;
+            lightColour = tmp;
+        }
+
+        // bottom side
+        path.colour( darkColour );
+        path.moveTo( outerTop );
+        path.pathTo( innerTop );
+        path.pathTo( { innerBottom.x, innerTop.y } );
+        path.pathTo( { outerBottom.x, outerTop.y } );
+        path.pathTo( outerTop );
+
+        // left side
+        path.colour( darkColour );
+        path.moveTo( outerTop );
+        path.pathTo( innerTop );
+        path.pathTo( { innerTop.x, innerBottom.y } );
+        path.pathTo( { outerTop.x, outerBottom.y } );
+        path.pathTo( outerTop );
+
+        // top side
+        path.colour( lightColour );
+        path.moveTo( { outerTop.x, outerBottom.y} );
+        path.pathTo( { innerTop.x, innerBottom.y} );
+        path.pathTo( innerBottom );
+        path.pathTo( outerBottom );
+        path.pathTo( { outerTop.x, outerBottom.y}  );
+
+        // right side
+        path.colour( lightColour );
+        path.moveTo( outerBottom );
+        path.pathTo( innerBottom );
+        path.pathTo( { innerBottom.x, innerTop.y } );
+        path.pathTo( { outerBottom.x, outerTop.y } );
+        path.pathTo( outerBottom  );
+
+        // inner box
+        path.colour( baseColour );
+        makeBox( path, innerTop, innerBottom );
+    }
+
+    BasicMenu::BasicMenu(fggl::App& app) : AppState(app) {
+
+    }
+
+    void BasicMenu::update() {
+
+    }
+
+    void BasicMenu::render(gfx::Paint& paint) {
+
+
+        const math::vec2 btnSize{ 150.0f, 30.0f };
+        const float spacing = 5;
+
+        const float padX = 50.0f;
+        const float padY = 50.0f;
+
+        math::vec2 pos { 1920.0f - ( padX + btnSize.x ), padY };
+        for (int i=0; i<10; i++) {
+            gfx::Path2D btn( pos );
+            makeButton( btn, pos, btnSize, i==2, i==1 );
+            paint.fill( btn );
+            pos.y += (btnSize.y + spacing);
+        }
+
+        pos.x = padX;
+        pos.y = padY;
+        for ( int i = 0; i <= 10; i++ ) {
+            gfx::Path2D btn( pos );
+            makeProgress( btn, pos, btnSize, i / 10.f );
+            paint.fill( btn );
+
+            pos.y += (btnSize.y + spacing);
+        }
+
+        for ( int i = 0; i <= 10; i++ ) {
+            gfx::Path2D btn( pos );
+            makeSlider( btn, pos, btnSize, i / 10.0f );
+            paint.fill( btn );
+
+            pos.y += (btnSize.y + spacing);
+        }
+
+    }
+
+    void BasicMenu::activate() {
+
+    }
+
+    void BasicMenu::deactivate() {
+
+    }
+
+};
diff --git a/include/fggl/app.hpp b/include/fggl/app.hpp
index 964cfe597e9e9676fada4008e64c9108635b9491..34145abd67605a4f0d933b406684d3f50c318711 100644
--- a/include/fggl/app.hpp
+++ b/include/fggl/app.hpp
@@ -30,6 +30,8 @@
 
 #include <fggl/ecs3/types.hpp>
 #include <fggl/ecs3/module/module.h>
+#include <fggl/gfx/window.hpp>
+#include <fggl/gfx/paint.hpp>
 #include <fggl/util/states.hpp>
 
 namespace fggl {
@@ -72,7 +74,7 @@ namespace fggl {
              * It is not safe to assume the render target will always be the same, as the scene may be
              * rendered in mutliple passes (eg, for VR requirements).
              */
-            virtual void render() = 0;
+            virtual void render(gfx::Paint& paint) = 0;
 
             virtual void activate() {}
             virtual void deactivate() {}
@@ -90,15 +92,19 @@ namespace fggl {
             App(const App& app) = delete;
             App& operator=(App other) = delete;
 
+            inline void setWindow( std::unique_ptr<gfx::Window>&& window ) {
+                m_window = std::move(window);
+            }
+
             /**
              * Perform main game loop functions.
              */
             int run(int argc, const char** argv);
 
             template<typename T>
-            void add_state(const Identifer& name) {
+            T& add_state(const Identifer& name) {
                 static_assert( std::is_base_of<AppState,T>::value, "States must be AppStates");
-                m_states.put<T>(name, *this);
+                return m_states.put<T>(name, *this);
             }
 
             template<typename T, typename... Args>
@@ -129,6 +135,7 @@ namespace fggl {
             bool m_running;
             std::unique_ptr<ecs3::TypeRegistry> m_types;
             std::unique_ptr<ecs3::ModuleManager> m_modules;
+            std::unique_ptr<gfx::Window> m_window;
             AppMachine m_states;
     };
 
diff --git a/include/fggl/data/model.hpp b/include/fggl/data/model.hpp
index 88ef649b973f461330f34d9922e8a48ca1cbd797..955231ac9a5c162f495f1bb893117c64ba787d3a 100644
--- a/include/fggl/data/model.hpp
+++ b/include/fggl/data/model.hpp
@@ -13,6 +13,26 @@ namespace fggl::data {
 		math::vec3 colour;
 	};
 
+    struct Vertex2D{
+        fggl::math::vec2 position;
+        fggl::math::vec3 colour;
+    };
+
+    struct Mesh2D {
+        std::vector<Vertex2D> vertexList;
+        std::vector<uint32_t> indexList;
+
+        inline std::size_t add_vertex(const Vertex2D& vertex) {
+            vertexList.push_back( vertex );
+            return vertexList.size() - 1;
+        }
+
+        inline void add_index(uint32_t idx) {
+            indexList.push_back( idx );
+        }
+    };
+
+
 	// comparison operators
 
 	inline bool operator<(const Vertex& lhs, const Vertex& rhs) {
diff --git a/include/fggl/debug/debug.h b/include/fggl/debug/debug.h
index 0b75b81f3113026da7684aa7b1bc510635bd0296..24577331993b48de15464db4e874a6f811787e7e 100644
--- a/include/fggl/debug/debug.h
+++ b/include/fggl/debug/debug.h
@@ -18,7 +18,7 @@ namespace fggl::debug {
 
 	class DebugUI {
 		public:
-			explicit DebugUI(std::shared_ptr<gfx::Window>& window);
+			explicit DebugUI(std::shared_ptr<gfx::GlfwWindow>& window);
 			~DebugUI();
 
 			void frameStart();
diff --git a/include/fggl/ecs3/module/module.h b/include/fggl/ecs3/module/module.h
index 255cea9c1a6c7084aceb97df3bf43db0d9af4e0f..b38d1e3441695e8d1b3b060e8dc8704a1594ad24 100644
--- a/include/fggl/ecs3/module/module.h
+++ b/include/fggl/ecs3/module/module.h
@@ -20,6 +20,9 @@ namespace fggl::ecs3 {
             [[nodiscard]] virtual std::string name() const = 0;
 
             virtual void onLoad(ModuleManager& manager, TypeRegistry& tr) {};
+
+            virtual void onFrameStart();
+            virtual void onFrameEnd();
     };
 
     class ModuleManager {
@@ -42,6 +45,18 @@ namespace fggl::ecs3 {
                 m_types.callbackAdd( Component<C>::typeID(), cb);
             }
 
+            void onFrameStart() {
+                for ( auto& [id,ptr] : m_modules ) {
+                    ptr->onFrameStart();
+                }
+            }
+
+            void onFrameEnd() {
+                for ( auto& [id,ptr] : m_modules ) {
+                    ptr->onFrameEnd();
+                }
+            }
+
         private:
             TypeRegistry& m_types;
             std::map<std::string, std::shared_ptr<Module>> m_modules;
diff --git a/include/fggl/gfx/compat.hpp b/include/fggl/gfx/compat.hpp
index b7933526ed94f0a3f02c74853c989beda24a51df..d1fcfa04329cec66d5ae38547abf65f097944851 100644
--- a/include/fggl/gfx/compat.hpp
+++ b/include/fggl/gfx/compat.hpp
@@ -29,12 +29,16 @@ namespace fggl::gfx {
 		}
 
         inline
-        std::shared_ptr<Window> createWindow(const std::string& title) {
-            auto window = std::make_shared<Window>();
+        std::unique_ptr<GlfwWindow> createWindow(const std::string& title) {
+            auto window = std::make_unique<GlfwWindow>();
             window->title(title);
             return window;
         }
 
+        void onFrameStart() override {
+            context.pollEvents();
+        }
+
         [[nodiscard]]
         std::string name() const override {
             return "gfx::glfw";
diff --git a/include/fggl/gfx/ogl/backend.hpp b/include/fggl/gfx/ogl/backend.hpp
index 96b863e0d410f1505d0e87249fc0f4f0bf8bf45b..c30f2cbdfdbf47f9892a0da1d287fbc815a8afba 100644
--- a/include/fggl/gfx/ogl/backend.hpp
+++ b/include/fggl/gfx/ogl/backend.hpp
@@ -14,13 +14,10 @@ namespace fggl::gfx {
 
 	class GlGraphics {
 		public:
-			GlGraphics(const std::shared_ptr<Window> window);
+			GlGraphics(const Window& window);
 			~GlGraphics();
 
 			void clear();
-
-		private:
-			std::shared_ptr<Window> m_window;
 	};
 
 	class Shader {
diff --git a/include/fggl/gfx/ogl/compat.hpp b/include/fggl/gfx/ogl/compat.hpp
index 19741fab830b0e92c6ff70d346b55996b49c3d58..fe09fd0e57c239e6ecd8ca877206aa86e65655a0 100644
--- a/include/fggl/gfx/ogl/compat.hpp
+++ b/include/fggl/gfx/ogl/compat.hpp
@@ -30,12 +30,12 @@ namespace fggl::gfx {
 	// fake module support - allows us to still RAII
 	//
 	struct ecsOpenGLModule : ecs3::Module {
-		fggl::gfx::Graphics ogl;
 		fggl::gfx::MeshRenderer renderer;
 		fggl::gfx::ShaderCache cache;
 
-		ecsOpenGLModule(std::shared_ptr<Window> window, std::shared_ptr<fggl::data::Storage> storage) :
-			ogl(std::move(window)), renderer(), cache(std::move(storage)) {	}
+		ecsOpenGLModule(Window& window, std::shared_ptr<fggl::data::Storage> storage) :
+            renderer(),
+            cache(std::move(storage)) {	}
 
         std::string name() const override {
             return "gfx::opengl";
diff --git a/include/fggl/gfx/ogl/renderer.hpp b/include/fggl/gfx/ogl/renderer.hpp
index fcbdb8d56c6125f14a72487bc1cb5ca0399358ca..aed7979f2bcff3a123b6a8a9387198bcd705c0c1 100644
--- a/include/fggl/gfx/ogl/renderer.hpp
+++ b/include/fggl/gfx/ogl/renderer.hpp
@@ -1,11 +1,17 @@
 #ifndef FGGL_GFX_RENDERER_H
 #define FGGL_GFX_RENDERER_H
 
+#include <memory>
 #include <vector>
 
 #include <fggl/data/model.hpp>
 #include <fggl/ecs3/ecs.hpp>
+
 #include <fggl/gfx/ogl/backend.hpp>
+#include <fggl/gfx/ogl/shader.hpp>
+
+#include <fggl/gfx/renderer.hpp>
+#include <fggl/gfx/paint.hpp>
 
 namespace fggl::gfx {
 
@@ -35,8 +41,23 @@ namespace fggl::gfx {
 			float total;
 	};
 
+    class OpenGL4Backend : public Graphics {
+        public:
+            OpenGL4Backend();
+            ~OpenGL4Backend() override = default;
+
+            void clear() override;
+
+            void draw2D(const Paint& paint) override;
+
+        private:
+            GlRenderToken m_token2D;
+            std::unique_ptr<ShaderCache> m_cache;
+    };
+
+    using OpenGL4 = OpenGL4Backend;
+
 	// specialisation hooks
-	using Graphics = GlGraphics;
 	using MeshRenderer = GlMeshRenderer;
 	using MeshToken = GlRenderToken;
 
diff --git a/include/fggl/gfx/ogl/shader.hpp b/include/fggl/gfx/ogl/shader.hpp
index b7e0d48bb1a6b70e25323e3bb95aed323451362f..c74d89f7baf52fee5b52910c84d6cb763301766b 100644
--- a/include/fggl/gfx/ogl/shader.hpp
+++ b/include/fggl/gfx/ogl/shader.hpp
@@ -22,6 +22,14 @@ namespace fggl::gfx {
 		bool hasGeom = false;
 	};
 
+    inline ShaderConfig ShaderFromName( const std::string& name ) {
+        return {
+            .name = name,
+            .vertex = name + "_vert.glsl",
+            .fragment = name + "_frag.glsl"
+        };
+    }
+
 	struct BinaryCache {
 		void* data = nullptr;
 		GLsizei size = 0;
diff --git a/include/fggl/gfx/paint.hpp b/include/fggl/gfx/paint.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..74ac95ede16bb7a50afaeba22885b55e28a5531c
--- /dev/null
+++ b/include/fggl/gfx/paint.hpp
@@ -0,0 +1,88 @@
+#ifndef FGGL_GFX_PAINT_H
+#define FGGL_GFX_PAINT_H
+
+#include <vector>
+#include <fggl/math/types.hpp>
+
+namespace fggl::gfx {
+
+    using RadianAngle = float;
+
+    enum class PathType {
+        MOVE,
+        PATH,
+        BAZIER2,
+        BAZIER3,
+        COLOUR,
+        CLOSE
+    };
+
+    struct Path2D {
+        inline explicit Path2D( math::vec2 start ) : m_points(), m_types() {
+            moveTo( start );
+        }
+
+        inline void moveTo(math::vec2 pos) {
+            m_points.push_back( pos );
+            m_types.push_back( PathType::MOVE );
+        }
+
+        inline void pathTo(math::vec2 pos) {
+            m_points.push_back( pos );
+            m_types.push_back( PathType::PATH );
+        }
+
+        void bezierTo(math::vec2 cp1, math::vec2 pos);
+        void bezierTo(math::vec2 cp1, math::vec2 cp2, math::vec2 pos);
+
+        void arc(math::vec2 center, float radius, RadianAngle startAngle, RadianAngle endAngle, bool ccw);
+
+        void colour(math::vec3 colour) {
+            m_colours.push_back( colour );
+            m_types.push_back( PathType::COLOUR );
+        }
+
+        void close() {
+            pathTo( m_points[0] );
+            m_types.push_back( PathType::CLOSE );
+        }
+
+        std::vector< math::vec2 > m_points;
+        std::vector< PathType > m_types;
+        std::vector< math::vec3 > m_colours;
+    };
+
+    enum class PaintType {
+        FILL,
+        STROKE
+    };
+
+    struct PaintCmd {
+        PaintType type;
+        Path2D path;
+    };
+
+    class Paint {
+        public:
+            Paint() = default;
+            Paint(Paint& paint) = delete;
+
+            inline void fill(Path2D& path) {
+                m_cmds.push_back( { PaintType::FILL, path } );
+            }
+
+            void stroke(Path2D& path) {
+                m_cmds.push_back( { PaintType::STROKE, path } );
+            }
+
+            const std::vector< PaintCmd >& cmds() const {
+                return m_cmds;
+            }
+
+        private:
+            std::vector< PaintCmd > m_cmds;
+    };
+
+}
+
+#endif
diff --git a/include/fggl/gfx/renderer.hpp b/include/fggl/gfx/renderer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c112cd2f9316746ea52f128fc740db6724c6dfcc
--- /dev/null
+++ b/include/fggl/gfx/renderer.hpp
@@ -0,0 +1,20 @@
+#include <functional>
+#include <memory>
+
+namespace fggl::gfx {
+
+    class RenderBackend {
+        public:
+            RenderBackend();
+            virtual ~RenderBackend() = default;
+
+            virtual void clear() = 0;
+            virtual void swap() = 0;
+
+    };
+
+    using RenderBackendPtr = std::unique_ptr<RenderBackend>;
+    using RenderBackendFactory = std::function< fggl::gfx::RenderBackendPtr&&() >;
+
+
+};
diff --git a/include/fggl/gfx/vector.hpp b/include/fggl/gfx/vector.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..979f9e2d85280923f545566c46885b6d0f3f554e
--- /dev/null
+++ b/include/fggl/gfx/vector.hpp
@@ -0,0 +1,20 @@
+#ifndef FGGL_GFX_VECTOR_H
+#define FGGL_GFX_VECTOR_H
+
+#include <fggl/math/types.hpp>
+#include <vector>
+
+namespace fggl::gfx {
+
+    struct Rectangle {
+        math::vec2 topLeft;
+        math::vec2 size;
+    };
+
+    struct Polygon {
+        std::vector<math::vec2> points;
+    };
+
+}
+
+#endif
diff --git a/include/fggl/gfx/vulkan/common.hpp b/include/fggl/gfx/vulkan/common.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8e22017609247c6e748444bdff8849c0e1dfe6cf
--- /dev/null
+++ b/include/fggl/gfx/vulkan/common.hpp
@@ -0,0 +1,6 @@
+#ifndef FGGL_GFX_VK_COMMON_H
+#define FGGL_GFX_VK_COMMON_H
+
+#include <vulkan/vulkan.hpp>
+
+#endif
diff --git a/include/fggl/gfx/vulkan/vulkan.hpp b/include/fggl/gfx/vulkan/vulkan.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e32d6b3f612d306385fe11b0362d119d9e295d00
--- /dev/null
+++ b/include/fggl/gfx/vulkan/vulkan.hpp
@@ -0,0 +1,35 @@
+#ifndef FGGL_GFX_VK_CORE_H
+#define FGGL_GFX_VK_CORE_H
+
+#include <memory>
+
+#include <fggl/app.hpp>
+
+#include <vulkan/vulkan.hpp>
+#include <vulkan/vulkan_raii.hpp>
+
+namespace fggl::gfx::vkgfx {
+
+    constexpr char const* EngineName = "FGGL";
+
+    class VulkanGraphics {
+
+        private:
+            vk::raii::Instance m_instance;
+            std::unique_ptr<vk::raii::SwapchainKHR> m_swapchain;
+    };
+
+    class VulkanContext {
+        public:
+            VulkanContext();
+
+            VulkanGraphics& createGraphics();
+
+        private:
+            vk::raii::Context m_context;
+    };
+
+
+}
+
+#endif
diff --git a/include/fggl/gfx/window.hpp b/include/fggl/gfx/window.hpp
index f81224656eea180f34325ebe955e3bc7285e7d0e..fbb7900896d58d2fd75269f9f6296a9f62fbddf2 100644
--- a/include/fggl/gfx/window.hpp
+++ b/include/fggl/gfx/window.hpp
@@ -9,6 +9,7 @@
 #include <fggl/gfx/common.hpp>
 #include <fggl/math/types.hpp>
 #include <fggl/gfx/input.hpp>
+#include <fggl/gfx/windowing.hpp>
 
 namespace fggl::gfx {
 
@@ -39,15 +40,18 @@ namespace fggl::gfx {
 		GlDebugContext = GLFW_OPENGL_DEBUG_CONTEXT,
 		NoError = GLFW_CONTEXT_NO_ERROR
 	};
-	class Window {
+	class GlfwWindow : public Window {
 		public:
-			Window();
-			~Window();
-            Window(Window&) = delete;
+			GlfwWindow();
+			~GlfwWindow();
+            GlfwWindow(Window&) = delete;
+            GlfwWindow(Window&&) = delete;
 
 			// window <-> opengl stuff
-			void activate() const;
-			void swap();
+            void activate() const override;
+
+            void frameStart() override;
+            void frameEnd() override;
 
 			inline float width() const {
 				return m_framesize.x;
diff --git a/include/fggl/gfx/windowing.hpp b/include/fggl/gfx/windowing.hpp
index 71d9f0e28b5c8cd9d738bed50c3249e3d4f5f814..9f912a07a82a600e8adcde8b52eec59b5fae642b 100644
--- a/include/fggl/gfx/windowing.hpp
+++ b/include/fggl/gfx/windowing.hpp
@@ -3,24 +3,37 @@
 #define FGGL_GFX_WINDOWING_H
 
 #include <memory>
+#include <fggl/gfx/paint.hpp>
 
 namespace fggl::gfx {
 
     class Graphics {
         public:
-            virtual void clear();
+            virtual ~Graphics() = default;
+            virtual void clear() = 0;
+
+            virtual void draw2D(const Paint& paint) = 0;
     };
 
     class Window {
         public:
-            Window();
+            virtual ~Window() = default;
+            virtual void activate() const = 0;
 
             template<typename T>
             void make_graphics() {
+                activate();
                 m_graphics = std::make_unique<T>();
             }
 
-        private:
+            virtual void frameStart() = 0;
+            virtual void frameEnd() = 0;
+
+            Graphics& graphics() {
+                return *m_graphics;
+            }
+
+        protected:
             std::unique_ptr<Graphics> m_graphics;
     };
 
diff --git a/include/fggl/math/easing.hpp b/include/fggl/math/easing.hpp
index 81e484feb22752dd4832af944c18e411ab634e80..db53218384da48e18b8eeedffd6b62d4d6de9992 100644
--- a/include/fggl/math/easing.hpp
+++ b/include/fggl/math/easing.hpp
@@ -2,8 +2,8 @@
 // Created by webpigeon on 12/12/2021.
 //
 
-#ifndef FGGL_UTILS_HPP
-#define FGGL_UTILS_HPP
+#ifndef FGGL_MATH_EASING_H
+#define FGGL_MATH_EASING_H
 
 #include <fggl/math/types.hpp>
 
diff --git a/include/fggl/math/triangulation.hpp b/include/fggl/math/triangulation.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9861a1de15ab06f1354a67b4996ea3d0a2a9afb6
--- /dev/null
+++ b/include/fggl/math/triangulation.hpp
@@ -0,0 +1,164 @@
+#ifndef FGGL_MATH_TRIS_H
+#define FGGL_MATH_TRIS_H
+
+#include <fggl/math/types.hpp>
+#include <fggl/data/model.hpp>
+
+namespace fggl::math {
+
+    using Polygon = std::vector<math::vec2>;
+    using PolygonVertex = std::vector<data::Vertex2D>;
+
+    constexpr int POSITIVE = 1;
+    constexpr int NEGATIVE = -1;
+    constexpr int UNSET = 0;
+
+    /**
+     * Put an angle in the range [-PI, PI].
+     */
+    inline float clampAngle( float radianAngle ) {
+        if ( radianAngle <= M_PI ) {
+            return radianAngle + M_PI_2;
+        } else if ( radianAngle > M_PI ) {
+            return radianAngle - M_PI_2;
+        } else {
+            return radianAngle;
+        }
+    }
+
+    static void checkSign( float value, int& sign, int& firstSign, int& flips ) {
+        if ( value > 0 ) {
+            if ( sign == UNSET ) {
+                firstSign = POSITIVE;
+            } else if ( sign < 0 ) {
+                flips++;
+            }
+        } else if ( value < 0 ) {
+            if ( sign == UNSET ) {
+                firstSign = NEGATIVE;
+            } else if ( sign > 0 ) {
+                flips++;
+            }
+            sign = NEGATIVE;
+        }
+    }
+
+    /**
+     * Check if a polygon is convex.
+     *
+     * see https://math.stackexchange.com/a/1745427
+     */
+    bool isConvex(const Polygon& polygon) {
+        if ( polygon.size() < 3 ) {
+            return false;
+        }
+
+        const auto n = polygon.size();
+
+        auto wSign = UNSET;
+
+        auto xSign = UNSET;
+        auto xFirstSign = UNSET;
+        auto xFlips = 0;
+
+        auto ySign = UNSET;
+        auto yFirstSign = UNSET;
+        auto yFlips = 0;
+
+        auto curr = polygon[ n - 1 ];
+        auto next = polygon[ n ];
+
+        for ( auto& v : polygon ) {
+            auto prev = curr;
+            curr = next;
+            next = v;
+
+            auto before = curr - prev;
+            auto after = next - curr;
+
+            checkSign( after.x, xSign, xFirstSign, xFlips );
+            if ( xFlips > 2 ) {
+                return false;
+            }
+
+            checkSign( after.y, ySign, yFirstSign, yFlips );
+            if ( yFlips > 2 ) {
+                return false;
+            }
+
+            auto w = before.x * after.y - after.x * before.y;
+            if ( wSign == UNSET && w != 0 ) {
+                wSign = w;
+            } else if ( wSign > 0 && w < 0 ) {
+                return false;
+            } else if ( wSign < 0 && w > 0 ){
+                return false;
+            }
+        }
+
+        if ( xSign != UNSET && ( xFirstSign != UNSET ) && ( xSign != xFirstSign ) ) {
+            xFlips += 1;
+        }
+        if ( ySign != UNSET && ( yFirstSign != UNSET ) && ( ySign != yFirstSign ) ) {
+            yFlips += 1;
+        }
+        if ( xFlips != 2 || yFlips != 2 ) {
+            return false;
+        }
+
+        return true;
+    }
+
+    static data::Vertex2D pointToVertex( const math::vec2& point ) {
+        return data::Vertex2D{
+            .position = point,
+            .colour = {1.0f, 1.0f, 1.0f}
+        };
+    }
+
+    /**
+     * Fast Triangulation for convex polygons.
+     */
+    void fanTriangulation(const Polygon& polygon, data::Mesh2D& mesh) {
+        assert(polygon.size() >= 3);
+
+        // add the first two points to the mesh
+        auto firstIdx = mesh.add_vertex( pointToVertex(polygon[0]) );
+        auto prevIdx = mesh.add_vertex( pointToVertex(polygon[1]) );
+
+        // deal with the indicies
+        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( pointToVertex(polygon[i + 2]) );
+            mesh.add_index( currIdx );
+            prevIdx = currIdx;
+        }
+    }
+
+    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 indicies
+        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::util
+
+#endif
+
diff --git a/include/fggl/scenes/menu.hpp b/include/fggl/scenes/menu.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c36040195c0d8229a748c515095daf4dfba8d226
--- /dev/null
+++ b/include/fggl/scenes/menu.hpp
@@ -0,0 +1,25 @@
+#ifndef FGGL_SCENES_MENU_H
+#define FGGL_SCENES_MENU_H
+
+#include <fggl/app.hpp>
+#include <fggl/input/input.hpp>
+
+namespace fggl::scenes {
+
+    class BasicMenu : public AppState {
+        public:
+            BasicMenu(App& owner);
+
+            void update() override;
+            void render(gfx::Paint& paint) override;
+
+            void activate() override;
+            void deactivate() override;
+
+        private:
+            input::Input* m_inputs;
+    };
+
+} // namepace fggl::scenes
+
+#endif
diff --git a/include/fggl/util/service.h b/include/fggl/util/service.h
index 9d1c26108c224409a204bbbed41d2462d02629ae..4fe8bd83528897b6b8937979aec41249c7e56d11 100644
--- a/include/fggl/util/service.h
+++ b/include/fggl/util/service.h
@@ -34,6 +34,12 @@ namespace fggl::util {
             m_services[info] = ptr;
         }
 
+        template<typename T>
+        std::shared_ptr<T> get() {
+            auto info = std::type_index(typeid(T));
+            return std::static_pointer_cast<T>(m_services.at(info));
+        }
+
         template<typename T>
         std::shared_ptr<T> providePtr() {
             auto info = std::type_index(typeid(T));
diff --git a/include/fggl/util/states.hpp b/include/fggl/util/states.hpp
index 66ca6d3343d4ecc09272e9873abe89020cfb1b2b..a72fe314a483b4512046f67514f3db7dd4e1a736 100644
--- a/include/fggl/util/states.hpp
+++ b/include/fggl/util/states.hpp
@@ -42,7 +42,7 @@ namespace fggl::util {
             StateMachine& operator=(StateMachine other) = delete;
 
             template<typename T, typename... Args>
-            void put(const Identifer& name, Args&&... args) {
+            T& put(const Identifer& name, Args&&... args) {
                 static_assert( std::is_base_of<StateType,T>::value, "States must be AppStates");
                 m_states[name] = std::make_unique<T>( std::forward<Args...>(args...) );
 
@@ -50,6 +50,8 @@ namespace fggl::util {
                 if ( m_active.empty() ) {
                     m_active = name;
                 }
+
+                return *(T*)(m_states[name].get());
             }
 
             void change(const Identifer& name) {