diff --git a/demo/main.cpp b/demo/main.cpp
index 25aa1c385be68fc7ec23d801170b0b428b5644e0..9fac445c47dd5a14b0729c8ad3a271e155a44c81 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -52,34 +52,14 @@ enum camera_type { cam_free, cam_arcball };
 camera_type cam_mode = cam_free;
 
 //TODO proper input system
-void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
+using namespace fggl::input;
+using InputManager = std::shared_ptr<fggl::input::Input>;
 
-	if ( glfwGetKey(window.handle(), GLFW_KEY_F2) == GLFW_PRESS ) {
-		cam_mode = cam_free; 
-	}
-	if ( glfwGetKey(window.handle(), GLFW_KEY_F3) == GLFW_PRESS ) {
-		cam_mode = cam_arcball;
-	}
-
-	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
-	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
-
-	const glm::vec3 dir = ( camTransform->origin() - camComp->target );
-	const glm::vec3 forward = glm::normalize( dir );
-
-	// scroll wheel
-	glm::vec3 motion(0.0f);
-	float delta = (float)input.scrollDeltaY();
-	if ( (glm::length( dir ) < 25.0f && delta < 0.0f) || (glm::length( dir ) > 2.5f && delta > 0.0f) )
-		motion -= (forward * delta);
-
-	camTransform->origin( camTransform->origin() + motion );
-}
-
-void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
+void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputManager input, fggl::ecs::entity_t cam) {
 	// see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
 	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
 	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
+	auto& mouse = input->mouse;
 
 	glm::vec4 position(camTransform->origin(), 1.0f);
 	glm::vec4 pivot(camComp->target, 1.0f);
@@ -89,8 +69,8 @@ void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::
 
 	float deltaAngleX = ( 2 * M_PI / window.width() ); 
 	float deltaAngleY = ( M_PI / window.height() );
-	float xAngle = ( input.cursorDeltaX() ) * deltaAngleX;
-	float yAngle = ( input.cursorDeltaY() ) * deltaAngleY;
+	float xAngle = ( -mouse.axisDelta(fggl::input::MouseAxis::X) ) * deltaAngleX;
+	float yAngle = ( -mouse.axisDelta(fggl::input::MouseAxis::Y) ) * deltaAngleY;
 
 	auto cosAngle = glm::dot( viewDir, fggl::math::UP );
 	if ( cosAngle * sgn(deltaAngleY) > 0.99f ) {
@@ -114,31 +94,40 @@ constexpr float ROT_SPEED = 0.05f;
 constexpr float PAN_SPEED = 0.05f;
 constexpr glm::mat4 MAT_IDENTITY(1.0f);
 
-void process_freecam(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
+
+void process_freecam(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputManager input, fggl::ecs::entity_t cam) {
 	float rotationValue = 0.0f;
 	glm::vec3 translation(0.0f);
 
+	auto& keyboard = input->keyboard;
+	auto code_q = glfwGetKeyScancode(GLFW_KEY_Q);
+	auto code_e = glfwGetKeyScancode(GLFW_KEY_E);
+	auto code_w = glfwGetKeyScancode(GLFW_KEY_W);
+	auto code_s = glfwGetKeyScancode(GLFW_KEY_S);
+	auto code_d = glfwGetKeyScancode(GLFW_KEY_D);
+	auto code_a = glfwGetKeyScancode(GLFW_KEY_A);
+
 	// calulate rotation (user input)
-	if ( glfwGetKey(window.handle(), GLFW_KEY_Q) == GLFW_PRESS ) {
+	if ( keyboard.down( code_q ) ) {
 		rotationValue = ROT_SPEED; 
-	} else if ( glfwGetKey(window.handle(), GLFW_KEY_E) == GLFW_PRESS ) {
+	} else if ( keyboard.down(code_e) ) {
 		rotationValue = -ROT_SPEED;
 	}
 
 	// calulate movement (user input)
-	if ( glfwGetKey(window.handle(), GLFW_KEY_W) == GLFW_PRESS ) {
+	if ( keyboard.down(code_w) ) {
 		translation -= fggl::math::RIGHT;
 	}
 
-	if ( glfwGetKey(window.handle(), GLFW_KEY_S) == GLFW_PRESS ) {
+	if ( keyboard.down(code_s) ) {
 		translation += fggl::math::RIGHT;
 	}
 
-	if ( glfwGetKey(window.handle(), GLFW_KEY_D) == GLFW_PRESS ) {
+	if ( keyboard.down(code_d) ) {
 		translation += fggl::math::FORWARD;
 	}
 
-	if ( glfwGetKey(window.handle(), GLFW_KEY_A) == GLFW_PRESS ) {
+	if ( keyboard.down(code_a) ) {
 		translation -= fggl::math::FORWARD;
 	}
 
@@ -172,14 +161,37 @@ void process_freecam(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::
 	camComp->target = pivot;
 }
 
+void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputManager input, fggl::ecs::entity_t cam) {
+	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
+	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
+
+	const glm::vec3 dir = ( camTransform->origin() - camComp->target );
+	const glm::vec3 forward = glm::normalize( dir );
+
+	// scroll wheel
+	glm::vec3 motion(0.0f);
+	float delta = input->mouse.axisDelta( fggl::input::MouseAxis::SCROLL_Y );
+	if ( (glm::length( dir ) < 25.0f && delta < 0.0f) || (glm::length( dir ) > 2.5f && delta > 0.0f) )
+		motion -= (forward * delta);
+
+	camTransform->origin( camTransform->origin() + motion );
+
+	if ( cam_mode == cam_arcball || input->mouse.button( fggl::input::MouseButton::MIDDLE ) ) { 
+		process_arcball(window, ecs, input, cam);
+	} else if ( cam_mode == cam_free ) {
+		process_freecam(window, ecs, input, cam);
+	}
+}
+
 int main(int argc, char* argv[]) {
 	// setup ECS
 	fggl::ecs::ECS ecs;
+	auto inputs = std::make_shared<fggl::input::Input>();
 
 	// build our main window
-	auto glfwModule = fggl::gfx::ecsInitGlfw(ecs);
+	auto glfwModule = fggl::gfx::ecsInitGlfw(ecs, inputs);
 
-	fggl::gfx::Window win( fggl::gfx::Input::instance() );
+	fggl::gfx::Window win;
 	win.title("FGGL Demo");
 	win.fullscreen( true );
 
@@ -253,9 +265,6 @@ int main(int argc, char* argv[]) {
 		fggl::gfx::onStaticMeshAdded(ecs, entity, glModule);
 	}
 
-	fggl::gfx::Input& input = fggl::gfx::Input::instance();
-
-
 	bool joystickWindow = true;
 	bool gamepadWindow = true;
 
@@ -269,8 +278,7 @@ int main(int argc, char* argv[]) {
 		// Setup setup
 		//
 		time.tick( glfwGetTimerValue() );
-
-		input.frame();
+		inputs->frame( time.delta() );
 
 		glfwModule->context.pollEvents();
 		debug.frameStart();
@@ -278,98 +286,36 @@ int main(int argc, char* argv[]) {
 		//
 		// update step
 		// 
-		process_camera(win, ecs, input, camEnt);
-		if ( cam_mode == cam_arcball ) { 
-			if ( input.mouseDown( fggl::gfx::MOUSE_2 ) ) {
-				process_arcball(win, ecs, input, camEnt);
-			}
-		} else if ( cam_mode == cam_free ) {
-			process_freecam(win, ecs, input, camEnt);
-		}
-
-		// imgui joystick debug
-		ImGui::Begin("Joysticks", &joystickWindow);
-		for ( int i=0; i<16; i++ ) {
-			bool present = input.hasJoystick(i);
-			std::string title = "Joystick " + std::to_string(i);
-
-			if ( ImGui::TreeNode(title.c_str()) ) {
-				ImGui::Text("present: %s", present ? "yes" : "no" );
-
-				if ( present ) {
-					const fggl::gfx::Joystick& joyData = input.joystick(i);
-					ImGui::Text( "%s", joyData.name );
-					ImGui::Text( "gamepad: %s", joyData.gamepad ? "yes" : "no" );
-					ImGui::Text( "axes: %d, buttons: %d, hats: %d",
-							joyData.axisCount,
-							joyData.buttonCount,
-							joyData.hatCount);
-
-					if (ImGui::TreeNode("axis##2")) {
-						// dump data
-						for ( int axid = 0; axid < joyData.axisCount; axid++ ) {
-							ImGui::Text("axis %d, value: %f", 
-									axid, joyData.axes[axid] );
-						}
-						ImGui::TreePop();
-					}
-
-					if (ImGui::TreeNode("buttons##2")) {
-						// dump data
-						for ( int btnid = 0; btnid < joyData.buttonCount; btnid++ ) {
-							ImGui::Text("button %d, value: %s", btnid,
-									joyData.buttons[btnid] == GLFW_PRESS
-									? "down" : "up" );
-						}
-						ImGui::TreePop();
-					}
-
-					if (ImGui::TreeNode("hats##2")) {
-						// dump data
-						for ( int btnid = 0; btnid < joyData.hatCount; btnid++ ) {
-							ImGui::Text("button %d, value: %d",
-									btnid, joyData.hats[btnid] );
-						}
-						ImGui::TreePop();
-					}
-				}
-
-				ImGui::TreePop();
-				ImGui::Separator();
-			}
-
-		}
-		ImGui::End();
+		process_camera(win, ecs, inputs, camEnt);
 
 		// imgui gamepad debug
+		auto& gamepads = inputs->gamepads;
 		ImGui::Begin("GamePad", &gamepadWindow);
 		for ( int i=0; i<16; i++ ) {
 			std::string title = "GamePad " + std::to_string(i);
 
-			bool present = input.hasJoystick(i);
+			bool present = gamepads.present(i);
 			if ( ImGui::TreeNode(title.c_str()) ) {
 				ImGui::Text("present: %s", present ? "yes" : "no" );
 
 				if ( present ) {
 
 					if ( ImGui::TreeNode("buttons##2") ) {
-						for ( auto& btn : fggl::gfx::PadButtons ) {
-							auto label = fggl::gfx::PadButtonLabels[ (int) btn ];
-							ImGui::Text( "%s: %i %i %i", label.c_str(), 
-									input.padDown(i, btn),
-									input.padPressed(i, btn),
-									input.padReleased(i, btn)
+						for ( auto& btn : fggl::input::GamepadButtonsMicrosoft ) {
+							ImGui::Text( "%s: %i %i %i", btn.name,
+									gamepads.button(i, btn.id),
+									gamepads.buttonPressed(i, btn.id),
+									gamepads.buttonReleased(i, btn.id)
 									);
 						}
 						ImGui::TreePop();
 					}
 
 					if ( ImGui::TreeNode("axes##2") ) {
-						for ( auto& axis : fggl::gfx::PadAxes ) {
-							auto label = fggl::gfx::PadAxisLabels[ (int) axis ];
-							ImGui::Text("%s: %f %f", label.c_str(),
-									input.padAxis(i, axis),
-									input.padAxisDelta(i, axis)
+						for ( auto& axis : fggl::input::GamepadAxes ) {
+							ImGui::Text("%s: %f %f", axis.name,
+									gamepads.axis(i, axis.id),
+									gamepads.axisDelta(i, axis.id)
 								   );
 
 						}
diff --git a/fggl/gfx/compat.hpp b/fggl/gfx/compat.hpp
index bc40713b576c27796915ac4c20e20884cc997a94..62b617d45b28980d036dad4fd2bf12825ecaf62a 100644
--- a/fggl/gfx/compat.hpp
+++ b/fggl/gfx/compat.hpp
@@ -23,7 +23,7 @@ namespace fggl::gfx {
 	struct ecsGlfwModule {
 		GlfwContext context;
 
-		inline ecsGlfwModule() : context() {
+		inline ecsGlfwModule(std::shared_ptr<fggl::input::Input> inputs) : context(inputs) {
 		}
 
 	};
@@ -32,8 +32,8 @@ namespace fggl::gfx {
 	//
 	// fake module/callbacks - our ECS doesn't have module/callback support yet.
 	// 
-	inline GlfwModule ecsInitGlfw(ecs::ECS& ecs) {
-		auto mod = std::make_shared<ecsGlfwModule>();
+	inline GlfwModule ecsInitGlfw(ecs::ECS& ecs, std::shared_ptr<fggl::input::Input> inputs) {
+		auto mod = std::make_shared<ecsGlfwModule>(inputs);
 		return mod;
 	}
 
diff --git a/fggl/gfx/window.cpp b/fggl/gfx/window.cpp
index b97e2095acfb8369b24de83383cad4ceb33d6b12..78ab5f904144131bf99fb64e542c6c267465503b 100644
--- a/fggl/gfx/window.cpp
+++ b/fggl/gfx/window.cpp
@@ -1,5 +1,6 @@
 
 #include "window.hpp"
+#include "window_input.hpp"
 
 #include <iostream>
 #include <string>
@@ -22,76 +23,86 @@ static void framebuffer_resize(GLFWwindow* window, int width, int height) {
 }
 
 static void fggl_input_cursor(GLFWwindow* window, double x, double y) {
-	Input& input = Input::instance();
-	input.mousePos(x, y);
+	auto& input = GlfwInputManager::instance();
+	input.onMouseMove(x, y);
 }
 
 static void fggl_input_scroll(GLFWwindow* window, double x, double y) {
-	Input& input = Input::instance();
-	input.mouseScroll(x, y);
+	auto& input = GlfwInputManager::instance();
+	input.onMouseScroll(x, y);
 }
 
 static void fggl_input_mouse_btn(GLFWwindow* window, int btn, int action, int mods) {
-	// as we need the singleton for joysticks, might as well use it for these as well...
-	Input& input = Input::instance();
-	fggl::gfx::MouseButton buttonBit = (fggl::gfx::MouseButton)(1 << btn);
-	input.mouseBtn( buttonBit, action == GLFW_PRESS );
-}
-
-static void fggl_activate_joystick(Input& input, int jid) {
-	fggl::gfx::Joystick data;
-	data.name = glfwGetJoystickName(jid);
-	data.axes = glfwGetJoystickAxes(jid, &data.axisCount);
-	data.buttons = glfwGetJoystickButtons(jid, &data.buttonCount);
-	data.hats = glfwGetJoystickHats(jid, &data.hatCount);
-	data.gamepad = glfwJoystickIsGamepad(jid);
-	input.joystickConnect(jid, data);
-
-	if ( data.gamepad ) {
+	auto& input = GlfwInputManager::instance();
+	input.onMouseButton(btn, action == GLFW_PRESS);
+}
+
+static void fggl_input_keyboard(GLFWwindow* window, int key, int scancode, int action, int mods) {
+	auto& input = GlfwInputManager::instance();
+	input.onKeyEvent( scancode, action == GLFW_PRESS || action == GLFW_REPEAT);
+}
+
+static void fggl_update_joystick(fggl::input::GamepadInput& input, int jid) {
+	//glfwGetJoystickName(jid);
+	bool isGamepad = glfwJoystickIsGamepad(jid);
+
+	if ( isGamepad ) {
+		if( !input.present(jid) ) {
+			input.setActive(jid, true);
+		}
+
 		GLFWgamepadstate state;
 		glfwGetGamepadState(jid, &state);
 
-		fggl::gfx::PadState padState;
+		fggl::input::GamepadState gamepadState;
 		for (int i=0; i<=GLFW_GAMEPAD_BUTTON_LAST; i++) {
-			padState.buttons[i] = state.buttons[i];
+			gamepadState.buttons[i] = state.buttons[i];
 		}
 
 		for (int i=0; i<=GLFW_GAMEPAD_AXIS_LAST; i++) {
-			padState.axes[i] = state.axes[i];
+			gamepadState.axes[i] = state.axes[i];
 		}
 
-		input.padState(jid, padState);
+		input.update(jid, gamepadState);
+	} else {
+		input.setActive(jid, false);
 	}
 }
 
-static void fggl_joystick_init() {
-	Input& input = Input::instance();
-	for (int jid=0; jid<16; jid++) {
-		if ( glfwJoystickPresent(jid) ) {
-			fggl_activate_joystick(input, jid);
-		}
+static void fggl_joystick_poll() {
+	auto& input = GlfwInputManager::instance();
+	if ( !input.alive() ) {
+		return;
 	}
-}
 
-static void fggl_joystick_poll() {
-	Input& input = Input::instance();
-	for (int jid=0; jid<16; jid++) {
+	auto& gamepadCtl = input.gamepads();
+	for (int jid=0; jid < GLFW_JOYSTICK_LAST; jid++) {
 		if ( glfwJoystickPresent(jid) ) {
-			fggl_activate_joystick(input, jid);
+			fggl_update_joystick(gamepadCtl, jid);
+		} else {
+			gamepadCtl.setActive(jid, false);
 		}
 	}
 }
 
 static void fggl_joystick(int jid, int state) {
-	Input& input = Input::instance();
+	auto& input = GlfwInputManager::instance();
+	if ( !input.alive() ) {
+		return;
+	}
+
+	auto& gamepadCtl = input.gamepads();
 	if ( state == GLFW_CONNECTED ) {
-		fggl_activate_joystick(input, jid);
-	} else if ( state == GLFW_DISCONNECTED ) {
-		input.joystickDisconnect(jid);
+		fggl_update_joystick( gamepadCtl, jid );
+	} else {
+		gamepadCtl.setActive(jid, false);
 	}
 }
 
-GlfwContext::GlfwContext() {
+GlfwContext::GlfwContext( std::shared_ptr<fggl::input::Input> input ) {
+	auto& glfwCallbacks = GlfwInputManager::instance();
+	glfwCallbacks.setup(input);
+
 	glfwInitHint(GLFW_JOYSTICK_HAT_BUTTONS, GLFW_FALSE);
 
 	int state = glfwInit();
@@ -102,7 +113,7 @@ GlfwContext::GlfwContext() {
 	}
 
 	// joysticks are global
-	fggl_joystick_init();
+	fggl_joystick_poll();
 	glfwSetJoystickCallback(fggl_joystick);
 }
 
@@ -115,7 +126,7 @@ void GlfwContext::pollEvents() {
 	fggl_joystick_poll();
 }
 
-Window::Window(Input& input) : m_window(nullptr), m_input(input) {
+Window::Window() : m_window(nullptr) {
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
 	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
 	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
@@ -134,6 +145,7 @@ Window::Window(Input& input) : m_window(nullptr), m_input(input) {
 	glfwSetScrollCallback(m_window, fggl_input_scroll);
 	glfwSetCursorPosCallback(m_window, fggl_input_cursor);
 	glfwSetMouseButtonCallback(m_window, fggl_input_mouse_btn);
+	glfwSetKeyCallback(m_window, fggl_input_keyboard);
 }
 
 Window::~Window() {
diff --git a/fggl/gfx/window.hpp b/fggl/gfx/window.hpp
index ebc5db23be8a16d0a911c03ae05fc1a59e7a1b60..d6bb8d9c63b56a607846cd13844fecdf856bc2a9 100644
--- a/fggl/gfx/window.hpp
+++ b/fggl/gfx/window.hpp
@@ -3,7 +3,9 @@
 
 #include <cassert>
 #include <string>
+#include <memory>
 
+#include <fggl/input/input.hpp>
 #include <fggl/gfx/common.hpp>
 #include <fggl/math/types.hpp>
 #include <fggl/gfx/input.hpp>
@@ -12,7 +14,7 @@ namespace fggl::gfx {
 
 	class GlfwContext {
 		public:
-			GlfwContext();
+			GlfwContext(std::shared_ptr<fggl::input::Input> input);
 			~GlfwContext();
 
 			void pollEvents();
@@ -39,7 +41,7 @@ namespace fggl::gfx {
 	};
 	class Window {
 		public:
-			Window(Input& input);
+			Window();
 			~Window();
 
 			// window <-> opengl stuff
@@ -120,17 +122,8 @@ namespace fggl::gfx {
 				return m_window;
 			}
 
-			inline Input& input() {
-				return m_input;
-			}
-
-/*			inline const Input* input() const {
-				return m_input;
-			}*/
-
 		private:
 			GLFWwindow* m_window;
-			Input& m_input;
 			math::vec2 m_framesize;
 
 			inline void set_hint(int hint, bool state) const {
diff --git a/fggl/gfx/window_input.hpp b/fggl/gfx/window_input.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6583efd28428beeca81b6027cb6411c894705a84
--- /dev/null
+++ b/fggl/gfx/window_input.hpp
@@ -0,0 +1,73 @@
+#ifndef FGGL_GFX_GLFW_INPUT_H
+#define FGGL_GFX_GLFW_INPUT_H
+
+#include <memory>
+#include <string>
+
+#include <fggl/gfx/window.hpp>
+#include <fggl/input/input.hpp>
+
+
+namespace fggl::gfx {
+
+	class GlfwInputManager {
+		public:
+			static GlfwInputManager& instance() {
+				static GlfwInputManager *instance = new GlfwInputManager();
+				return *instance;
+			}
+
+			inline void setup(std::shared_ptr<input::Input> input) {
+				m_inputs = input;
+			}
+
+			inline bool alive() {
+				return m_inputs != nullptr;
+			}
+
+			inline input::MouseInput& mouse() {
+				return m_inputs->mouse;
+			}
+
+			inline input::KeyboardInput& keyboard() {
+				return m_inputs->keyboard;
+			}
+
+			inline input::GamepadInput& gamepads() {
+				return m_inputs->gamepads;
+			}
+
+			inline void onMouseMove(float x, float y) {
+				if (m_inputs != nullptr ) {
+					m_inputs->mouse.axis(input::MouseAxis::X, x);
+					m_inputs->mouse.axis(input::MouseAxis::Y, y);
+				}
+			}
+
+			inline void onMouseScroll(float x, float y) {
+				if (m_inputs != nullptr) {
+					m_inputs->mouse.axis(input::MouseAxis::SCROLL_X, x);
+					m_inputs->mouse.axis(input::MouseAxis::SCROLL_Y, y);
+				}
+			}
+
+			inline void onMouseButton(int btn, bool state) {
+				if ( m_inputs != nullptr ) {
+					m_inputs->mouse.button((fggl::input::MouseButton)btn, state);
+				}
+			}
+
+			inline void onKeyEvent(int scancode, bool state) {
+				if ( m_inputs != nullptr ) {
+					m_inputs->keyboard.set(scancode, state);
+				}
+			}
+
+		private:
+			std::shared_ptr<input::Input> m_inputs;
+
+	};
+
+};
+
+#endif
diff --git a/fggl/input/gamepad.hpp b/fggl/input/gamepad.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..069e249fe81ad7c92f57fd53bc4a3fa40d16df28
--- /dev/null
+++ b/fggl/input/gamepad.hpp
@@ -0,0 +1,175 @@
+#ifndef FGGL_INPUT_GAMEPAD_H
+#define FGGL_INPUT_GAMEPAD_H
+
+#include <string>
+#include <array>
+#include <bitset>
+#include <cassert>
+
+namespace fggl::input {
+
+	enum class GamepadButton {
+		A,
+		B,
+		X,
+		Y,
+		BUMPER_LEFT,
+		BUMPER_RIGHT,
+		BACK,
+		START,
+		GUIDE,
+		THUMB_LEFT,
+		THUMB_RIGHT,
+		DPAD_UP,
+		DPAD_RIGHT,
+		DPAD_DOWN,
+		DPAD_LEFT,
+		SQUARE = 0,
+		CIRCLE = 1,
+		TRIANGLE = 2,
+		CROSS = 3
+	};
+
+	enum class GamepadAxis {
+		LEFT_X,
+		LEFT_Y,
+		RIGHT_X,
+		RIGHT_Y,
+		TRIGGER_LEFT,
+		TRIGGER_RIGHT
+	};
+
+	struct GamepadButtonRecord {
+		GamepadButton id;
+		char name[15];
+	};
+
+	struct GamepadAxisRecord {
+		GamepadAxis id;
+		char name[30];
+	};
+
+	constexpr std::array<GamepadButtonRecord, 15> GamepadButtonsMicrosoft = {{
+		{GamepadButton::A, "A"},
+		{GamepadButton::B, "B"},
+		{GamepadButton::X, "X"},
+		{GamepadButton::Y, "Y"},
+		{GamepadButton::BUMPER_LEFT,  "Left Bumper"},
+		{GamepadButton::BUMPER_RIGHT, "Right Bumper"},
+		{GamepadButton::BACK, "Back"},
+		{GamepadButton::START, "Start"},
+		{GamepadButton::GUIDE, "Guide"},
+		{GamepadButton::THUMB_LEFT, "Thumb Left"},
+		{GamepadButton::THUMB_RIGHT, "Thumb Right"},
+		{GamepadButton::DPAD_UP, "Dpad Up"},
+		{GamepadButton::DPAD_RIGHT, "Dpad Right"},
+		{GamepadButton::DPAD_DOWN, "Dpad Down"},
+		{GamepadButton::DPAD_LEFT, "Dpad Left"},
+	}};
+
+	constexpr std::array<GamepadAxisRecord, 6> GamepadAxes = {{
+		{GamepadAxis::LEFT_X, "left stick left/right"},
+		{GamepadAxis::LEFT_Y, "left stick up/down"},
+		{GamepadAxis::RIGHT_X, "right stick left/right"},
+		{GamepadAxis::RIGHT_Y, "right stick up/down"},
+		{GamepadAxis::TRIGGER_LEFT, "left trigger"},
+		{GamepadAxis::TRIGGER_RIGHT, "right trigger"},
+	}};
+
+	constexpr std::array<GamepadButtonRecord, 15> GamepadButtonsPlaystation = {{
+		{GamepadButton::SQUARE, "Square"},
+		{GamepadButton::CIRCLE, "Circle"},
+		{GamepadButton::TRIANGLE, "Triangle"},
+		{GamepadButton::CROSS, "Cross"},
+		{GamepadButton::BUMPER_LEFT,  "Left Bumper"},
+		{GamepadButton::BUMPER_RIGHT, "Right Bumper"},
+		{GamepadButton::BACK, "Back"},
+		{GamepadButton::START, "Start"},
+		{GamepadButton::GUIDE, "Guide"},
+		{GamepadButton::THUMB_LEFT, "Thumb Left"},
+		{GamepadButton::THUMB_RIGHT, "Thumb Right"},
+		{GamepadButton::DPAD_UP, "Dpad Up"},
+		{GamepadButton::DPAD_RIGHT, "Dpad Right"},
+		{GamepadButton::DPAD_DOWN, "Dpad Down"},
+		{GamepadButton::DPAD_LEFT, "Dpad Left"},
+	}};
+
+	struct GamepadState {
+		std::bitset<GamepadButtonsMicrosoft.size()> buttons;
+		float axes[GamepadAxes.size()];
+	};
+
+	constexpr size_t MaxControllers = 16;
+
+	struct GamepadInput {
+		public:
+			inline bool present(size_t id) const {
+				assert( id < MaxControllers );
+				return m_active[id];
+			}
+
+			inline float axis(size_t id, GamepadAxis axis) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return m_current[id].axes[(int)axis];
+			}
+
+			inline float axisDelta(size_t id, GamepadAxis axis) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return m_current[id].axes[(int)axis] - m_previous[id].axes[(int)axis];
+			}
+
+			inline bool button(size_t id, GamepadButton btn) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return m_current[id].buttons[(int)btn];
+			}
+
+			inline bool buttonPressed(size_t id, GamepadButton btn) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return m_current[id].buttons[(int)btn] && !m_previous[id].buttons[(int)btn];
+			}
+
+			inline bool buttonReleased(size_t id, GamepadButton btn) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return !m_current[id].buttons[(int)btn] && m_previous[id].buttons[(int)btn];
+			}
+
+			inline bool buttonChanged(size_t id, GamepadButton btn) const {
+				if ( !present(id) ) {
+					return 0.0f;
+				}
+				return m_current[id].buttons[(int)btn] != m_previous[id].buttons[(int)btn];
+			}
+
+			inline void frame(float dt) {
+				m_previous = m_current;
+			}
+
+			inline void update(size_t id, const GamepadState& state) {
+				assert( present(id) );
+				m_current[id] = state;
+			}
+
+			inline void setActive(size_t id, bool state) {
+				assert( id < MaxControllers );
+				m_active[id] = state;
+			}
+
+		private:
+			std::bitset<MaxControllers> m_active;
+			std::array<GamepadState, MaxControllers> m_current;
+			std::array<GamepadState, MaxControllers> m_previous;
+	};
+
+};
+
+#endif
diff --git a/fggl/input/input.hpp b/fggl/input/input.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d57033937c703b96cb3942f844f63a974cb473f4
--- /dev/null
+++ b/fggl/input/input.hpp
@@ -0,0 +1,29 @@
+#ifndef FGGL_INPUT_H
+#define FGGL_INPUT_H
+
+#include <memory>
+#include <fggl/input/keyboard.hpp>
+#include <fggl/input/mouse.hpp>
+#include <fggl/input/gamepad.hpp>
+
+namespace fggl::input {
+
+	class Input {
+		public:
+			inline Input() : keyboard(), mouse(), gamepads() {
+			}
+
+			inline void frame(float dt) {
+				keyboard.frame(dt);
+				mouse.frame(dt);
+				gamepads.frame(dt);
+			}
+
+			KeyboardInput keyboard;
+			MouseInput mouse;
+			GamepadInput gamepads;
+	};
+
+}
+
+#endif
diff --git a/fggl/input/keyboard.hpp b/fggl/input/keyboard.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4f6e828f1f0c3be361732d5c8a20f2773363e01
--- /dev/null
+++ b/fggl/input/keyboard.hpp
@@ -0,0 +1,64 @@
+#ifndef FGGL_INPUT_KEYBOARD_H
+#define FGGL_INPUT_KEYBOARD_H
+
+#include <array>
+#include <vector>
+#include <set>
+#include <iostream>
+
+namespace fggl::input {
+	using scancode_t = int;
+
+	struct KeyboardState {
+		std::set<scancode_t> m_keys;
+
+		inline void clear() {
+			m_keys.clear();
+		}
+
+		inline void set(scancode_t code, bool state) {
+			if ( state ) {
+				m_keys.insert( code );
+			} else {
+				m_keys.erase( code );
+			}
+		}
+
+		inline bool down(scancode_t scancode) const {
+			if ( m_keys.empty() )
+				return false;
+
+			return m_keys.find(scancode) != m_keys.end();
+		}
+	};
+
+	class KeyboardInput {
+		public:
+			inline void frame(float dt) {
+				m_prev = m_curr;
+			}
+
+			inline void set(scancode_t scancode, bool state) {
+				m_curr.set(scancode, state);
+			}
+
+			inline bool down(scancode_t scancode) const {
+				return m_curr.down(scancode);
+			}
+
+			inline bool pressed(scancode_t scancode) const {
+				return m_curr.down(scancode) && !m_prev.down(scancode);
+			}
+
+			inline bool released(scancode_t scancode) const {
+				return !m_curr.down(scancode) && m_prev.down(scancode);
+			}
+
+		private:
+			KeyboardState m_curr;
+			KeyboardState m_prev;
+	};
+
+};
+
+#endif
diff --git a/fggl/input/mouse.hpp b/fggl/input/mouse.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d99160b9b15e61072eeede3102bbf6c4802d032f
--- /dev/null
+++ b/fggl/input/mouse.hpp
@@ -0,0 +1,102 @@
+#ifndef FGGL_INPUT_MOUSE_H
+#define FGGL_INPUT_MOUSE_H
+
+#include <string>
+#include <array>
+#include <bitset>
+
+namespace fggl::input {
+
+	enum class MouseButton {
+		LEFT,
+		MIDDLE,
+		RIGHT,
+		EXTRA_1,
+		EXTRA_2,
+		EXTRA_3,
+		EXTRA_4,
+		EXTRA_5
+	};
+
+	enum class MouseAxis {
+		X,
+		Y,
+		SCROLL_X,
+		SCROLL_Y
+	};
+
+	struct MouseButtonRecord {
+		MouseButton id;
+		char name[15];
+	};
+
+	struct MouseAxisRecord {
+		MouseAxis id;
+		char name[15];
+	};
+
+	constexpr std::array<MouseButtonRecord, 8> MouseButtons = {{
+		{MouseButton::LEFT, "Left"},
+		{MouseButton::MIDDLE, "Middle"},
+		{MouseButton::RIGHT, "Right"},
+		{MouseButton::EXTRA_1, "Extra 1"},
+		{MouseButton::EXTRA_2, "Extra 2"},
+		{MouseButton::EXTRA_3, "Extra 3"},
+		{MouseButton::EXTRA_4, "Extra 4"},
+		{MouseButton::EXTRA_5, "Extra 5"},
+	}};
+
+	constexpr std::array<MouseAxisRecord, 4> MouseAxes = {{
+		{MouseAxis::X, "Left/Right"},
+		{MouseAxis::Y, "Up/Down"},
+		{MouseAxis::SCROLL_X, "Scroll X"},
+		{MouseAxis::SCROLL_Y, "Scroll Y"}
+	}};
+
+	struct MouseState {
+		float axis[MouseAxes.size()];
+		std::bitset<MouseButtons.size()> buttons;
+	};
+
+	class MouseInput {
+		public:
+			inline void frame(float dt) {
+				m_prev = m_curr;
+			}
+
+			inline void axis(MouseAxis axis, float value) {
+				m_curr.axis[(int)axis] = value;
+			}
+
+			inline float axis(MouseAxis axis) const {
+				return m_curr.axis[(int)axis];
+			}
+
+			inline float axisDelta(MouseAxis axis) const {
+				return m_curr.axis[(int)axis] - m_prev.axis[(int)axis];
+			}
+
+			inline void button(MouseButton btn, bool state) {
+				m_curr.buttons[(int)btn] = state;
+			}
+
+			inline bool button(MouseButton btn) const {
+				return m_curr.buttons[(int)btn];
+			}
+
+			inline bool pressed(MouseButton btn) const {
+				return m_curr.buttons[(int)btn] && !m_prev.buttons[(int)btn];
+			}
+
+			inline bool released(MouseButton btn) const {
+				return !m_curr.buttons[(int)btn] && m_prev.buttons[(int)btn];
+			}
+
+		private:
+			MouseState m_curr;
+			MouseState m_prev;
+	};
+
+};
+
+#endif