summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/Entities/Player.cpp64
-rw-r--r--src/Entities/Player.hpp27
-rw-r--r--src/GFX/Mouse.cpp24
-rw-r--r--src/GFX/Mouse.hpp20
-rw-r--r--src/GFX/Window.cpp26
-rw-r--r--src/GFX/Window.hpp26
-rw-r--r--src/Game.cpp9
-rw-r--r--src/Input.cpp221
-rw-r--r--src/Input.hpp116
-rw-r--r--src/Time.cpp7
-rw-r--r--src/Time.hpp17
12 files changed, 429 insertions, 130 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 93976e2..5117ea3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -43,7 +43,6 @@ add_executable(meowcraft
     src/GFX/Camera.cpp src/GFX/Camera.hpp
     src/Math/Rotation.hpp
     src/GFX/Shading/Uniform.cpp src/GFX/Shading/Uniform.hpp
-    src/GFX/Mouse.cpp src/GFX/Mouse.hpp
     src/GFX/Texture.cpp src/GFX/Texture.hpp
     src/Assets.cpp src/Assets.hpp
     src/GFX/Image/RawImage.cpp src/GFX/Image/RawImage.hpp
@@ -87,6 +86,7 @@ add_executable(meowcraft
     src/GFX/Actions.hpp
     src/GFX/Resources.cpp src/GFX/Resources.hpp
     src/ThreadRole.hpp
+    src/Input.cpp src/Input.hpp
 )
 
 if (WIN32)
diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp
index 7a1c1af..83c3f5e 100644
--- a/src/Entities/Player.cpp
+++ b/src/Entities/Player.cpp
@@ -1,13 +1,13 @@
 #include "Player.hpp"
-#include "../Common/Casts.hpp"
+#include "../Input.hpp"
 #include <unordered_set>
 
 namespace MC::Entities {
 
-void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world) {
-    auto const input_direction = directional_input(window);
+void Player::update(Time const& time, Input const& input, GFX::Camera& camera, World::World& world) {
+    auto const input_direction = directional_input(input);
 
-    auto destination = movement(window, time, input_direction);
+    auto destination = movement(time, input_direction);
 
     if (can_collide()) {
         if (m_velocity.y() < 0.0)
@@ -25,12 +25,12 @@ void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera,
     }
 
     move_to(destination);
-    rotate(rotational_input(window));
+    rotate(rotational_input(input));
 
     update_camera_position(camera);
 
     update_targeted_block(world);
-    actions(window, world);
+    actions(input, world);
 }
 
 void Player::render(GFX::Actions& actions) {
@@ -72,17 +72,17 @@ Bool Player::can_collide() const {
     return m_movement != MovementMode::NoClip;
 }
 
-Position::World Player::movement(GFX::Window& window, const Time& time, Vec3 input_direction) {
+Position::World Player::movement(Time const& time, Vec3 input_direction) {
     switch (m_movement) {
-        case MovementMode::Walking: m_velocity = walking_velocity(window, time, input_direction); break;
-        case MovementMode::Flying: m_velocity = flying_velocity(window, time, input_direction); break;
-        case MovementMode::NoClip: m_velocity = noclip_velocity(window, time, input_direction); break;
+        case MovementMode::Walking: m_velocity = walking_velocity(time, input_direction); break;
+        case MovementMode::Flying: m_velocity = flying_velocity(time, input_direction); break;
+        case MovementMode::NoClip: m_velocity = noclip_velocity(time, input_direction); break;
     }
 
     return m_transform.position() + m_velocity;
 }
 
-Vec3 Player::walking_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) {
+Vec3 Player::walking_velocity(Time const& time, Vec3 input_direction) {
     constexpr auto base_move_speed = 8.0;
     constexpr auto initial_jump_velocity = 0.16;
     constexpr auto gravity = 0.6;
@@ -109,7 +109,7 @@ Vec3 Player::walking_velocity(GFX::Window& window, const Time& time, Vec3 input_
     };
 }
 
-Vec3 Player::flying_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) {
+Vec3 Player::flying_velocity(Time const& time, Vec3 input_direction) const {
     constexpr auto base_move_speed = 10.0;
 
     auto flying_direction = m_transform.right() * input_direction.x() + m_transform.forward() * input_direction.z();
@@ -126,15 +126,13 @@ Vec3 Player::flying_velocity(GFX::Window& window, const Time& time, Vec3 input_d
     };
 }
 
-Vec3 Player::noclip_velocity(GFX::Window& window, const Time& time, Vec3 input_direction) {
+Vec3 Player::noclip_velocity(Time const& time, Vec3 input_direction) const {
     constexpr auto base_move_speed = 10.0;
 
-    Real const boost = TO(Real, window.key(GLFW_KEY_LEFT_CONTROL, GLFW_PRESS)) * 75.0;
-
     auto const [x, y, z] = input_direction.elements;
     auto const direction = m_transform.right() * x + Vec3(0, y, 0) + m_transform.forward() * z;
 
-    return direction * (base_move_speed + boost) * time.delta();
+    return direction * base_move_speed * time.delta();
 }
 
 void Player::update_targeted_block(World::World& world) {
@@ -152,10 +150,10 @@ void Player::update_targeted_block(World::World& world) {
     else m_targeted_block = {};
 }
 
-void Player::actions(GFX::Window& window, World::World& world) {
+void Player::actions(Input const& input, World::World& world) {
     // Breaking and placing blocks.
-    auto left_click = window.mouse(GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS);
-    auto right_click = window.mouse(GLFW_MOUSE_BUTTON_RIGHT, GLFW_PRESS);
+    auto left_click = input.pressed(Mouse::Left);
+    auto right_click = input.pressed(Mouse::Right);
     if (left_click || right_click) {
         if (m_targeted_block.has_value()) {
             auto targeted_block = m_targeted_block.value();
@@ -173,11 +171,15 @@ void Player::actions(GFX::Window& window, World::World& world) {
     }
 
     // Toggle movement modes.
-    // TODO: Need an input system for this, can't get an event only at
-    // the moment the key is pressed, so can't toggle between modes on single press.
-    if (window.key(GLFW_KEY_1, GLFW_PRESS)) m_movement = MovementMode::Walking;
-    else if (window.key(GLFW_KEY_2, GLFW_PRESS)) m_movement = MovementMode::Flying;
-    else if (window.key(GLFW_KEY_3, GLFW_PRESS)) m_movement = MovementMode::NoClip;
+    if (input.double_pressed(Key::Space)) {
+        if (m_movement == MovementMode::Walking) m_movement = MovementMode::Flying;
+        else if (m_movement == MovementMode::Flying) m_movement = MovementMode::Walking;
+    }
+
+    if (input.pressed(Key::N)) {
+        if (m_movement == MovementMode::NoClip) m_movement = MovementMode::Walking;
+        else m_movement = MovementMode::NoClip;
+    }
 }
 
 #define STUCK_THRESHOLD 100
@@ -319,20 +321,18 @@ void Player::update_camera_position(GFX::Camera& camera) const {
     camera.set_angles(cam_transform.rotation());
 }
 
-Vec3 Player::directional_input(GFX::Window& window) {
-    auto key = [&](Int k) -> Real { return window.key(k, GLFW_PRESS); };
-
-    Real x = key(GLFW_KEY_D) - key(GLFW_KEY_A);
-    Real y = key(GLFW_KEY_SPACE) - key(GLFW_KEY_LEFT_SHIFT);
-    Real z = key(GLFW_KEY_S) - key(GLFW_KEY_W);
+Vec3 Player::directional_input(Input const& input) {
+    Real x = input.held(Key::D) - input.held(Key::A);
+    Real y = input.held(Key::Space) - input.held(Key::LeftShift);
+    Real z = input.held(Key::S) - input.held(Key::W);
 
     return {x, y, z};
 }
 
-Rotation Player::rotational_input(GFX::Window& window) {
+Rotation Player::rotational_input(Input const& input) {
     constexpr auto base_rotation_speed = 0.1f;
 
-    auto r = window.mouse_delta();
+    auto r = input.mouse_movement();
     return {r.y() * base_rotation_speed, r.x() * base_rotation_speed, 0.0f};
 }
 
diff --git a/src/Entities/Player.hpp b/src/Entities/Player.hpp
index 2b69d57..342d965 100644
--- a/src/Entities/Player.hpp
+++ b/src/Entities/Player.hpp
@@ -1,14 +1,15 @@
 #pragma once
 
+#include "../Common/Pure.hpp"
 #include "../Time.hpp"
 #include "../Transform.hpp"
 #include "../GFX/Actions.hpp"
 #include "../GFX/Camera.hpp"
 #include "../World/World.hpp"
-#include "../GFX/Window.hpp"
 #include "../Math/AABB.hpp"
 #include "../Math/Rotation.hpp"
 #include "../World/Position.hpp"
+#include "../Input.hpp"
 
 namespace MC::Entities {
 class Player {
@@ -16,9 +17,9 @@ public:
     explicit Player(Position::World position)
         : m_transform(position), m_outline_mesh(create_outline_cube_mesh()) {}
 
-    Position::World position() const { return m_transform.position(); }
+    PURE Position::World position() const { return m_transform.position(); }
 
-    void update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world);
+    void update(Time const& time, Input const& input, GFX::Camera& camera, World::World& world);
     void render(GFX::Actions& actions);
 
     void move(Position::WorldOffset by);
@@ -27,14 +28,14 @@ public:
     void rotate(Rotation by);
     void rotate_to(Rotation to);
 
-    AABB bounds() const;
+    PURE AABB bounds() const;
 
 private:
     struct BlockedAxis {
         Bool positive, negative;
     };
 
-    Bool can_collide() const;
+    PURE Bool can_collide() const;
 
     struct ProcessCollisionsResult {
         Position::World position;
@@ -45,19 +46,19 @@ private:
 
     Position::World rescue_from_void_on_new_chunk(Time const& time, World::World& world, Position::World position);
 
-    Position::World movement(GFX::Window& window, const Time& time, Vec3 input_direction);
-    Vec3 walking_velocity(GFX::Window& window, const Time& time, Vec3 input_direction);
-    Vec3 flying_velocity(GFX::Window& window, const Time& time, Vec3 input_direction);
-    Vec3 noclip_velocity(GFX::Window& window, const Time& time, Vec3 input_direction);
+    Position::World movement(Time const& time, Vec3 input_direction);
+    Vec3 walking_velocity(Time const& time, Vec3 input_direction);
+    PURE Vec3 flying_velocity(Time const& time, Vec3 input_direction) const;
+    PURE Vec3 noclip_velocity(Time const& time, Vec3 input_direction) const;
 
     void update_targeted_block(World::World& world);
-    void actions(GFX::Window& window, World::World& world);
+    void actions(Input const& input, World::World& world);
 
-    Transform camera_transform() const;
+    PURE Transform camera_transform() const;
     void update_camera_position(GFX::Camera& camera) const;
 
-    static Vec3 directional_input(GFX::Window& window);
-    static Rotation rotational_input(GFX::Window& window);
+    static Vec3 directional_input(Input const& input);
+    static Rotation rotational_input(Input const& input);
 
     // Creates a bounding box where `position` is at the center of the bottom face.
     static AABB bounding_box_for_position(Position::World position);
diff --git a/src/GFX/Mouse.cpp b/src/GFX/Mouse.cpp
deleted file mode 100644
index 49f6972..0000000
--- a/src/GFX/Mouse.cpp
+++ /dev/null
@@ -1,24 +0,0 @@
-#include "Mouse.hpp"
-
-namespace MC::GFX {
-
-Vector<2> Mouse::update(GLFWwindow* window) {
-    Real x, y;
-    glfwGetCursorPos(window, &x, &y);
-
-    if (m_first_event) {
-        m_last_x = x;
-        m_last_y = y;
-
-        m_first_event = false;
-    }
-
-    Vector<2> movement{static_cast<Real>(x) - m_last_x,  static_cast<Real>(y) - m_last_y};
-
-    m_last_x = x;
-    m_last_y = y;
-
-    return movement;
-}
-
-}
\ No newline at end of file
diff --git a/src/GFX/Mouse.hpp b/src/GFX/Mouse.hpp
deleted file mode 100644
index ad940d4..0000000
--- a/src/GFX/Mouse.hpp
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma once
-
-#include <GLFW/glfw3.h>
-#include "../Math/Vector.hpp"
-
-namespace MC::GFX {
-
-class Mouse {
-public:
-    Mouse() = default;
-
-    Vector<2> update(GLFWwindow* window);
-private:
-    Bool m_first_event = true;
-
-    Real m_last_x = 0.0f;
-    Real m_last_y = 0.0f;
-};
-
-}
\ No newline at end of file
diff --git a/src/GFX/Window.cpp b/src/GFX/Window.cpp
index 33e76d5..5c391c8 100644
--- a/src/GFX/Window.cpp
+++ b/src/GFX/Window.cpp
@@ -3,10 +3,12 @@
 #include "../Common/Assert.hpp"
 #include "../ThreadRole.hpp"
 #include "Window.hpp"
+#include "../Common/Casts.hpp"
+#include <string>
 
 namespace MC::GFX {
 
-Window::Window(const Char* title, U32 width, U32 height) {
+Window::Window(std::string const& title, U32 const width, U32 const height) {
     ASSERT_MAIN_THREAD();
     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
@@ -14,7 +16,7 @@ Window::Window(const Char* title, U32 width, U32 height) {
     glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
     glfwWindowHint(GLFW_DOUBLEBUFFER, GL_TRUE);
 
-    m_window = glfwCreateWindow(width, height, title, nullptr, nullptr);
+    m_window = glfwCreateWindow(TO(I32, width), TO(I32, height), title.c_str(), nullptr, nullptr);
     if (m_window == nullptr) {
         throw std::runtime_error("Failed to create window.");
     }
@@ -36,32 +38,20 @@ GLFWwindow* Window::get() const {
     return m_window;
 }
 
-void Window::close() {
+void Window::close() const {
     glfwSetWindowShouldClose(m_window, true);
 }
 
-Vector<2> Window::mouse_delta() {
-    return m_mouse.update(m_window);
-}
-
-Bool Window::key(I32 key, I32 type) const {
-    return glfwGetKey(m_window, key) == type;
-}
-
-Bool Window::mouse(I32 key, I32 type) const {
-    return glfwGetMouseButton(m_window, key) == type;
-}
-
-void Window::start_render() {
+void Window::start_render() const {
     glfwSwapBuffers(m_window);
 }
 
-void Window::poll_events() {
+void Window::poll_events() const {
     ASSERT_MAIN_THREAD();
     glfwPollEvents();
 }
 
-void Window::on_size_change(void (callback)(GLFWwindow*, I32, I32)) {
+void Window::on_size_change(void (callback)(GLFWwindow*, I32, I32)) const {
     glfwSetFramebufferSizeCallback(m_window, callback);
 }
 
diff --git a/src/GFX/Window.hpp b/src/GFX/Window.hpp
index c26b0fd..86920b5 100644
--- a/src/GFX/Window.hpp
+++ b/src/GFX/Window.hpp
@@ -1,35 +1,31 @@
 #pragma once
 
-#include "../Common/Sizes.hpp"
-#include "../Math/Vector.hpp"
+#include <string>
 #include <GLFW/glfw3.h>
-#include "Mouse.hpp"
+#include "../Common/Pure.hpp"
+#include "../Common/Sizes.hpp"
 
 namespace MC::GFX {
 
 class Window {
 public:
-    Window(const Char* title, U32 width, U32 height);
+    Window(std::string const& title, U32 width, U32 height);
     ~Window();
 
-    GLFWwindow* get() const;
+    PURE GLFWwindow* get() const;
 
-    void on_size_change(void (* callback)(GLFWwindow*, I32, I32));
+    void on_size_change(void (* callback)(GLFWwindow*, I32, I32)) const;
 
     void attach() const;
     void detach() const;
 
-    void close();
-    void start_render();
-    void poll_events();
-    Vector<2> mouse_delta();
+    void close() const;
+    void start_render() const;
+    void poll_events() const;
 
-    Bool key(I32 key, I32 type) const;
-    Bool mouse(I32 key, I32 type) const;
-    Bool should_close() const;
+    PURE Bool should_close() const;
 private:
     GLFWwindow* m_window;
-    Mouse m_mouse;
 };
 
-}
\ No newline at end of file
+}
diff --git a/src/Game.cpp b/src/Game.cpp
index 231c681..dd04b2f 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -21,6 +21,9 @@ void Game::run() const {
     World::Clouds clouds{};
     Entities::Player player{{0, World::Chunk::Height / 2.0, 0}};
 
+    Input::register_callbacks(m_window);
+
+    Input input;
     Time time;
 
     while (!m_window.should_close()) {
@@ -33,11 +36,13 @@ void Game::run() const {
 
         time.start_frame();
 
-        if (m_window.key(GLFW_KEY_ESCAPE, GLFW_PRESS)) {
+        input.update(time.frame_start());
+
+        if (input.pressed(Key::Escape)) {
             m_window.close();
         }
 
-        player.update(time, m_window, camera, world);
+        player.update(time, input, camera, world);
         clouds.update(time);
 
         GFX::Actions actions;
diff --git a/src/Input.cpp b/src/Input.cpp
new file mode 100644
index 0000000..2362794
--- /dev/null
+++ b/src/Input.cpp
@@ -0,0 +1,221 @@
+#include "Input.hpp"
+#include "ThreadRole.hpp"
+
+namespace MC {
+
+Bool Input::s_key_buffer[KeyCount] = { false };
+Bool Input::s_mouse_buffer[MouseCount] = { false };
+Vec2 Input::s_mouse_position = { 0, 0 };
+Input::MouseEventType Input::s_mouse_event_type = MouseEventType::None;
+
+void Input::register_callbacks(GFX::Window const& window) {
+    ASSERT_MAIN_THREAD();
+
+    glfwSetKeyCallback(window.get(), key_callback);
+    glfwSetCursorPosCallback(window.get(), cursor_position_callback);
+    glfwSetMouseButtonCallback(window.get(), mouse_button_callback);
+}
+
+// I think this is the Windows default for double click...
+#define DOUBLE_PRESS_DELTA_MS 500
+
+void Input::update(Time::Timestamp const current_time) {
+    ASSERT_MAIN_THREAD();
+
+    std::swap(m_state, m_previous_state);
+
+    for (auto& key : m_double_press->keys) key = false;
+    for (auto& mouse_button : m_double_press->mouse_buttons) mouse_button = false;
+
+    for (USize i = 0; i < KeyCount; ++i) {
+        m_state->map.keys[i] = s_key_buffer[i];
+        if (key_pressed(m_state, m_previous_state, TO(Key, i))) {
+            auto previous_release_time = m_last_key_release->keys[i];
+            m_double_press->keys[i] = current_time - previous_release_time < DOUBLE_PRESS_DELTA_MS;
+            m_last_key_release->keys[i] = current_time;
+        }
+    }
+
+    for (USize i = 0; i < MouseCount; ++i) {
+        m_state->map.mouse_buttons[i] = s_mouse_buffer[i];
+        if (mouse_button_pressed(m_state, m_previous_state, TO(Mouse, i))) {
+            auto previous_release_time = m_last_key_release->keys[i];
+            m_double_press->keys[i] = current_time - previous_release_time < DOUBLE_PRESS_DELTA_MS;
+            m_last_key_release->keys[i] = current_time;
+        }
+    }
+
+    if (s_mouse_event_type == MouseEventType::First)
+        m_previous_state->mouse_position = s_mouse_position;
+    m_state->mouse_position = s_mouse_position;
+}
+
+Bool Input::pressed(Key const key) const {
+    return key_pressed(m_state, m_previous_state, key);
+}
+
+Bool Input::pressed(Mouse const button) const {
+    return mouse_button_pressed(m_state, m_previous_state, button);
+}
+
+Bool Input::held(Key const key) const {
+    return m_state->map.keys[key_value(key)];
+}
+
+Bool Input::held(Mouse const button) const {
+    return m_state->map.mouse_buttons[mouse_button_value(button)];
+}
+
+Bool Input::released(Key const key) const {
+    return key_released(m_state, m_previous_state, key);
+}
+
+Bool Input::released(Mouse const button) const {
+    return mouse_button_released(m_state, m_previous_state, button);
+}
+
+Bool Input::double_pressed(Key const key) const {
+    return m_double_press->keys[key_value(key)];
+}
+
+Bool Input::double_pressed(Mouse const button) const {
+    return m_double_press->mouse_buttons[mouse_button_value(button)];
+}
+
+Vector<2> Input::mouse_position() const {
+    return m_state->mouse_position;
+}
+
+Vector<2> Input::mouse_movement() const {
+    return m_state->mouse_position - m_previous_state->mouse_position;
+}
+
+Bool Input::key_pressed(
+    std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Key key
+) {
+    return current->map.keys[key_value(key)] && !previous->map.keys[key_value(key)];
+}
+
+Bool Input::mouse_button_pressed(
+    std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Mouse mouse
+) {
+    return current->map.mouse_buttons[mouse_button_value(mouse)] && !previous->map.mouse_buttons[mouse_button_value(mouse)];
+}
+
+Bool Input::key_released(
+    std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Key key
+) {
+    return !current->map.keys[key_value(key)] && previous->map.keys[key_value(key)];
+}
+
+Bool Input::mouse_button_released(
+    std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Mouse mouse
+) {
+    return !current->map.mouse_buttons[mouse_button_value(mouse)] && previous->map.mouse_buttons[mouse_button_value(mouse)];
+}
+
+USize Input::key_value(Key key) {
+    return TO(USize, key);
+}
+
+USize Input::mouse_button_value(Mouse button) {
+    return TO(USize, button);
+}
+
+Key Input::from_glfw_key(I32 const key) {
+    switch (key) {
+        case GLFW_KEY_A: return Key::A;
+        case GLFW_KEY_B: return Key::B;
+        case GLFW_KEY_C: return Key::C;
+        case GLFW_KEY_D: return Key::D;
+        case GLFW_KEY_E: return Key::E;
+        case GLFW_KEY_F: return Key::F;
+        case GLFW_KEY_G: return Key::G;
+        case GLFW_KEY_H: return Key::H;
+        case GLFW_KEY_I: return Key::I;
+        case GLFW_KEY_J: return Key::J;
+        case GLFW_KEY_K: return Key::K;
+        case GLFW_KEY_L: return Key::L;
+        case GLFW_KEY_M: return Key::M;
+        case GLFW_KEY_N: return Key::N;
+        case GLFW_KEY_O: return Key::O;
+        case GLFW_KEY_P: return Key::P;
+        case GLFW_KEY_Q: return Key::Q;
+        case GLFW_KEY_R: return Key::R;
+        case GLFW_KEY_S: return Key::S;
+        case GLFW_KEY_T: return Key::T;
+        case GLFW_KEY_U: return Key::U;
+        case GLFW_KEY_V: return Key::V;
+        case GLFW_KEY_W: return Key::W;
+        case GLFW_KEY_X: return Key::X;
+        case GLFW_KEY_Y: return Key::Y;
+        case GLFW_KEY_Z: return Key::Z;
+        case GLFW_KEY_0: return Key::Zero;
+        case GLFW_KEY_1: return Key::One;
+        case GLFW_KEY_2: return Key::Two;
+        case GLFW_KEY_3: return Key::Three;
+        case GLFW_KEY_4: return Key::Four;
+        case GLFW_KEY_5: return Key::Five;
+        case GLFW_KEY_6: return Key::Six;
+        case GLFW_KEY_7: return Key::Seven;
+        case GLFW_KEY_8: return Key::Eight;
+        case GLFW_KEY_9: return Key::Nine;
+        case GLFW_KEY_ESCAPE: return Key::Escape;
+        case GLFW_KEY_ENTER: return Key::Enter;
+        case GLFW_KEY_TAB: return Key::Tab;
+        case GLFW_KEY_BACKSPACE: return Key::Backspace;
+        case GLFW_KEY_INSERT: return Key::Insert;
+        case GLFW_KEY_DELETE: return Key::Delete;
+        case GLFW_KEY_RIGHT: return Key::Right;
+        case GLFW_KEY_LEFT: return Key::Left;
+        case GLFW_KEY_DOWN: return Key::Down;
+        case GLFW_KEY_UP: return Key::Up;
+        case GLFW_KEY_LEFT_SHIFT: return Key::LeftShift;
+        case GLFW_KEY_RIGHT_SHIFT: return Key::RightShift;
+        case GLFW_KEY_LEFT_CONTROL: return Key::LeftControl;
+        case GLFW_KEY_RIGHT_CONTROL: return Key::RightControl;
+        case GLFW_KEY_LEFT_ALT: return Key::LeftAlt;
+        case GLFW_KEY_RIGHT_ALT: return Key::RightAlt;
+        case GLFW_KEY_SPACE: return Key::Space;
+        case GLFW_KEY_F1: return Key::F1;
+        case GLFW_KEY_F2: return Key::F2;
+        case GLFW_KEY_F3: return Key::F3;
+        case GLFW_KEY_F4: return Key::F4;
+        case GLFW_KEY_F5: return Key::F5;
+        case GLFW_KEY_F6: return Key::F6;
+        case GLFW_KEY_F7: return Key::F7;
+        case GLFW_KEY_F8: return Key::F8;
+        case GLFW_KEY_F9: return Key::F9;
+        case GLFW_KEY_F10: return Key::F10;
+        case GLFW_KEY_F11: return Key::F11;
+        case GLFW_KEY_F12: return Key::F12;
+        default: UNREACHABLE("Unknown key."); return TO(Key, 0);
+    }
+}
+
+Mouse Input::from_glfw_mouse(I32 const button) {
+    switch (button) {
+        case GLFW_MOUSE_BUTTON_LEFT: return Mouse::Left;
+        case GLFW_MOUSE_BUTTON_RIGHT: return Mouse::Right;
+        case GLFW_MOUSE_BUTTON_MIDDLE: return Mouse::Middle;
+        default: UNREACHABLE("Unknown mouse button."); return TO(Mouse, 0);
+    }
+}
+
+void Input::key_callback(GLFWwindow* _window, I32 const key, I32 _scancode, I32 const action, I32 _mods) {
+    if (action == GLFW_PRESS) s_key_buffer[key_value(from_glfw_key(key))] = true;
+    else if (action == GLFW_RELEASE) s_key_buffer[key_value(from_glfw_key(key))] = false;
+}
+
+void Input::cursor_position_callback(GLFWwindow* _window, F64 x, F64 y) {
+    if (s_mouse_event_type == MouseEventType::None) s_mouse_event_type = MouseEventType::First;
+    else if (s_mouse_event_type == MouseEventType::First) s_mouse_event_type = MouseEventType::Subsequent;
+    s_mouse_position = { x, y };
+}
+
+void Input::mouse_button_callback(GLFWwindow* _window, I32 const button, I32 const action, I32 _mods) {
+    if (action == GLFW_PRESS) s_mouse_buffer[mouse_button_value(from_glfw_mouse(button))] = true;
+    else if (action == GLFW_RELEASE) s_mouse_buffer[mouse_button_value(from_glfw_mouse(button))] = false;
+}
+
+}
diff --git a/src/Input.hpp b/src/Input.hpp
new file mode 100644
index 0000000..683ee55
--- /dev/null
+++ b/src/Input.hpp
@@ -0,0 +1,116 @@
+#pragma once
+
+#include "Time.hpp"
+#include "Common/Casts.hpp"
+#include "Common/Sizes.hpp"
+#include "Common/Pure.hpp"
+#include "GFX/Window.hpp"
+#include "Math/Vector.hpp"
+
+namespace MC {
+
+// TODO: Add some more keys.
+// TODO: I think some of these keys are "modifiers" under GLFW, and should be handled differently.
+enum class Key {
+    A, B, C, D, E, F, G, H, I, J, K, L, M,
+    N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
+    Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine,
+    Escape, Enter, Tab, Backspace, Insert, Delete, Right, Left, Down, Up,
+    LeftShift, RightShift, LeftControl, RightControl, LeftAlt, RightAlt,
+    Space, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
+};
+
+constexpr USize KeyCount = 65;
+static_assert(KeyCount == TO(USize, Key::F12) + 1, "Key count mismatch.");
+
+enum class Mouse {
+    Left, Right, Middle,
+};
+
+constexpr USize MouseCount = 3;
+static_assert(MouseCount == TO(USize, Mouse::Middle) + 1, "Mouse count mismatch.");
+
+// The simple input system.
+// All methods are thread safe, except the initial registration of the input callbacks
+// and the state update. Both must be called from the main thread.
+class Input {
+public:
+    explicit Input() = default;
+
+    // Registers the input callbacks for the given window.
+    // This function must be called from the main thread.
+    static void register_callbacks(GFX::Window const& window);
+
+    // Updates the input state from the callback buffers.
+    // This function must be called from the main thread.
+    void update(Time::Timestamp current_time);
+
+    // Returns true if the key was pressed during the last frame.
+    PURE Bool pressed(Key key) const;
+    // Returns true if the mouse button was pressed during the last frame.
+    PURE Bool pressed(Mouse button) const;
+
+    // Returns true if the key is currently held down.
+    PURE Bool held(Key key) const;
+    // Returns true if the mouse button is currently held down.
+    PURE Bool held(Mouse button) const;
+
+    // Returns true if the key was released during the last frame.
+    PURE Bool released(Key key) const;
+    // Returns true if the mouse button was released during the last frame.
+    PURE Bool released(Mouse button) const;
+
+    // Returns true if the key was double pressed during the last frame.
+    // A double press is defined as two presses within a certain time frame.
+    PURE Bool double_pressed(Key key) const;
+    // Returns true if the mouse button was double pressed during the last frame.
+    // A double press is defined as two presses within a certain time frame.
+    PURE Bool double_pressed(Mouse button) const;
+
+    // Returns the position of the mouse cursor in the window.
+    PURE Vector<2> mouse_position() const;
+
+    // Returns the change in the position of the mouse cursor since the last frame.
+    PURE Vector<2> mouse_movement() const;
+
+private:
+    template <typename T>
+    struct ButtonMap {
+        T keys[KeyCount] = { 0 };
+        T mouse_buttons[MouseCount] = { 0 };
+    };
+
+    struct FrameState {
+        ButtonMap<Bool> map;
+        Vec2 mouse_position = { 0, 0 };
+    };
+
+    std::unique_ptr<FrameState> m_state = std::make_unique<FrameState>();
+    std::unique_ptr<FrameState> m_previous_state = std::make_unique<FrameState>();
+
+    std::unique_ptr<ButtonMap<Time::Timestamp>> m_last_key_release = std::make_unique<ButtonMap<Time::Timestamp>>();
+    std::unique_ptr<ButtonMap<Bool>> m_double_press = std::make_unique<ButtonMap<Bool>>();
+
+    static Bool key_pressed(std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Key key);
+    static Bool mouse_button_pressed(std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Mouse mouse);
+    static Bool key_released(std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Key key);
+    static Bool mouse_button_released(std::unique_ptr<FrameState> const& current, std::unique_ptr<FrameState> const& previous, Mouse mouse);
+
+    static Bool s_key_buffer[KeyCount];
+    static Bool s_mouse_buffer[MouseCount];
+    static Vec2 s_mouse_position;
+    enum class MouseEventType { None, First, Subsequent };
+    static MouseEventType s_mouse_event_type;
+
+    static USize key_value(Key key);
+    static USize mouse_button_value(Mouse button);
+
+    static Key from_glfw_key(I32 key);
+    static Mouse from_glfw_mouse(I32 button);
+
+    static void key_callback(GLFWwindow* _window, I32 key, I32 _scancode, I32 action, I32 _mods);
+    static void cursor_position_callback(GLFWwindow* _window, F64 x, F64 y);
+    static void mouse_button_callback(GLFWwindow* _window, I32 button, I32 action, I32 _mods);
+};
+
+}
diff --git a/src/Time.cpp b/src/Time.cpp
index e459374..1953fed 100644
--- a/src/Time.cpp
+++ b/src/Time.cpp
@@ -1,4 +1,5 @@
 #include "Time.hpp"
+#include "Common/Casts.hpp"
 #include <chrono>
 #include <algorithm>
 
@@ -10,7 +11,7 @@ void Time::start_frame() {
 
 void Time::end_frame() {
     auto frame_end = now();
-    m_delta = (frame_end - m_current_frame_start) / 1000.0;
+    m_delta = TO(Real, frame_end - m_current_frame_start) / 1000.0;
     m_delta = std::clamp(m_delta, delta_min, delta_max);
 
     m_total_frames++;
@@ -28,6 +29,10 @@ Real Time::delta() const {
     return m_delta;
 }
 
+Time::Timestamp Time::frame_start() const {
+    return m_current_frame_start;
+}
+
 Time::Timestamp Time::now() {
     auto time = std::chrono::system_clock::now().time_since_epoch();
     auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(time);
diff --git a/src/Time.hpp b/src/Time.hpp
index 04f22c8..64ab4a2 100644
--- a/src/Time.hpp
+++ b/src/Time.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "Common/Pure.hpp"
 #include "Common/Sizes.hpp"
 
 namespace MC {
@@ -14,9 +15,17 @@ public:
     void start_frame();
     void end_frame();
 
-    U64 total_frames() const;
-    Tick tick() const;
-    Real delta() const;
+    // The total number of frames that have been rendered.
+    // Starts at 1, with 0 being reserved as the empty value.
+    PURE U64 total_frames() const;
+    // The current frame number.
+    // Starts at 1, with 0 being reserved as the empty value.
+    // This is the same as total_frames(), but is more descriptive, sometimes.
+    // :)
+    PURE Tick tick() const;
+    PURE Real delta() const;
+
+    PURE Timestamp frame_start() const;
 
     static Timestamp now();
 
@@ -24,7 +33,7 @@ private:
     static constexpr Real delta_max = 1.0 / 10.0;
     static constexpr Real delta_min = 1.0 / 1000.0;
 
-    U64 m_total_frames = 0;
+    U64 m_total_frames = 1;
 
     Timestamp m_current_frame_start = 0;
     Real m_delta = 0;