yterm

commit 42fb13dfd7344f65a4de83ff53e6663415d1f348

Author: mesyeti <mesyeti@mesyeti.uk>

compiles

 .gitignore | 2 
 .gitmodules | 3 
 LICENSE | 21 ++
 Makefile | 89 +++++++++
 source/backend.c | 21 ++
 source/backend.h | 65 +++++++
 source/backends/glLegacy.c | 357 ++++++++++++++++++++++++++++++++++++++++
 source/backends/glLegacy.h | 34 +++
 source/backends/stub.c | 143 ++++++++++++++++
 source/backends/stub.h | 8 
 source/common.h | 23 ++
 source/event.c | 96 ++++++++++
 source/event.h | 81 +++++++++
 source/event/sdl2.c | 109 ++++++++++++
 source/input.c | 127 ++++++++++++++
 source/input.h | 65 +++++++
 source/input/sdl.c | 222 ++++++++++++++++++++++++
 source/input/sdl.h | 9 +
 source/keyboard.c | 118 +++++++++++++
 source/keyboard.h | 118 +++++++++++++
 source/log.c | 121 +++++++++++++
 source/log.h | 10 +
 source/main.c | 3 
 source/mem.c | 27 +++
 source/mem.h | 11 +
 source/platform.h | 10 +
 source/platform/unix.c | 20 ++
 source/platform/windows.c | 46 +++++
 source/stb.c | 7 
 source/stb.h | 12 +
 source/texture.c | 75 ++++++++
 source/texture.h | 9 +
 source/types.c | 7 
 source/types.h | 34 +++
 source/util.c | 197 ++++++++++++++++++++++
 source/util.h | 48 +++++
 source/video.c | 71 +++++++
 source/video.h | 27 +++
 source/window.h | 29 +++
 source/window/sdl2.c | 72 ++++++++
 source/window/sdl2.h | 12 +


diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..0a56c13a91b347267c67f8d1e7503768586478ff
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/bin/
+/yterm




diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..431d35d81572daa170007f517078c1565db1310b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "lib/stb"]
+	path = lib/stb
+	url = https://github.com/nothings/stb




diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..1ed3e4dda99de0370f14cca4125e048a128b66aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 MESYETI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.




diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..b89644fb3d2fbe3fefc9f998df99f69022c19bd7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,89 @@
+SOURCES := $(wildcard source/*.c) $(wildcard source/**/*.c) 
+OBJECTS := $(patsubst source/%.c,bin/%.o,$(SOURCES))
+PLAT    := $(shell uname -s)
+
+ifeq ($(PLAT),NetBSD)
+	override CPPFLAGS += -I/usr/X11R7/include -I/usr/pkg/include
+	override LDFLAGS += -L/usr/X11R7/lib -L/usr/pkg/lib -Wl,-R/usr/X11R7/lib -Wl,-R/usr/pkg/lib
+endif
+
+ifeq ($(PLAT),windows)
+	CC := x86_64-w64-mingw32-gcc
+	override LDLIBS += -lkernel32 -l:libSDL2.a -lole32 -loleaut32 -limm32
+	override LDLIBS += -lsetupapi -lversion -lgdi32 -lwinmm -lopengl32
+else
+	override LDLIBS += -lSDL2 -lGL
+endif
+LD := $(CC)
+
+OUT := yterm
+
+ifeq ($(STATIC), y)
+	override CFLAGS += -static -static-libgcc -static-libstdc++
+endif
+
+override CFLAGS += -std=c99 -Wall -Wextra -Wuninitialized -Wundef -pedantic -Ilib -Werror=return-type
+override LDLIBS += -lm
+
+override CPPFLAGS += -DYT_BACKEND_GL_11
+override CPPFLAGS += -DYT_WINDOW_SDL2 -DYT_INPUT_SDL2 -DYT_EVENT_SDL2
+override CPPFLAGS += -DSDL_MAIN_HANDLED -D_POSIX_C_SOURCE=199309L
+
+ifeq ($(BUILD),release)
+	override CFLAGS += -O3
+	#override CPPFLAGS += -NDEBUG
+else
+	override CFLAGS += -O0 -ggdb -fno-omit-frame-pointer
+	override LDFLAGS += -O0 -ggdb -fno-omit-frame-pointer
+	ifeq ($(ASAN),y)
+		override CFLAGS += -fsanitize=address,undefined
+		override LDFLAGS += -fsanitize=address,undefined
+	endif
+endif
+
+.SECONDEXPANSION:
+
+deps.filter := %.c %.h
+deps.option := -MM
+define deps
+$$(filter $$(deps.filter),,$$(shell $(CC) $(CFLAGS) $(CPPFLAGS) -E $(deps.option) $(1)))
+endef
+
+all: $(OUT)
+	@:
+
+run: $(OUT)
+	'$(dir $<)$(notdir $<)' $(RUNFLAGS)
+
+yterm: $(OBJECTS)
+	$(LD) $(LDFLAGS) $^ $(LDLIBS) -o $@
+
+bin/:
+	mkdir -p bin
+
+bin/backends:
+	mkdir -p bin/backends
+
+
+bin/input:
+	mkdir -p bin/input
+
+bin/window:
+	mkdir -p bin/window
+
+bin/event:
+	mkdir -p bin/event
+
+bin/platform:
+	mkdir -p bin/platform
+
+bin/%.o: source/%.c $(call deps,source/%.c) | bin/ bin/backends bin/input bin/window bin/event bin/platform
+	$(CC) $(CFLAGS) $(CPPFLAGS) $< -c -o $@
+
+clean:
+	-rm -r bin
+
+distclean: clean
+	rm $(OUT)
+
+.PHONY: all run clean distclean install








diff --git a/source/backend.c b/source/backend.c
new file mode 100644
index 0000000000000000000000000000000000000000..dc71c0b9a001274147f77bcf3656a61eed1c1f0f
--- /dev/null
+++ b/source/backend.c
@@ -0,0 +1,21 @@
+#include "backend.h"
+
+BackendOptions backendOptions = {
+	.vsync = true,
+	.name  = "gl1"
+};
+
+void Backend_VLine(int x, int y, int thick, int len, Colour colour) {
+	Backend_RenderRect((Rect) {x, y, thick, len}, colour);
+}
+
+void Backend_HLine(int x, int y, int thick, int len, Colour colour) {
+	Backend_RenderRect((Rect) {x, y, len, thick}, colour);
+}
+
+void Backend_RenderRectOL(Rect rect, Colour colour) {
+	Backend_HLine(rect.x, rect.y, 1, rect.w, colour);
+	Backend_VLine(rect.x, rect.y, 1, rect.h, colour);
+	Backend_HLine(rect.x, rect.y + rect.h - 1, 1, rect.w, colour);
+	Backend_VLine(rect.x + rect.w - 1, rect.y, 1, rect.h, colour);
+}




diff --git a/source/backend.h b/source/backend.h
new file mode 100644
index 0000000000000000000000000000000000000000..20e886e2cafdfb920f3f74c5211678f0f799e2d9
--- /dev/null
+++ b/source/backend.h
@@ -0,0 +1,65 @@
+#ifndef YT_BACKEND_H
+#define YT_BACKEND_H
+
+// defined:
+//   (struct) Texture
+#if defined(YT_BACKEND_GL_11)
+	#include "backends/glLegacy.h"
+#elif defined(YT_BACKEND_STUB)
+	#include "backends/stub.h"
+#else
+	#error "No backend"
+#endif
+
+#include "types.h"
+#include "video.h"
+#include "window.h"
+
+typedef struct {
+	Rect rect;
+	bool enabled;
+} BackendViewport;
+
+typedef struct {
+	bool vsync;
+	char name[20];
+} BackendOptions;
+
+extern BackendOptions backendOptions;
+
+typedef struct {
+	bool   doTint;
+	Colour tint;
+} TextureRenderOpt;
+
+// implemented per backend
+void     Backend_Init(bool beforeWindow);
+void     Backend_Free(void);
+void     Backend_SetTarget(Window* window);
+Texture* Backend_LoadTexture(uint8_t* data, int w, int h, int aW, int aH, int ch);
+void     Backend_FreeTexture(Texture* texture);
+Vec2     Backend_GetTextureSize(Texture* texture);
+void     Backend_OnWindowResize(void);
+
+void Backend_DrawTexture(
+	Texture* texture, TextureRenderOpt* p_opt, Rect* p_src, Rect* p_dest
+);
+
+void Backend_Begin(void);
+void Backend_Clear(uint8_t r, uint8_t g, uint8_t b);
+void Backend_SetViewport(int x, int y, int w, int h);
+void Backend_EnableViewport(bool enable);
+void Backend_RenderRect(Rect rect, Colour colour);
+void Backend_RenderLine(Vec2 a, Vec2 b, Colour colour);
+void Backend_EnableAlpha(bool enable);
+void Backend_FinishRender(void);
+
+BackendViewport Backend_SaveViewport(void);
+void            Backend_RestoreViewport(BackendViewport viewport);
+
+// portable
+void Backend_HLine(int x, int y, int thick, int len, Colour colour);
+void Backend_VLine(int x, int y, int thick, int len, Colour colour);
+void Backend_RenderRectOL(Rect rect, Colour colour);
+
+#endif




diff --git a/source/backends/glLegacy.c b/source/backends/glLegacy.c
new file mode 100644
index 0000000000000000000000000000000000000000..61db6d5ba459a25d6cd6117a407368d0cf585e62
--- /dev/null
+++ b/source/backends/glLegacy.c
@@ -0,0 +1,357 @@
+#include "../stb.h"
+#include "../mem.h"
+#include "../util.h"
+#include "../video.h"
+#include "../backend.h"
+
+#ifdef YT_BACKEND_GL_11
+#include "glLegacy.h"
+
+#ifndef GL_CLAMP_TO_EDGE_SGIS
+	#define GL_CLAMP_TO_EDGE_SGIS GL_CLAMP_TO_EDGE
+#endif
+
+static void GL_Error(GLenum error, const char* file, int line) {
+	const char* errorStr;
+	switch (error) {
+		case GL_INVALID_ENUM:      errorStr = "Invalid enum"; break;
+		case GL_INVALID_VALUE:     errorStr = "Invalid value"; break;
+		case GL_INVALID_OPERATION: errorStr = "Invalid operation"; break;
+		case GL_STACK_OVERFLOW:    errorStr = "Stack overflow"; break;
+		case GL_STACK_UNDERFLOW:   errorStr = "Stack underflow"; break;
+		case GL_OUT_OF_MEMORY:     errorStr = "Out of memory"; break;
+		default:                   errorStr = "???";
+	}
+
+	Error("%s:%d: OpenGL error: %s", file, line, errorStr);
+}
+
+#define GL_CALL(CALL, FILE, LINE) do { \
+	CALL; \
+	GLenum error = glGetError(); \
+ \
+	if (error != GL_NO_ERROR) { \
+		GL_Error(error, FILE, LINE); \
+	} \
+} while(0)
+
+#define GL(CALL) GL_CALL(CALL, __FILE__, __LINE__)
+
+typedef struct {
+	bool edgeClamp;
+	bool multiTexture;
+} Features;
+
+static Features features = {
+	.edgeClamp    = false,
+	.multiTexture = false
+};
+
+typedef struct {
+	bool alpha;
+
+	BackendViewport viewport;
+
+	SDL_GLContext ctx;
+	Texture       textures[32];
+} State;
+
+static State state;
+
+void Backend_Init(bool beforeWindow) {
+	if (beforeWindow) {
+		int major = 1;
+		int minor = 1;
+
+		assert(SDL_GL_SetAttribute(
+			SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY
+		) == 0);
+		assert(SDL_GL_SetAttribute(
+			SDL_GL_CONTEXT_MAJOR_VERSION, major
+		) == 0);
+		assert(SDL_GL_SetAttribute(
+			SDL_GL_CONTEXT_MINOR_VERSION, minor
+		) == 0);
+		//#if USE_KHR_DEBUG
+		//    assert(SDL_GL_SetAttribute(
+		//	    SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG
+		//    ) == 0);
+		//#endif
+
+		state.viewport.enabled = false;
+		state.viewport.rect    = (Rect) {0, 0, 0, 0};
+
+		return;
+	}
+
+	state.ctx = SDL_GL_CreateContext(video.windows[0].window);
+	assert(SDL_GL_MakeCurrent(video.windows[0].window, state.ctx) == 0);
+
+	Log("Backend info: GL Legacy");
+
+	Log("==================");
+	Log("Vendor:           %s", (const char*) glGetString(GL_VENDOR));
+	Log("Renderer:         %s", (const char*) glGetString(GL_RENDERER));
+	Log("Version:          %s", (const char*) glGetString(GL_VERSION));
+
+	const char* ext = glGetString(GL_EXTENSIONS);
+
+	if (strstr(ext, "GL_ARB_multitexture") != NULL) {
+		Log("ARB multitexture extension available");
+		features.multiTexture = true;
+	}
+	else {
+		Log("ARB multitexture extension not available");
+		features.multiTexture = false;
+	}
+
+	if (strstr(ext, "SGIS_texture_edge_clamp") != NULL) {
+		Log("SGIS texture edge clamp extension available");
+		features.edgeClamp = true;
+	}
+	else {
+		Log("SGIS texture edge clamp extension not available");
+		features.edgeClamp = false;
+	}
+
+	Log("==================");
+
+	/*Log("Extensions:");
+	printf("    ");
+
+	const char* ext = (const char*) glGetString(GL_EXTENSIONS);
+	for (size_t i = 0; ext[i] != 0; ++ i) {
+		if (ext[i] == ' ') {
+			printf("\n    ");
+		}
+		else {
+			putchar(ext[i]);
+		}
+	}
+
+	printf("\r");*/
+
+	glEnable(GL_TEXTURE_2D);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+}
+
+void Backend_Free(void) {
+	SDL_GL_DeleteContext(state.ctx);
+	// Model_Free(&state.model);
+}
+
+void Backend_SetTarget(Window* window) {
+	(void) window; // unimplemented
+
+	static bool warning = false;
+	if (!warning) {
+		warning = true;
+		Warn("Backend_SetTarget unsupported");
+	}
+}
+
+Texture* Backend_LoadTexture(uint8_t* data, int w, int h, int aW, int aH, int ch) {
+	GLuint tex;
+	GL(glGenTextures(1, &tex));
+	GL(glBindTexture(GL_TEXTURE_2D, tex));
+	GL(glTexImage2D(
+		GL_TEXTURE_2D, 0, ch, aW, aH, 0, (ch == 3) ? GL_RGB : GL_RGBA,
+		GL_UNSIGNED_BYTE, data
+	));
+	GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
+	GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
+	GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,     GL_REPEAT));
+	GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,     GL_REPEAT));
+
+	free(data);
+	GL(glBindTexture(GL_TEXTURE_2D, 0));
+
+	// now put this texture in the texture array
+	for (size_t i = 0; i < sizeof(state.textures) / sizeof(Texture); ++ i) {
+		if (!state.textures[i].used) {
+			state.textures[i].used         = true;
+			state.textures[i].name         = tex;
+			state.textures[i].width        = w;
+			state.textures[i].height       = h;
+			state.textures[i].actualWidth  = aW;
+			state.textures[i].actualHeight = aH;
+			return &state.textures[i];
+		}
+	}
+
+	Error("No more room for textures");
+	return NULL;
+}
+
+void Backend_FreeTexture(Texture* texture) {
+	GL(glDeleteTextures(1, &texture->name));
+	texture->used = false;
+}
+
+Vec2 Backend_GetTextureSize(Texture* texture) {
+	return (Vec2) {texture->width, texture->height};
+}
+
+void Backend_DrawTexture(
+	Texture* texture, TextureRenderOpt* p_opt, Rect* p_src, Rect* p_dest
+) {
+	TextureRenderOpt opt;
+	if (p_opt == NULL) {
+		opt.doTint = false;
+	}
+	else {
+		opt = *p_opt;
+	}
+
+	Rect src;
+	if (p_src == NULL) {
+		src = (Rect) {0, 0, texture->width, texture->height};
+	}
+	else {
+		src = *p_src;
+	}
+
+	Rect dest;
+	if (p_dest == NULL) {
+		dest = (Rect) {0, 0, video.windows[0].width, video.windows[0].height};
+	}
+	else {
+		dest = *p_dest;
+	}
+
+	GL(glBindTexture(GL_TEXTURE_2D, texture->name));
+
+	glBegin(GL_TRIANGLE_FAN);
+
+	if (opt.doTint) {
+		glColor3ub(opt.tint.r, opt.tint.g, opt.tint.b);
+	}
+	else {
+		glColor3ub(255, 255, 255);
+	}
+
+	glTexCoord2f(
+		((float) src.x) / ((float) texture->actualWidth),
+		((float) src.y) / ((float) texture->actualHeight)
+	);
+	glVertex2i(dest.x, dest.y);
+	glTexCoord2f(
+		((float) (src.x + src.w)) / ((float) texture->actualWidth),
+		((float) src.y) / ((float) texture->actualHeight)
+	);
+	glVertex2i(dest.x + dest.w, dest.y);
+	glTexCoord2f(
+		((float) (src.x + src.w)) / ((float) texture->actualWidth),
+		((float) (src.y + src.h)) / ((float) texture->actualHeight)
+	);
+	glVertex2i(dest.x + dest.w, dest.y + dest.h);
+	glTexCoord2f(
+		((float) src.x) / ((float) texture->actualWidth),
+		((float) (src.y + src.h)) / ((float) texture->actualHeight)
+	);
+	glVertex2i(dest.x, dest.y + dest.h);
+	GL(glEnd());
+
+	GL(glBindTexture(GL_TEXTURE_2D, 0));
+}
+
+void Backend_Begin(void) {
+	glViewport(0, 0, video.windows[0].aWidth, video.windows[0].aHeight);
+	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+	glDisable(GL_DEPTH_TEST);
+	glDisable(GL_CULL_FACE);
+	glEnable(GL_BLEND);
+	GL(glMatrixMode(GL_MODELVIEW));
+	GL(glLoadIdentity());
+	GL(glMatrixMode(GL_PROJECTION));
+	GL(glLoadIdentity());
+
+	float floatW = (float) video.windows[0].width;
+	float floatH = (float) video.windows[0].height;
+
+	GL(glOrtho(0.0, floatW, floatH, 0.0, -1.0, 1.0));
+}
+
+void Backend_Clear(uint8_t r, uint8_t g, uint8_t b) {
+	glClearColor(((float) r) / 256.0, ((float) g) / 256.0, ((float) b) / 256.0, 1.0f);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void Backend_SetViewport(int x, int y, int w, int h) {
+	state.viewport.rect = (Rect) {x, y, w, h};
+	GL(glScissor(x, video.windows[0].height - 1 - (y + h), w, h));
+}
+
+void Backend_EnableViewport(bool enable) {
+	state.viewport.enabled = enable;
+
+	if (enable) {
+		GL(glEnable(GL_SCISSOR_TEST));
+	}
+	else {
+		GL(glDisable(GL_SCISSOR_TEST));
+	}
+}
+
+void Backend_RenderRect(Rect rect, Colour colour) {
+	GL(glDisable(GL_TEXTURE_2D));
+	glBegin(GL_TRIANGLE_FAN);
+
+	if (state.alpha) {
+		glColor4ub(colour.r, colour.g, colour.b, colour.a);
+	}
+	else {
+		glColor3ub(colour.r, colour.g, colour.b);
+	}
+
+	glVertex2i(rect.x,          rect.y);
+	glVertex2i(rect.x + rect.w, rect.y);
+	glVertex2i(rect.x + rect.w, rect.y + rect.h);
+	glVertex2i(rect.x,          rect.y + rect.h);
+	GL(glEnd());
+	GL(glEnable(GL_TEXTURE_2D));
+}
+
+void Backend_RenderLine(Vec2 a, Vec2 b, Colour colour) {
+	GL(glDisable(GL_TEXTURE_2D));
+	glBegin(GL_LINES);
+	glColor3ub(colour.r, colour.g, colour.b);
+	glVertex2i(a.x, a.y);
+	glVertex2i(b.x, b.y);
+	GL(glEnd());
+	GL(glEnable(GL_TEXTURE_2D));
+}
+
+void Backend_EnableAlpha(bool enable) {
+	state.alpha = enable;
+
+	if (enable) {
+		GL(glEnable(GL_BLEND));
+	}
+	else {
+		GL(glDisable(GL_BLEND));
+	}
+}
+
+void Backend_FinishRender(void) {
+	GL(glFinish());
+	SDL_GL_SwapWindow(video.windows[0].window);
+}
+
+BackendViewport Backend_SaveViewport(void) {
+	return state.viewport;
+}
+
+void Backend_RestoreViewport(BackendViewport viewport) {
+	state.viewport = viewport;
+
+	Backend_SetViewport(
+		state.viewport.rect.x, state.viewport.rect.y,
+		state.viewport.rect.w, state.viewport.rect.h
+	);
+	Backend_EnableViewport(state.viewport.enabled);
+}
+
+#endif




diff --git a/source/backends/glLegacy.h b/source/backends/glLegacy.h
new file mode 100644
index 0000000000000000000000000000000000000000..0e5275fc0b61e9cb26cf8c92ed3e17c4edeec0c8
--- /dev/null
+++ b/source/backends/glLegacy.h
@@ -0,0 +1,34 @@
+#ifndef YT_BACKENDS_GL_LEGACY_H
+#define YT_BACKENDS_GL_LEGACY_H
+
+#include "../common.h"
+
+#define GL_GLEXT_PROTOTYPES
+#ifdef YT_BACKEND_GL_11
+	#ifdef PLATFORM_OSX
+		#include <OpenGL/gl.h>
+		#include <OpenGL/glext.h>
+	#else
+		#include <GL/gl.h>
+		#include <GL/glext.h>
+	#endif
+#endif
+
+#ifndef USE_KHR_DEBUG
+    #if !defined(NDEBUG) && defined(GL_KHR_debug) && GL_KHR_debug
+        #define USE_KHR_DEBUG 1
+    #else
+        #define USE_KHR_DEBUG 0
+    #endif
+#endif
+
+typedef struct {
+	bool   used;
+	GLuint name;
+	int    width;
+	int    height;
+	int    actualWidth;
+	int    actualHeight;
+} Texture;
+
+#endif




diff --git a/source/backends/stub.c b/source/backends/stub.c
new file mode 100644
index 0000000000000000000000000000000000000000..cef9bcf20d6656bc713220bb20785b174ff581dd
--- /dev/null
+++ b/source/backends/stub.c
@@ -0,0 +1,143 @@
+#include "../backend.h"
+
+#ifdef YT_BACKEND_STUB
+#include "stub.h"
+
+void Backend_Init(bool beforeWindow) {
+	(void) beforeWindow;
+}
+
+void Backend_Free(void) {
+	
+}
+
+void Backend_SetTarget(Window* window) {
+	(void) window;
+}
+
+Texture* Backend_LoadTexture(uint8_t* data, int width, int height, int ch) {
+	(void) data;
+	(void) width;
+	(void) height;
+	(void) ch;
+	return NULL;
+}
+
+void Backend_FreeTexture(Texture* texture) {
+	(void) texture;
+}
+
+Vec2 Backend_GetTextureSize(Texture* texture) {
+	(void) texture;
+	return (Vec2) {0, 0};
+}
+
+void Backend_RenderScene(void) {
+	
+}
+
+void Backend_OnMapFree(void) {
+	
+}
+
+void Backend_OnWindowResize(void) {
+	
+}
+
+void Backend_RenderModel(Model* model, ModelRenderOpt* opt) {
+	(void) model;
+	(void) opt;
+}
+
+void Backend_UseHoldModel(const char* path, ModelRenderOpt opt) {
+	(void) path;
+	(void) opt;
+}
+
+void Backend_DrawTexture(
+	Texture* texture, TextureRenderOpt* p_opt, Rect* p_src, Rect* p_dest
+) {
+	(void) texture;
+
+	TextureRenderOpt opt;
+	if (p_opt == NULL) {
+		opt.doTint = false;
+	}
+	else {
+		opt = *p_opt;
+	}
+
+	Rect src;
+	if (p_src == NULL) {
+		src = (Rect) {0, 0, texture->width, texture->height};
+	}
+	else {
+		src = *p_src;
+	}
+
+	Rect dest;
+	if (p_dest == NULL) {
+		dest = (Rect) {0, 0, video.width, video.height};
+	}
+	else {
+		dest = *p_dest;
+	}
+}
+
+void Backend_Begin(void) {
+	
+}
+
+void Backend_Begin2D(bool originTop) {
+	(void) originTop;
+}
+
+void Backend_Clear(uint8_t r, uint8_t g, uint8_t b) {
+	(void) r;
+	(void) g;
+	(void) b;
+}
+
+void Backend_SetViewport(int x, int y, int w, int h) {
+	(void) x;
+	(void) y;
+	(void) w;
+	(void) h;
+}
+
+void Backend_EnableViewport(bool enable) {
+	(void) enable;
+}
+
+void Backend_RenderRect(Rect rect, Colour colour) {
+	(void) rect;
+	(void) colour;
+}
+
+void Backend_RenderLine(Vec2 a, Vec2 b, Colour colour) {
+	(void) a;
+	(void) b;
+	(void) colour;
+}
+
+void Backend_EnableAlpha(bool enable) {
+	(void) enable;
+}
+
+void Backend_InitSkybox(void) {
+	
+}
+
+void Backend_FinishRender(void) {
+	
+}
+
+BackendViewport Backend_SaveViewport(void) {
+	return (BackendViewport) {};
+}
+
+void Backend_RestoreViewport(BackendViewport viewport) {
+	(void) viewport;
+}
+
+#endif




diff --git a/source/backends/stub.h b/source/backends/stub.h
new file mode 100644
index 0000000000000000000000000000000000000000..77d457fc539a168878dc66b12a87fa2dce5163da
--- /dev/null
+++ b/source/backends/stub.h
@@ -0,0 +1,8 @@
+#ifndef YT_BACKEND_STUB_H
+#define YT_BACKEND_STUB_H
+
+typedef struct {
+	int empty;
+} Texture;
+
+#endif




diff --git a/source/common.h b/source/common.h
new file mode 100644
index 0000000000000000000000000000000000000000..b417ea4ed6619f40724cfd5257a9858f536f5024
--- /dev/null
+++ b/source/common.h
@@ -0,0 +1,23 @@
+#ifndef YT_COMMON_H
+#define YT_COMMON_H
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+
+#define PI 3.14159265359
+
+#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
+	#define PLATFORM_WINDOWS
+#elif defined(__APPLE__)
+	#define PLATFORM_OSX
+#elif defined(__linux__)
+	#define PLATFORM_LINUX
+#elif defined(__unix__)
+	#define PLATFORM_UNIX
+#endif
+
+#endif




diff --git a/source/event/sdl2.c b/source/event/sdl2.c
new file mode 100644
index 0000000000000000000000000000000000000000..83bb3197bef7035b87faeb128fd943670a43b67a
--- /dev/null
+++ b/source/event/sdl2.c
@@ -0,0 +1,109 @@
+#include "../event.h"
+#include "../video.h"
+#include "../input/sdl.h"
+
+#ifdef YT_EVENT_SDL2
+
+void Event_PrepareExternal(void) {
+	
+}
+
+static int GetWindow(uint32_t id) {
+	for (int i = 0; i < WIN_NUM; ++ i) {
+		if (SDL_GetWindowID(video.windows[i].window) == id) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+bool Event_PollExternal(Event* event) {
+	SDL_Event e;
+
+	while (SDL_PollEvent(&e)) {
+		switch (e.type) {
+			case SDL_MOUSEMOTION: {
+				event->mouseMove = (Event_MouseMove) {
+					.type   = EVENT_MOUSE_MOVE,
+					.window = GetWindow(e.motion.windowID),
+					.x      = e.motion.x,
+					.y      = e.motion.y,
+					.xRel   = e.motion.xrel,
+					.yRel   = e.motion.yrel
+				};
+				break;
+			}
+			case SDL_MOUSEBUTTONUP:
+			case SDL_MOUSEBUTTONDOWN: {
+				uint8_t button;
+
+				switch (e.button.button) {
+					case SDL_BUTTON_LEFT:   button = 0; break;
+					case SDL_BUTTON_MIDDLE: button = 1; break;
+					case SDL_BUTTON_RIGHT:  button = 2; break;
+					default:                button = 0;
+				}
+
+				event->mouseButton = (Event_MouseButton) {
+					.type   = e.type == SDL_MOUSEBUTTONDOWN?
+						EVENT_MOUSE_BUTTON_DOWN : EVENT_MOUSE_BUTTON_UP,
+					.window = GetWindow(e.button.windowID),
+					.button = button,
+					.x      = e.button.x,
+					.y      = e.button.y
+				};
+				break;
+			}
+			case SDL_KEYUP:
+			case SDL_KEYDOWN: {
+				event->key = (Event_Key) {
+					.type = e.type == SDL_KEYDOWN?
+						EVENT_KEY_DOWN : EVENT_KEY_UP,
+					.key  = Input_SDLScancodeToKey(e.key.keysym.scancode)
+				};
+				break;
+			}
+			case SDL_QUIT: {
+				event->type = EVENT_QUIT;
+				break;
+			}
+			case SDL_WINDOWEVENT: {
+				if (e.window.event == SDL_WINDOWEVENT_RESIZED) {
+					event->windowResize = (Event_WindowResize) {
+						.type   = EVENT_WINDOW_RESIZE,
+						.window = GetWindow(e.button.windowID),
+						.width  = e.window.data1,
+						.height = e.window.data2
+					};
+					return true;
+				}
+
+				continue;
+			}
+			case SDL_TEXTINPUT: {
+				Event_TextInput textInput;
+				textInput.type = EVENT_TEXT_INPUT;
+				strcpy(textInput.input, e.text.text);
+
+				event->textInput = textInput;
+				break;
+			}
+			default: continue;
+		}
+
+		return true;
+	}
+
+	return false;
+}
+
+void Event_StartTextInput(void) {
+	SDL_StartTextInput();
+}
+
+void Event_StopTextInput(void) {
+	SDL_StopTextInput();
+}
+
+#endif




diff --git a/source/event.c b/source/event.c
new file mode 100644
index 0000000000000000000000000000000000000000..c6a3f127f6874c39494c31991943d5fc6372468a
--- /dev/null
+++ b/source/event.c
@@ -0,0 +1,96 @@
+#include <string.h>
+#include "mem.h"
+#include "util.h"
+#include "event.h"
+#include "input/sdl.h"
+
+Event events[EVENTS_AMOUNT];
+
+typedef struct {
+	Event_Type    type;
+	Event_Handler func;
+} Handler;
+
+static Handler* handlers   = NULL;
+static size_t   handlerNum = 0;
+
+void Event_Init(void) {
+	for (size_t i = 0; i < EVENTS_AMOUNT; ++ i) {
+		events[i].type = EVENT_NONE;
+	}
+}
+
+void Event_Free(void) {
+	if (handlers) {
+		free(handlers);
+	}
+}
+
+static int FindFree(void) {
+	int i;
+
+	for (i = 0; i < EVENTS_AMOUNT; ++ i) {
+		if (events[i].type == EVENT_NONE) return i;
+	}
+
+	for (i = 0; i < EVENTS_AMOUNT; ++ i) {
+		printf("Event %d: %d\n", i, (int) events[i].type);
+	}
+
+	Error("Event pool full");
+	return -1;
+}
+
+void Event_Add(Event e) {
+	events[FindFree()] = e;
+}
+
+void Event_Update(void) {
+	Event e;
+
+	Event_PrepareExternal();
+	while (Event_PollExternal(&e)) {
+		events[FindFree()] = e;
+
+		for (size_t i = 0; i < handlerNum; ++ i) {
+			if (handlers[i].type == e.type) {
+				handlers[i].func(&e);
+			}
+		}
+	}
+}
+
+bool Event_Available(void) {
+	for (size_t i = 0; i < EVENTS_AMOUNT; ++ i) {
+		if (events[i].type != EVENT_NONE) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool Event_Poll(Event* e) {
+	if (!Event_Available()) {
+		Event_Update();
+	}
+
+	for (size_t i = 0; i < EVENTS_AMOUNT; ++ i) {
+		if (events[i].type != EVENT_NONE) {
+			*e             = events[i];
+			events[i].type = EVENT_NONE;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void Event_AddHandler(Event_Type type, Event_Handler func) {
+	Handler handler = (Handler) {type, func};
+
+	++ handlerNum;
+	handlers = SafeRealloc(handlers, handlerNum * sizeof(Handler));
+
+	handlers[handlerNum - 1] = handler;
+}




diff --git a/source/event.h b/source/event.h
new file mode 100644
index 0000000000000000000000000000000000000000..b32e58db315dca59e76f5fa6a3912f7808818483
--- /dev/null
+++ b/source/event.h
@@ -0,0 +1,81 @@
+#ifndef YT_EVENT_H
+#define YT_EVENT_H
+
+#include "window.h"
+#include "keyboard.h"
+
+// i don't use an enum because of compiler warnings about missing cases in switch
+// statements for event types
+#define EVENT_NONE              0
+#define EVENT_MOUSE_MOVE        1
+#define EVENT_MOUSE_BUTTON_DOWN 2
+#define EVENT_MOUSE_BUTTON_UP   3
+#define EVENT_KEY_DOWN          4
+#define EVENT_KEY_UP            5
+#define EVENT_QUIT              6
+#define EVENT_WINDOW_RESIZE     7
+#define EVENT_TEXT_INPUT        8
+
+typedef uint8_t Event_Type;
+
+typedef struct {
+	Event_Type type;
+	int        window;
+	int        x;
+	int        y;
+	int        xRel;
+	int        yRel;
+} Event_MouseMove;
+
+typedef struct {
+	Event_Type type;
+	int        window;
+	uint8_t    button;
+	int        x;
+	int        y;
+} Event_MouseButton;
+
+typedef struct {
+	Event_Type type;
+	Key        key;
+} Event_Key;
+
+typedef struct {
+	Event_Type type;
+	int        window;
+	int        width;
+	int        height;
+} Event_WindowResize;
+
+typedef struct {
+	Event_Type type;
+	char       input[60];
+} Event_TextInput;
+
+typedef union {
+	Event_Type         type;
+	Event_MouseMove    mouseMove;
+	Event_MouseButton  mouseButton;
+	Event_Key          key;
+	Event_WindowResize windowResize;
+	Event_TextInput    textInput;
+} Event;
+
+#define EVENTS_AMOUNT 32
+extern Event events[EVENTS_AMOUNT];
+
+typedef void (*Event_Handler)(Event* event);
+
+void Event_Init(void);
+void Event_Free(void);
+void Event_Add(Event e);
+void Event_Update(void);
+void Event_PrepareExternal(void);
+bool Event_PollExternal(Event* event); // defined in source/event/*.c
+bool Event_Available(void);
+bool Event_Poll(Event* e);
+void Event_StartTextInput(void); // defined in source/event/*.c
+void Event_StopTextInput(void); // defined in source/event/*.c
+void Event_AddHandler(Event_Type type, Event_Handler func);
+
+#endif




diff --git a/source/input/sdl.c b/source/input/sdl.c
new file mode 100644
index 0000000000000000000000000000000000000000..a0ae7c9b3362f938832a7558184f2dba8860e3b4
--- /dev/null
+++ b/source/input/sdl.c
@@ -0,0 +1,222 @@
+#ifdef YT_INPUT_SDL2
+#include <SDL2/SDL.h>
+
+#include "sdl.h"
+#include "../input.h"
+#include "../keyboard.h"
+
+Key Input_SDLScancodeToKey(SDL_Scancode key) {
+	switch (key) {
+		case SDL_SCANCODE_A:             return KEY_A;
+		case SDL_SCANCODE_B:             return KEY_B;
+		case SDL_SCANCODE_C:             return KEY_C;
+		case SDL_SCANCODE_D:             return KEY_D;
+		case SDL_SCANCODE_E:             return KEY_E;
+		case SDL_SCANCODE_F:             return KEY_F;
+		case SDL_SCANCODE_G:             return KEY_G;
+		case SDL_SCANCODE_H:             return KEY_H;
+		case SDL_SCANCODE_I:             return KEY_I;
+		case SDL_SCANCODE_J:             return KEY_J;
+		case SDL_SCANCODE_K:             return KEY_K;
+		case SDL_SCANCODE_L:             return KEY_L;
+		case SDL_SCANCODE_M:             return KEY_M;
+		case SDL_SCANCODE_N:             return KEY_N;
+		case SDL_SCANCODE_O:             return KEY_O;
+		case SDL_SCANCODE_P:             return KEY_P;
+		case SDL_SCANCODE_Q:             return KEY_Q;
+		case SDL_SCANCODE_R:             return KEY_R;
+		case SDL_SCANCODE_S:             return KEY_S;
+		case SDL_SCANCODE_T:             return KEY_T;
+		case SDL_SCANCODE_U:             return KEY_U;
+		case SDL_SCANCODE_V:             return KEY_V;
+		case SDL_SCANCODE_W:             return KEY_W;
+		case SDL_SCANCODE_X:             return KEY_X;
+		case SDL_SCANCODE_Y:             return KEY_Y;
+		case SDL_SCANCODE_Z:             return KEY_Z;
+		case SDL_SCANCODE_0:             return KEY_0;
+		case SDL_SCANCODE_1:             return KEY_1;
+		case SDL_SCANCODE_2:             return KEY_2;
+		case SDL_SCANCODE_3:             return KEY_3;
+		case SDL_SCANCODE_4:             return KEY_4;
+		case SDL_SCANCODE_5:             return KEY_5;
+		case SDL_SCANCODE_6:             return KEY_6;
+		case SDL_SCANCODE_7:             return KEY_7;
+		case SDL_SCANCODE_8:             return KEY_8;
+		case SDL_SCANCODE_9:             return KEY_9;
+		case SDL_SCANCODE_RETURN:        return KEY_RETURN;
+		case SDL_SCANCODE_ESCAPE:        return KEY_ESCAPE;
+		case SDL_SCANCODE_BACKSPACE:     return KEY_BACKSPACE;
+		case SDL_SCANCODE_TAB:           return KEY_TAB;
+		case SDL_SCANCODE_SPACE:         return KEY_SPACE;
+		case SDL_SCANCODE_MINUS:         return KEY_MINUS;
+		case SDL_SCANCODE_EQUALS:        return KEY_EQUALS;
+		case SDL_SCANCODE_LEFTBRACKET:   return KEY_LEFT_BRACKET;
+		case SDL_SCANCODE_RIGHTBRACKET:  return KEY_RIGHT_BRACKET;
+		case SDL_SCANCODE_BACKSLASH:     return KEY_BACKSLASH;
+		case SDL_SCANCODE_SEMICOLON:     return KEY_SEMICOLON;
+		case SDL_SCANCODE_APOSTROPHE:    return KEY_APOSTROPHE;
+		case SDL_SCANCODE_GRAVE:         return KEY_GRAVE;
+		case SDL_SCANCODE_COMMA:         return KEY_COMMA;
+		case SDL_SCANCODE_PERIOD:        return KEY_PERIOD;
+		case SDL_SCANCODE_SLASH:         return KEY_SLASH;
+		case SDL_SCANCODE_CAPSLOCK:      return KEY_CAPS_LOCK;
+		case SDL_SCANCODE_F1:            return KEY_F1;
+		case SDL_SCANCODE_F2:            return KEY_F2;
+		case SDL_SCANCODE_F3:            return KEY_F3;
+		case SDL_SCANCODE_F4:            return KEY_F4;
+		case SDL_SCANCODE_F5:            return KEY_F5;
+		case SDL_SCANCODE_F6:            return KEY_F6;
+		case SDL_SCANCODE_F7:            return KEY_F7;
+		case SDL_SCANCODE_F8:            return KEY_F8;
+		case SDL_SCANCODE_F9:            return KEY_F9;
+		case SDL_SCANCODE_F10:           return KEY_F10;
+		case SDL_SCANCODE_F11:           return KEY_F11;
+		case SDL_SCANCODE_F12:           return KEY_F12;
+		case SDL_SCANCODE_PRINTSCREEN:   return KEY_PRINT_SCREEN;
+		case SDL_SCANCODE_SCROLLLOCK:    return KEY_SCROLL_LOCK;
+		case SDL_SCANCODE_PAUSE:         return KEY_PAUSE;
+		case SDL_SCANCODE_INSERT:        return KEY_INSERT;
+		case SDL_SCANCODE_HOME:          return KEY_HOME;
+		case SDL_SCANCODE_PAGEUP:        return KEY_PAGE_UP;
+		case SDL_SCANCODE_PAGEDOWN:      return KEY_PAGE_DOWN;
+		case SDL_SCANCODE_DELETE:        return KEY_DELETE;
+		case SDL_SCANCODE_END:           return KEY_END;
+		case SDL_SCANCODE_RIGHT:         return KEY_RIGHT;
+		case SDL_SCANCODE_LEFT:          return KEY_LEFT;
+		case SDL_SCANCODE_DOWN:          return KEY_DOWN;
+		case SDL_SCANCODE_UP:            return KEY_UP;
+		case SDL_SCANCODE_NUMLOCKCLEAR:  return KEY_NUM_LOCK;
+		case SDL_SCANCODE_KP_DIVIDE:     return KEY_NP_DIVIDE;
+		case SDL_SCANCODE_KP_MULTIPLY:   return KEY_NP_MULTIPLY;
+		case SDL_SCANCODE_KP_MINUS:      return KEY_NP_MINUS;
+		case SDL_SCANCODE_KP_PLUS:       return KEY_NP_PLUS;
+		case SDL_SCANCODE_KP_ENTER:      return KEY_NP_ENTER;
+		case SDL_SCANCODE_KP_0:          return KEY_NP_0;
+		case SDL_SCANCODE_KP_1:          return KEY_NP_1;
+		case SDL_SCANCODE_KP_2:          return KEY_NP_2;
+		case SDL_SCANCODE_KP_3:          return KEY_NP_3;
+		case SDL_SCANCODE_KP_4:          return KEY_NP_4;
+		case SDL_SCANCODE_KP_5:          return KEY_NP_5;
+		case SDL_SCANCODE_KP_6:          return KEY_NP_6;
+		case SDL_SCANCODE_KP_7:          return KEY_NP_7;
+		case SDL_SCANCODE_KP_8:          return KEY_NP_8;
+		case SDL_SCANCODE_KP_9:          return KEY_NP_9;
+		case SDL_SCANCODE_KP_PERIOD:     return KEY_NP_PERIOD;
+		default: return 0;
+	}
+}
+
+SDL_Scancode Input_KeyToSDLScancode(Key key) {
+	switch (key) {
+		case KEY_A:             return SDL_SCANCODE_A;
+		case KEY_B:             return SDL_SCANCODE_B;
+		case KEY_C:             return SDL_SCANCODE_C;
+		case KEY_D:             return SDL_SCANCODE_D;
+		case KEY_E:             return SDL_SCANCODE_E;
+		case KEY_F:             return SDL_SCANCODE_F;
+		case KEY_G:             return SDL_SCANCODE_G;
+		case KEY_H:             return SDL_SCANCODE_H;
+		case KEY_I:             return SDL_SCANCODE_I;
+		case KEY_J:             return SDL_SCANCODE_J;
+		case KEY_K:             return SDL_SCANCODE_K;
+		case KEY_L:             return SDL_SCANCODE_L;
+		case KEY_M:             return SDL_SCANCODE_M;
+		case KEY_N:             return SDL_SCANCODE_N;
+		case KEY_O:             return SDL_SCANCODE_O;
+		case KEY_P:             return SDL_SCANCODE_P;
+		case KEY_Q:             return SDL_SCANCODE_Q;
+		case KEY_R:             return SDL_SCANCODE_R;
+		case KEY_S:             return SDL_SCANCODE_S;
+		case KEY_T:             return SDL_SCANCODE_T;
+		case KEY_U:             return SDL_SCANCODE_U;
+		case KEY_V:             return SDL_SCANCODE_V;
+		case KEY_W:             return SDL_SCANCODE_W;
+		case KEY_X:             return SDL_SCANCODE_X;
+		case KEY_Y:             return SDL_SCANCODE_Y;
+		case KEY_Z:             return SDL_SCANCODE_Z;
+		case KEY_0:             return SDL_SCANCODE_0;
+		case KEY_1:             return SDL_SCANCODE_1;
+		case KEY_2:             return SDL_SCANCODE_2;
+		case KEY_3:             return SDL_SCANCODE_3;
+		case KEY_4:             return SDL_SCANCODE_4;
+		case KEY_5:             return SDL_SCANCODE_5;
+		case KEY_6:             return SDL_SCANCODE_6;
+		case KEY_7:             return SDL_SCANCODE_7;
+		case KEY_8:             return SDL_SCANCODE_8;
+		case KEY_9:             return SDL_SCANCODE_9;
+		case KEY_RETURN:        return SDL_SCANCODE_RETURN;
+		case KEY_ESCAPE:        return SDL_SCANCODE_ESCAPE;
+		case KEY_BACKSPACE:     return SDL_SCANCODE_BACKSPACE;
+		case KEY_TAB:           return SDL_SCANCODE_TAB;
+		case KEY_SPACE:         return SDL_SCANCODE_SPACE;
+		case KEY_MINUS:         return SDL_SCANCODE_MINUS;
+		case KEY_EQUALS:        return SDL_SCANCODE_EQUALS;
+		case KEY_LEFT_BRACKET:  return SDL_SCANCODE_LEFTBRACKET;
+		case KEY_RIGHT_BRACKET: return SDL_SCANCODE_RIGHTBRACKET;
+		case KEY_BACKSLASH:     return SDL_SCANCODE_BACKSLASH;
+		case KEY_SEMICOLON:     return SDL_SCANCODE_SEMICOLON;
+		case KEY_APOSTROPHE:    return SDL_SCANCODE_APOSTROPHE;
+		case KEY_GRAVE:         return SDL_SCANCODE_GRAVE;
+		case KEY_COMMA:         return SDL_SCANCODE_COMMA;
+		case KEY_PERIOD:        return SDL_SCANCODE_PERIOD;
+		case KEY_SLASH:         return SDL_SCANCODE_SLASH;
+		case KEY_CAPS_LOCK:     return SDL_SCANCODE_CAPSLOCK;
+		case KEY_F1:            return SDL_SCANCODE_F1;
+		case KEY_F2:            return SDL_SCANCODE_F2;
+		case KEY_F3:            return SDL_SCANCODE_F3;
+		case KEY_F4:            return SDL_SCANCODE_F4;
+		case KEY_F5:            return SDL_SCANCODE_F5;
+		case KEY_F6:            return SDL_SCANCODE_F6;
+		case KEY_F7:            return SDL_SCANCODE_F7;
+		case KEY_F8:            return SDL_SCANCODE_F8;
+		case KEY_F9:            return SDL_SCANCODE_F9;
+		case KEY_F10:           return SDL_SCANCODE_F10;
+		case KEY_F11:           return SDL_SCANCODE_F11;
+		case KEY_F12:           return SDL_SCANCODE_F12;
+		case KEY_PRINT_SCREEN:  return SDL_SCANCODE_PRINTSCREEN;
+		case KEY_SCROLL_LOCK:   return SDL_SCANCODE_SCROLLLOCK;
+		case KEY_PAUSE:         return SDL_SCANCODE_PAUSE;
+		case KEY_INSERT:        return SDL_SCANCODE_INSERT;
+		case KEY_HOME:          return SDL_SCANCODE_HOME;
+		case KEY_PAGE_UP:       return SDL_SCANCODE_PAGEUP;
+		case KEY_PAGE_DOWN:     return SDL_SCANCODE_PAGEDOWN;
+		case KEY_DELETE:        return SDL_SCANCODE_DELETE;
+		case KEY_END:           return SDL_SCANCODE_END;
+		case KEY_RIGHT:         return SDL_SCANCODE_RIGHT;
+		case KEY_LEFT:          return SDL_SCANCODE_LEFT;
+		case KEY_DOWN:          return SDL_SCANCODE_DOWN;
+		case KEY_UP:            return SDL_SCANCODE_UP;
+		case KEY_NUM_LOCK:      return SDL_SCANCODE_NUMLOCKCLEAR;
+		case KEY_NP_DIVIDE:     return SDL_SCANCODE_KP_DIVIDE;
+		case KEY_NP_MULTIPLY:   return SDL_SCANCODE_KP_MULTIPLY;
+		case KEY_NP_MINUS:      return SDL_SCANCODE_KP_MINUS;
+		case KEY_NP_PLUS:       return SDL_SCANCODE_KP_PLUS;
+		case KEY_NP_ENTER:      return SDL_SCANCODE_KP_ENTER;
+		case KEY_NP_0:          return SDL_SCANCODE_KP_0;
+		case KEY_NP_1:          return SDL_SCANCODE_KP_1;
+		case KEY_NP_2:          return SDL_SCANCODE_KP_2;
+		case KEY_NP_3:          return SDL_SCANCODE_KP_3;
+		case KEY_NP_4:          return SDL_SCANCODE_KP_4;
+		case KEY_NP_5:          return SDL_SCANCODE_KP_5;
+		case KEY_NP_6:          return SDL_SCANCODE_KP_6;
+		case KEY_NP_7:          return SDL_SCANCODE_KP_7;
+		case KEY_NP_8:          return SDL_SCANCODE_KP_8;
+		case KEY_NP_9:          return SDL_SCANCODE_KP_9;
+		case KEY_NP_PERIOD:     return SDL_SCANCODE_KP_PERIOD;
+		default: return 0;
+	}
+}
+
+bool Input_KeyPressed(Key key) {
+	const uint8_t* keyState = SDL_GetKeyboardState(NULL);
+
+	return keyState[Input_KeyToSDLScancode(key)]? true : false;
+}
+
+bool Input_MouseButtonPressed(uint8_t button) {
+	int x, y;
+
+	return (SDL_GetMouseState(&x, &y) & SDL_BUTTON(button))? true : false;
+}
+
+#endif




diff --git a/source/input/sdl.h b/source/input/sdl.h
new file mode 100644
index 0000000000000000000000000000000000000000..2697a4b616517636405e11c6d54a781c860c6c48
--- /dev/null
+++ b/source/input/sdl.h
@@ -0,0 +1,9 @@
+#if !defined(YT_INPUT_SDL_H) && defined(YT_INPUT_SDL2)
+
+#include <SDL2/SDL.h>
+#include "../keyboard.h"
+
+Key          Input_SDLScancodeToKey(SDL_Scancode key);
+SDL_Scancode Input_KeyToSDLScancode(Key key);
+
+#endif




diff --git a/source/input.c b/source/input.c
new file mode 100644
index 0000000000000000000000000000000000000000..6e4a070987ff8c3063e541f36e15750596eaceaf
--- /dev/null
+++ b/source/input.c
@@ -0,0 +1,127 @@
+#include <stdio.h>
+#include <string.h>
+#include "mem.h"
+#include "input.h"
+
+Input_Manager input = {
+	.binds      = NULL,
+	.bindAmount = 0,
+
+	.mouseBtn = {false, false, false},
+	.mousePos = {0, 0}
+};
+
+void Input_Free(void) {
+	if (input.binds) {
+		free(input.binds);
+		input.binds = NULL;
+	}
+
+	input.bindAmount = 0;
+}
+
+void Input_PrintBind(char* dest, size_t size, Input_BindID bindID) {
+	Input_Bind* bind = &input.binds[bindID];
+
+	switch (bind->type) {
+		case YT_INPUT_BIND_KEY: {
+			snprintf(dest, size, "key %s", Key_ToString(bind->key.key));
+			break;
+		}
+		case YT_INPUT_BIND_MOUSE_BUTTON: {
+			snprintf(dest, size, "button %d", bind->mouseButton.button);
+			break;
+		}
+		default: {
+			snprintf(dest, size, "???");
+			break;
+		}
+	}
+}
+
+bool Input_MatchBind(Input_BindID bindID, Event* e) {
+	if (bindID > input.bindAmount) return false;
+
+	Input_Bind* bind = &input.binds[bindID];
+
+	switch (bind->type) {
+		case YT_INPUT_BIND_KEY: {
+			if ((e->type != EVENT_KEY_DOWN) && (e->type != EVENT_KEY_UP)) {
+				return false;
+			}
+
+			return bind->key.key == e->key.key; // TODO: modifiers
+		}
+		case YT_INPUT_BIND_MOUSE_BUTTON: {
+			if (
+				(e->type != EVENT_MOUSE_BUTTON_DOWN) &&
+				(e->type != EVENT_MOUSE_BUTTON_UP)
+			) {
+				return false;
+			}
+
+			return bind->mouseButton.button == e->mouseButton.button;
+		}
+	}
+
+	return false;
+}
+
+bool Input_BindPressed(Input_BindID bindID) {
+	if (bindID > input.bindAmount) return false;
+
+	Input_Bind* bind = &input.binds[bindID];
+
+	switch (bind->type) {
+		case YT_INPUT_BIND_KEY: {
+			return Input_KeyPressed(bind->key.key); // TODO: modifiers
+		}
+		case YT_INPUT_BIND_MOUSE_BUTTON: {
+			return Input_MouseButtonPressed(bind->mouseButton.button);
+		}
+	}
+
+	return false;
+}
+
+Input_BindID Input_AddBind(Input_Bind bind) {
+	input.binds = SafeRealloc(input.binds, (input.bindAmount + 1) * sizeof(Input_Bind));
+	++ input.bindAmount;
+
+	input.binds[input.bindAmount - 1] = bind;
+	return input.bindAmount - 1;
+}
+
+Input_BindID Input_AddKeyBind(Key mod[3], Key key) {
+	Input_Bind bind;
+	bind.type    = YT_INPUT_BIND_KEY;
+	bind.key.key = key;
+	memcpy(bind.key.mod, mod, sizeof(bind.key.mod));
+
+	return Input_AddBind(bind);
+}
+
+Input_BindID Input_AddMouseButtonBind(uint8_t button) {
+	Input_Bind bind;
+	bind.type               = YT_INPUT_BIND_MOUSE_BUTTON;
+	bind.mouseButton.button = button;
+
+	return Input_AddBind(bind);
+}
+
+void Input_HandleEvent(Event* e) {
+	switch (e->type) {
+		case EVENT_MOUSE_MOVE: {
+			input.mousePos = (Vec2) {e->mouseMove.x, e->mouseMove.y};
+			break;
+		}
+		case EVENT_MOUSE_BUTTON_DOWN: {
+			input.mouseBtn[e->mouseButton.button] = true;
+			break;
+		}
+		case EVENT_MOUSE_BUTTON_UP: {
+			input.mouseBtn[e->mouseButton.button] = false;
+			break;
+		}
+	}
+}




diff --git a/source/input.h b/source/input.h
new file mode 100644
index 0000000000000000000000000000000000000000..073849888e907cb9dde41be4ad318a203a17f787
--- /dev/null
+++ b/source/input.h
@@ -0,0 +1,65 @@
+#ifndef YT_INPUT_H
+#define YT_INPUT_H
+
+#include "event.h"
+#include "types.h"
+#include "keyboard.h"
+
+// mouse stuff
+#define YT_MOUSE_BUTTON_1 0
+#define YT_MOUSE_BUTTON_2 1
+#define YT_MOUSE_BUTTON_3 2
+
+typedef enum {
+	YT_INPUT_BIND_KEY = 0,
+	YT_INPUT_BIND_MOUSE_BUTTON
+} Input_BindType;
+
+typedef struct {
+	Input_BindType type;
+
+	Key key;
+	Key mod[3];
+} Input_KeyBind;
+
+typedef struct {
+	Input_BindType type;
+	uint8_t        button;
+} Input_MouseButtonBind;
+
+typedef union {
+	Input_BindType        type;
+	Input_KeyBind         key;
+	Input_MouseButtonBind mouseButton;
+} Input_Bind;
+
+typedef struct {
+	Input_Bind* binds;
+	size_t      bindAmount;
+
+	bool mouseBtn[3];
+	Vec2 mousePos;
+} Input_Manager;
+
+typedef size_t Input_BindID;
+
+#define INPUT_BIND_NONE ((size_t) -1)
+
+extern Input_Manager input;
+
+// non-portable
+bool Input_KeyPressed(Key key);
+bool Input_MouseButtonPressed(uint8_t button);
+
+// portable
+// NOTE: NOT SAFE
+void         Input_Free(void);
+void         Input_PrintBind(char* dest, size_t size, Input_BindID bindID);
+bool         Input_MatchBind(Input_BindID bindID, Event* e);
+bool         Input_BindPressed(Input_BindID bindID);
+Input_BindID Input_AddBind(Input_Bind bind);
+Input_BindID Input_AddKeyBind(Key mod[3], Key key);
+Input_BindID Input_AddMouseButtonBind(uint8_t button);
+void         Input_HandleEvent(Event* e);
+
+#endif




diff --git a/source/keyboard.c b/source/keyboard.c
new file mode 100644
index 0000000000000000000000000000000000000000..e64afd863e5cda1be66f554700b3f05b0d53327f
--- /dev/null
+++ b/source/keyboard.c
@@ -0,0 +1,118 @@
+#include <string.h>
+#include "keyboard.h"
+
+// TODO: this thing has missing values
+static const char* keyTable[] = {
+	/*  0 */ /* KEY_NONE */          "None",
+	/*  1 */ /* KEY_A */             "A",
+	/*  2 */ /* KEY_B */             "B",
+	/*  3 */ /* KEY_C */             "C",
+	/*  4 */ /* KEY_D */             "D",
+	/*  5 */ /* KEY_E */             "E",
+	/*  6 */ /* KEY_F */             "F",
+	/*  7 */ /* KEY_G */             "G",
+	/*  8 */ /* KEY_H */             "H",
+	/*  9 */ /* KEY_I */             "I",
+	/* 10 */ /* KEY_J */             "J",
+	/* 11 */ /* KEY_K */             "K",
+	/* 12 */ /* KEY_L */             "L",
+	/* 13 */ /* KEY_M */             "M",
+	/* 14 */ /* KEY_N */             "N",
+	/* 15 */ /* KEY_O */             "O",
+	/* 16 */ /* KEY_P */             "P",
+	/* 17 */ /* KEY_Q */             "Q",
+	/* 18 */ /* KEY_R */             "R",
+	/* 19 */ /* KEY_S */             "S",
+	/* 20 */ /* KEY_T */             "T",
+	/* 21 */ /* KEY_U */             "U",
+	/* 22 */ /* KEY_V */             "V",
+	/* 23 */ /* KEY_W */             "W",
+	/* 24 */ /* KEY_X */             "X",
+	/* 25 */ /* KEY_Y */             "Y",
+	/* 26 */ /* KEY_Z */             "Z",
+	/* 27 */ /* KEY_0 */             "0",
+	/* 28 */ /* KEY_1 */             "1",
+	/* 29 */ /* KEY_2 */             "2",
+	/* 30 */ /* KEY_3 */             "3",
+	/* 31 */ /* KEY_4 */             "4",
+	/* 32 */ /* KEY_5 */             "5",
+	/* 33 */ /* KEY_6 */             "6",
+	/* 34 */ /* KEY_7 */             "7",
+	/* 35 */ /* KEY_8 */             "8",
+	/* 36 */ /* KEY_9 */             "9",
+	/* 37 */ /* KEY_RETURN */        "Return",
+	/* 38 */ /* KEY_ESCAPE */        "Escape",
+	/* 39 */ /* KEY_BACKSPACE */     "Backspace",
+	/* 40 */ /* KEY_TAB */           "Tab",
+	/* 41 */ /* KEY_SPACE */         "Space",
+	/* 42 */ /* KEY_MINUS */         "Minus",
+	/* 43 */ /* KEY_EQUALS */        "Equals",
+	/* 44 */ /* KEY_LEFT_BRACKET */  "Left bracket",
+	/* 45 */ /* KEY_RIGHT_BRACKET */ "Right bracket",
+	/* 46 */ /* KEY_BACKSLASH */     "Backslash",
+	/* 47 */ /* KEY_SEMICOLON */     "Semicolon",
+	/* 48 */ /* KEY_APOSTROPHE */    "Apostrophe",
+	/* 49 */ /* KEY_GRAVE */         "Grave",
+	/* 50 */ /* KEY_COMMA */         "Comma",
+	/* 51 */ /* KEY_PERIOD */        "Period",
+	/* 52 */ /* KEY_SLASH */         "Slash",
+	/* 53 */ /* KEY_CAPS_LOCK */     "Caps lock",
+	/* 54 */ /* KEY_F1 */            "F1",
+	/* 55 */ /* KEY_F2 */            "F2",
+	/* 56 */ /* KEY_F3 */            "F3",
+	/* 57 */ /* KEY_F4 */            "F4",
+	/* 58 */ /* KEY_F5 */            "F5",
+	/* 59 */ /* KEY_F6 */            "F6",
+	/* 60 */ /* KEY_F7 */            "F7",
+	/* 61 */ /* KEY_F8 */            "F8",
+	/* 62 */ /* KEY_F9 */            "F9",
+	/* 63 */ /* KEY_F10 */           "F10",
+	/* 64 */ /* KEY_F11 */           "F11",
+	/* 65 */ /* KEY_F12 */           "F12",
+	/* 66 */ /* KEY_PRINT_SCREEN */  "Print screen",
+	/* 67 */ /* KEY_SCROLL_LOCK */   "Scroll lock",
+	/* 68 */ /* KEY_PAUSE */         "Pause",
+	/* 69 */ /* KEY_INSERT */        "Insert",
+	/* 70 */ /* KEY_HOME */          "Home",
+	/* 71 */ /* KEY_PAGE_UP */       "Page up",
+	/* 72 */ /* KEY_PAGE_DOWN */     "Page down",
+	/* 73 */ /* KEY_DELETE */        "Delete",
+	/* 74 */ /* KEY_END */           "End",
+	/* 75 */ /* KEY_RIGHT */         "Right arrow",
+	/* 76 */ /* KEY_LEFT */          "Left arrow",
+	/* 77 */ /* KEY_DOWN */          "Down arrow",
+	/* 78 */ /* KEY_UP */            "Up arrow",
+	/* 79 */ /* KEY_NUM_LOCK */      "Num lock",
+	/* 80 */ /* KEY_NP_DIVIDE */     "Numpad divide",
+	/* 81 */ /* KEY_NP_MULTIPLY */   "Numpad multiply",
+	/* 82 */ /* KEY_NP_MINUS */      "Numpad minus",
+	/* 83 */ /* KEY_NP_PLUS */       "Numpad plus",
+	/* 84 */ /* KEY_NP_ENTER */      "Numpad enter",
+	/* 85 */ /* KEY_NP_0 */          "Numpad 0",
+	/* 86 */ /* KEY_NP_1 */          "Numpad 1",
+	/* 87 */ /* KEY_NP_2 */          "Numpad 2",
+	/* 88 */ /* KEY_NP_3 */          "Numpad 3",
+	/* 89 */ /* KEY_NP_4 */          "Numpad 4",
+	/* 90 */ /* KEY_NP_5 */          "Numpad 5",
+	/* 91 */ /* KEY_NP_6 */          "Numpad 6",
+	/* 92 */ /* KEY_NP_7 */          "Numpad 7",
+	/* 93 */ /* KEY_NP_8 */          "Numpad 8",
+	/* 94 */ /* KEY_NP_9 */          "Numpad 9",
+	/* 95 */ /* KEY_NP_PERIOD */     "Numpad period"
+};
+
+const char* Key_ToString(Key key) {
+	if ((key < 0) || (key >= KEY_AMOUNT)) {
+		return "???";
+	}
+
+	return keyTable[key];
+}
+
+Key Key_FromString(const char* str) {
+	for (size_t i = 0; i < KEY_AMOUNT; ++ i) {
+		if (strcmp(keyTable[i], str) == 0) return (Key) i;
+	}
+
+	return 0;
+}




diff --git a/source/keyboard.h b/source/keyboard.h
new file mode 100644
index 0000000000000000000000000000000000000000..68af91092313b988efa73a849bd574b144b976c1
--- /dev/null
+++ b/source/keyboard.h
@@ -0,0 +1,118 @@
+#ifndef YT_KEYBOARD_H
+#define YT_KEYBOARD_H
+
+#include "common.h"
+
+typedef enum {
+	KEY_NONE = 0,
+
+	KEY_A = 1,
+	KEY_B = 2,
+	KEY_C = 3,
+	KEY_D = 4,
+	KEY_E = 5,
+	KEY_F = 6,
+	KEY_G = 7,
+	KEY_H = 8,
+	KEY_I = 9,
+	KEY_J = 10,
+	KEY_K = 11,
+	KEY_L = 12,
+	KEY_M = 13,
+	KEY_N = 14,
+	KEY_O = 15,
+	KEY_P = 16,
+	KEY_Q = 17,
+	KEY_R = 18,
+	KEY_S = 19,
+	KEY_T = 20,
+	KEY_U = 21,
+	KEY_V = 22,
+	KEY_W = 23,
+	KEY_X = 24,
+	KEY_Y = 25,
+	KEY_Z = 26,
+
+	KEY_0 = 27,
+	KEY_1 = 28,
+	KEY_2 = 29,
+	KEY_3 = 30,
+	KEY_4 = 31,
+	KEY_5 = 32,
+	KEY_6 = 33,
+	KEY_7 = 34,
+	KEY_8 = 35,
+	KEY_9 = 36,
+
+	KEY_RETURN        = 37,
+	KEY_ESCAPE        = 38,
+	KEY_BACKSPACE     = 39,
+	KEY_TAB           = 40,
+	KEY_SPACE         = 41,
+	KEY_MINUS         = 42,
+	KEY_EQUALS        = 43,
+	KEY_LEFT_BRACKET  = 44,
+	KEY_RIGHT_BRACKET = 45,
+	KEY_BACKSLASH     = 46,
+	KEY_SEMICOLON     = 47,
+	KEY_APOSTROPHE    = 48,
+	KEY_GRAVE         = 49,
+	KEY_COMMA         = 50,
+	KEY_PERIOD        = 51,
+	KEY_SLASH         = 52,
+	KEY_CAPS_LOCK     = 53,
+
+	KEY_F1  = 54,
+	KEY_F2  = 55,
+	KEY_F3  = 56,
+	KEY_F4  = 57,
+	KEY_F5  = 58,
+	KEY_F6  = 59,
+	KEY_F7  = 60,
+	KEY_F8  = 61,
+	KEY_F9  = 62,
+	KEY_F10 = 63,
+	KEY_F11 = 64,
+	KEY_F12 = 65,
+
+	KEY_PRINT_SCREEN = 66,
+	KEY_SCROLL_LOCK  = 67,
+	KEY_PAUSE        = 68,
+	KEY_INSERT       = 69,
+
+	KEY_HOME      = 70,
+	KEY_PAGE_UP   = 71,
+	KEY_PAGE_DOWN = 72,
+	KEY_DELETE    = 73,
+	KEY_END       = 74,
+	KEY_RIGHT     = 75,
+	KEY_LEFT      = 76,
+	KEY_DOWN      = 77,
+	KEY_UP        = 78,
+
+	KEY_NUM_LOCK = 79,
+
+	KEY_NP_DIVIDE   = 80,
+	KEY_NP_MULTIPLY = 81,
+	KEY_NP_MINUS    = 82,
+	KEY_NP_PLUS     = 83,
+	KEY_NP_ENTER    = 84,
+	KEY_NP_0        = 85,
+	KEY_NP_1        = 86,
+	KEY_NP_2        = 87,
+	KEY_NP_3        = 88,
+	KEY_NP_4        = 89,
+	KEY_NP_5        = 90,
+	KEY_NP_6        = 91,
+	KEY_NP_7        = 92,
+	KEY_NP_8        = 93,
+	KEY_NP_9        = 94,
+	KEY_NP_PERIOD   = 95,
+
+	KEY_AMOUNT
+} Key;
+
+const char* Key_ToString(Key key);
+Key         Key_FromString(const char* str);
+
+#endif




diff --git a/source/log.c b/source/log.c
new file mode 100644
index 0000000000000000000000000000000000000000..1370d53de631952710cbdb1081e8c56a9bac5d87
--- /dev/null
+++ b/source/log.c
@@ -0,0 +1,121 @@
+#include <time.h>
+#include <stdarg.h>
+#include <string.h>
+#include <assert.h>
+#include "log.h"
+#include "mem.h"
+#include "window.h"
+
+static FILE* logFile = NULL;
+
+void Log_Init(void) {
+// 	logFile = fopen(AE_LOCATION "log.txt", "w");
+// 
+// 	if (!logFile) {
+// 		Warn("Cannot open log file: " AE_LOCATION "log.txt");
+// 	}
+}
+
+void Log_End(void) {
+	// if (!logFile) {
+	// 	fflush(logFile);
+	// 	fclose(logFile);
+	// }
+}
+
+// most of this is taken from vsprintf(3)
+#define FMT(RET, FORMAT) do { \
+	int     n    = 0; \
+	size_t  size = 0; \
+	va_list ap; \
+\
+	va_start(ap, FORMAT); \
+	n = vsnprintf(RET, size, FORMAT, ap); \
+	va_end(ap); \
+\
+	assert(n >= 0); \
+\
+	size = n + 1; \
+	RET = (char*) SafeMalloc(size); \
+	if (RET == NULL) { \
+		return; \
+	} \
+\
+	va_start(ap, FORMAT); \
+	n = vsnprintf(RET, size, FORMAT, ap); \
+	va_end(ap); \
+\
+	assert(n >= 0); \
+} while (0);
+
+void Log(const char* format, ...) {
+	char* ret = NULL;
+
+	FMT(ret, format);
+
+	time_t     rawTime;
+	struct tm* tm;
+	
+	time(&rawTime);
+	tm = localtime(&rawTime);
+	
+	printf("[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, ret);
+	if (logFile) {
+		fprintf(
+			logFile, "[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec,
+			ret
+		);
+		fflush(logFile);
+	}
+
+	free(ret);
+}
+
+void Error(const char* format, ...) {
+	char* ret = NULL;
+
+	FMT(ret, format);
+
+	time_t     rawTime;
+	struct tm* tm;
+	
+	time(&rawTime);
+	tm = localtime(&rawTime);
+	
+	printf("[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, ret);
+	if (logFile) {
+		fprintf(
+			logFile, "[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec,
+			ret
+		);
+		fflush(logFile);
+	}
+
+	Window_MessageBox(WINDOW_MSG_ERROR, "Fatal error", ret);
+	free(ret);
+	exit(1);
+}
+
+void Warn(const char* format, ...) {
+	char* ret = NULL;
+
+	FMT(ret, format);
+
+	time_t     rawTime;
+	struct tm* tm;
+	
+	time(&rawTime);
+	tm = localtime(&rawTime);
+	
+	printf("[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, ret);
+	if (logFile) {
+		fprintf(
+			logFile, "[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec,
+			ret
+		);
+		fflush(logFile);
+	}
+
+	Window_MessageBox(WINDOW_MSG_WARNING, "Warning", ret);
+	free(ret);
+}




diff --git a/source/log.h b/source/log.h
new file mode 100644
index 0000000000000000000000000000000000000000..61d7a2c15d8e3524b6a9edfe980798324053b6c2
--- /dev/null
+++ b/source/log.h
@@ -0,0 +1,10 @@
+#ifndef YT_LOG_H
+#define YT_LOG_H
+
+void Log_Init(void);
+void Log_End(void);
+void Log(const char* format, ...);
+void Error(const char* format, ...);
+void Warn(const char* format, ...);
+
+#endif




diff --git a/source/main.c b/source/main.c
new file mode 100644
index 0000000000000000000000000000000000000000..061ed7eefd8af6f58434de4e2c9fec463f6bf076
--- /dev/null
+++ b/source/main.c
@@ -0,0 +1,3 @@
+int main(void) {
+	return 0;
+}




diff --git a/source/mem.c b/source/mem.c
new file mode 100644
index 0000000000000000000000000000000000000000..bcef6460e47b9108ddcc2f316a9836fd77855b03
--- /dev/null
+++ b/source/mem.c
@@ -0,0 +1,27 @@
+#include <stdio.h>
+#include "mem.h"
+#include "common.h"
+
+void* SafeMalloc(size_t size) {
+	if (size == 0) return NULL;
+
+	void* ret = malloc(size);
+
+	if (ret == NULL) {
+		fprintf(stderr, "Malloc returned NULL\n");
+		exit(1);
+	}
+
+	return ret;
+}
+
+void* SafeRealloc(void* ptr, size_t size) {
+	void* ret = realloc(ptr, size);
+
+	if ((ret == NULL) && (size > 0)) {
+		fprintf(stderr, "Malloc returned NULL\n");
+		exit(1);
+	}
+
+	return ret;
+}




diff --git a/source/mem.h b/source/mem.h
new file mode 100644
index 0000000000000000000000000000000000000000..893c10a9495d957ef276fd4cd03e691afbc2b6c5
--- /dev/null
+++ b/source/mem.h
@@ -0,0 +1,11 @@
+#ifndef YT_MEM_H
+#define YT_MEM_H
+
+#include "common.h"
+
+#define NEW(T) (T*) SafeMalloc(sizeof(T))
+
+void* SafeMalloc(size_t size);
+void* SafeRealloc(void* ptr, size_t size);
+
+#endif




diff --git a/source/platform/unix.c b/source/platform/unix.c
new file mode 100644
index 0000000000000000000000000000000000000000..0aebddacddcfbf87717fc98f0f4b2f6036917ebe
--- /dev/null
+++ b/source/platform/unix.c
@@ -0,0 +1,20 @@
+#include "../platform.h"
+
+#if defined(PLATFORM_UNIX) || defined(PLATFORM_LINUX)
+#include <time.h>
+
+void Platform_Init(void) {
+	
+}
+
+void Platform_Quit(void) {
+	
+}
+
+uint64_t Platform_GetTime(void) {
+	struct timespec time;
+	clock_gettime(CLOCK_MONOTONIC, &time);
+	return time.tv_sec * 1000000 + time.tv_nsec / 1000;
+}
+
+#endif




diff --git a/source/platform/windows.c b/source/platform/windows.c
new file mode 100644
index 0000000000000000000000000000000000000000..7ffdf922ebe4bba533e7ed668984c87316a48a12
--- /dev/null
+++ b/source/platform/windows.c
@@ -0,0 +1,46 @@
+#include "../platform.h"
+
+#ifdef PLATFORM_WINDOWS
+#include <windows.h>
+
+static UINT          timerRes = 1;
+static LARGE_INTEGER perfFreq;
+
+void Platform_Init(void) {
+	// increase the timer res to the max to make timing stuff and waiting
+	// more accurate
+	TIMECAPS tc;
+
+	if (timeGetDevCaps(&tc, sizeof(tc)) != TIMERR_NOERROR) {
+	    if (timerRes < tc.wPeriodMin) {
+	    	timerRes = tc.wPeriodMin;
+	    }
+	    else if (timerRes > tc.wPeriodMax) {
+	    	timerRes = tc.wPeriodMax;
+	    }
+	}
+	timeBeginPeriod(timerRes);
+
+	LARGE_INTEGER perfFreq;
+	uint64_t      perfMul = 1000000;
+	QueryPerformanceFrequency(&perfFreq);
+	while (!(perfFreq.QuadPart % 10) && !(perfMul % 10)) {
+	    perfFreq.QuadPart /= 10;
+	    perfMul           /= 10;
+	}
+
+	// put the timer res back to the original value before exiting
+	timeEndPeriod(timerRes);
+}
+
+void Platform_Quit(void) {
+	timeEndPeriod(timerRes);
+}
+
+uint64_t Platform_GetTime(void) {
+	LARGE_INTEGER time;
+	QueryPerformanceCounter(&time);
+	return time.QuadPart * perfctmul / perfFreq.QuadPart;
+}
+
+#endif




diff --git a/source/platform.h b/source/platform.h
new file mode 100644
index 0000000000000000000000000000000000000000..27962be4902bfb840b45aa65c2675e3891344f1a
--- /dev/null
+++ b/source/platform.h
@@ -0,0 +1,10 @@
+#ifndef YT_PLATFORM_H
+#define YT_PLATFORM_H
+
+#include "common.h"
+
+void     Platform_Init(void);
+void     Platform_Quit(void);
+uint64_t Platform_GetTime(void);
+
+#endif




diff --git a/source/stb.c b/source/stb.c
new file mode 100644
index 0000000000000000000000000000000000000000..3ccc10ee72e8f8072442ee5d160c1ba1f9686f3b
--- /dev/null
+++ b/source/stb.c
@@ -0,0 +1,7 @@
+#define STB_IMAGE_IMPLEMENTATION
+#include <stb/stb_image.h>
+
+#include <stb/stb_vorbis.c>
+
+// #define STB_IMAGE_RESIZE_IMPLEMENTATION
+// #include <stb/stb_image_resize2.h>




diff --git a/source/stb.h b/source/stb.h
new file mode 100644
index 0000000000000000000000000000000000000000..70e9fa1fa9306a369a2735397b02e3910b1ca3ed
--- /dev/null
+++ b/source/stb.h
@@ -0,0 +1,12 @@
+#ifndef YT_STB_H
+#define YT_STB_H
+
+#include <stb/stb_image.h>
+
+// what the hell stb???
+#define STB_VORBIS_HEADER_ONLY
+#include <stb/stb_vorbis.c>
+
+//#include <stb/stb_image_resize2.h>
+
+#endif




diff --git a/source/texture.c b/source/texture.c
new file mode 100644
index 0000000000000000000000000000000000000000..b3561d5374f6ec03bac1c43635628af9cb17e8b0
--- /dev/null
+++ b/source/texture.c
@@ -0,0 +1,75 @@
+#include "stb.h"
+#include "mem.h"
+#include "util.h"
+#include "texture.h"
+
+Texture* Texture_LoadMem(uint8_t* img, size_t len) {
+	int width, height, ch;
+
+	uint8_t* data = stbi_load_from_memory(img, (int) len, &width, &height, &ch, 0);
+
+	if (data == NULL) {
+		Log("STB error: %s", stbi_failure_reason());
+	}
+
+	if (data == NULL) {
+		Log("Texture_LoadMem: data = NULL");
+		return NULL;
+	}
+
+	int newWidth  = width;
+	int newHeight = height;
+
+	if (width & (width - 1)) {
+		// i don't know what's going on either
+		-- newWidth;
+		newWidth |= newWidth >> 1;
+		newWidth |= newWidth >> 2;
+		newWidth |= newWidth >> 4;
+		newWidth |= newWidth >> 8;
+		newWidth |= newWidth >> 16;
+		++ newWidth;
+	}
+
+	if (height & (height - 1)) {
+		-- newHeight;
+		newHeight |= newHeight >> 1;
+		newHeight |= newHeight >> 2;
+		newHeight |= newHeight >> 4;
+		newHeight |= newHeight >> 8;
+		newHeight |= newHeight >> 16;
+		++ newHeight;
+	}
+
+	uint8_t* data2 = malloc(newWidth * newHeight * ch);
+	if (data2 == NULL) {
+		free(data);
+		Log("Failed to allocate texture");
+		return NULL;
+	}
+
+	memset(data2, 0, newWidth * newHeight * ch);
+
+	for (int y = 0; y < height; ++ y) {
+		memcpy(&data2[(y * newWidth * ch)], &data[(y * width * ch)], width * ch);
+	}
+
+	free(data);
+	return Backend_LoadTexture(data2, width, height, newWidth, newHeight, ch);
+}
+
+Texture* Texture_LoadFile(const char* path) {
+	FILE* file = fopen(path, "rb");
+
+	size_t size;
+	fseek(file, 0, SEEK_END);
+	size = (size_t) ftell(file);
+	fseek(file, 0, SEEK_SET);
+
+	uint8_t* data = SafeMalloc(size);
+	fread(data, 1, size, file); // TODO: check
+
+	Texture* ret = Texture_LoadMem(data, size);
+	free(data);
+	return ret;
+}




diff --git a/source/texture.h b/source/texture.h
new file mode 100644
index 0000000000000000000000000000000000000000..f2227d212533a15f5f3f86980d42b32c689f3723
--- /dev/null
+++ b/source/texture.h
@@ -0,0 +1,9 @@
+#ifndef YT_TEXTURE_H
+#define YT_TEXTURE_H
+
+#include "backend.h"
+
+Texture* Texture_LoadMem(uint8_t* img, size_t len);
+Texture* Texture_LoadFile(const char* path);
+
+#endif




diff --git a/source/types.c b/source/types.c
new file mode 100644
index 0000000000000000000000000000000000000000..29f7509b45fdbcff111ca20be150c99ade12788d
--- /dev/null
+++ b/source/types.c
@@ -0,0 +1,7 @@
+#include "types.h"
+
+bool PointInRect(Vec2 point, Rect rect) {
+	return
+		(point.x >= rect.x) && (point.x < rect.x + rect.w) &&
+		(point.y >= rect.y) && (point.y < rect.y + rect.h);
+}




diff --git a/source/types.h b/source/types.h
new file mode 100644
index 0000000000000000000000000000000000000000..73bbf8e8d6471a20ef4b5eab116782fa6531e3d2
--- /dev/null
+++ b/source/types.h
@@ -0,0 +1,34 @@
+#ifndef YT_TYPES_H
+#define YT_TYPES_H
+
+#include "common.h"
+
+typedef struct {
+	int x, y;
+} Vec2;
+
+typedef struct {
+	float x, y;
+} FVec2;
+
+typedef struct {
+	int x, y, w, h;
+} Rect;
+
+typedef struct {
+	float x, y, w, h;
+} FRect;
+
+typedef struct {
+	float x, y, z;
+} FVec3;
+
+typedef struct {
+	float pitch;
+	float yaw;
+	float roll;
+} Direction;
+
+bool PointInRect(Vec2 point, Rect rect);
+
+#endif




diff --git a/source/util.c b/source/util.c
new file mode 100644
index 0000000000000000000000000000000000000000..fadf402cb1db4cf9f705ed37a0562bfed606f6d8
--- /dev/null
+++ b/source/util.c
@@ -0,0 +1,197 @@
+#include <stdio.h>
+#include <stdarg.h>
+#include <time.h>
+#include <string.h>
+#include "mem.h"
+#include "util.h"
+#include "video.h"
+#include "common.h"
+
+float RadToDeg(float rad) {
+	return rad * 180 / PI;
+}
+
+float DegToRad(float deg) {
+	return deg * (PI / 180);
+}
+
+float Lerp(float a, float b, float t) {
+	return a * (1.0 - t) + (b * t);
+}
+
+FVec2 LerpVec(FVec2 a, FVec2 b, float t) {
+	return (FVec2) {Lerp(a.x, b.x, t), Lerp(a.y, b.y, t)};
+}
+
+float CosDeg(float deg) {
+	return cos(DegToRad(deg));
+}
+
+float SinDeg(float deg) {
+	return sin(DegToRad(deg));
+}
+
+bool FloatEqual(float a, float b, float margin) {
+	return fabsf(a - b) < margin;
+}
+
+float Distance(FVec2 a, FVec2 b) {
+	float x = (b.x - a.x) * (b.x - a.x);
+	float y = (b.y - a.y) * (b.y - a.y);
+	return sqrtf(x + y);
+}
+
+float DistanceI(Vec2 a, Vec2 b) {
+	return Distance(
+		(FVec2) {(float) a.x, (float) a.y}, (FVec2) {(float) b.x, (float) b.y}
+	);
+}
+
+float GetAngle(FVec2 a, FVec2 b) {
+	return RadToDeg(atan2(b.y - a.y, b.x - a.x));
+}
+
+#define CROSS_PRODUCT(X1, Y1, X2, Y2) ((X1) * (Y2) - (X2) * (Y1))
+
+static double PointCrossProduct(FVec2 a, FVec2 b) {
+	return CROSS_PRODUCT(a.x, a.y, b.x, b.y);
+}
+
+FVec2 LineIntersect(FVec2 a1, FVec2 b1, FVec2 b2, FVec2 a2) {
+	double vxp1 = PointCrossProduct(a1, b1);
+	double vxp2 = PointCrossProduct(a2, b2);
+	double vxp3 = CROSS_PRODUCT(a1.x - b1.x, a1.y - b1.y, a2.x - b2.x, a2.y - b2.y);
+	return (FVec2) {
+		CROSS_PRODUCT(vxp1, a1.x - b1.x, vxp2, a2.x - b2.x) / vxp3,
+		CROSS_PRODUCT(vxp1, a1.y - b1.y, vxp2, a2.y - b2.y) / vxp3,
+	};
+}
+
+float PointLineSide(FVec2 p, FVec2 a, FVec2 b) {
+    return CROSS_PRODUCT(b.x - a.x, b.y - a.y, p.x - a.x, p.y - a.y);
+}
+
+#define BOUND_MARGIN 0.001
+bool PointInLine(FVec2 p, FVec2 a, FVec2 b) {
+	if (a.x > b.x) SWAP(float, a.x, b.x);
+
+	if ((p.x < a.x - BOUND_MARGIN) || (p.x > b.x + BOUND_MARGIN)) return false;
+
+	if (a.y > b.y) SWAP(float, a.y, b.y);
+
+	if ((p.y < a.y - BOUND_MARGIN) || (p.y > b.y + BOUND_MARGIN)) return false;
+
+	return true;
+}
+#undef BOUND_MARGIN
+
+bool RectLineCollision(FVec2 a, FVec2 b, FRect rect, FVec2* res) {
+	FVec2 lines[5] = {
+		{rect.x, rect.y}, {rect.x + rect.w, rect.y},
+		{rect.x + rect.w, rect.y + rect.h}, {rect.x, rect.y + rect.h}
+	};
+	lines[4] = lines[0];
+
+	FVec2 center = {rect.x + (rect.w / 2), rect.y + (rect.h / 2)};
+
+	for (int i = 0; i < 4; ++ i) {
+		FVec2 intersection = LineIntersect(lines[i], lines[i + 1], a, b);
+
+		if (PointInLine(intersection, lines[i], lines[i + 1])) {
+			*res = (FVec2) {
+				intersection.x - center.x, intersection.y - center.y
+			};
+			return true;
+		}
+	}
+
+	return false;
+}
+
+float LinePointDistance(FVec2 la, FVec2 lb, FVec2 point) {
+	float a = -(lb.y - la.y);
+	float b = lb.x - la.x;
+	float c = -(a * la.x) - (b * la.y);
+
+	float dist  = (a * point.x) + (b * point.y) + c;
+	dist        = fabsf(dist);
+	dist       /= sqrtf((a * a) + (b * b));
+
+	return dist;
+}
+
+char* NewString(const char* src) {
+	char* ret = SafeMalloc(strlen(src) + 1);
+	strcpy(ret, src);
+	return ret;
+}
+
+char* ConcatString(const char* first, const char* second) {
+	char* ret = SafeMalloc(strlen(first) + strlen(second) + 1);
+	strcpy(ret, first);
+	strcat(ret, second);
+	return ret;
+}
+
+size_t StrArrayLength(char** array) {
+	size_t ret = 0;
+
+	while (*array != NULL) {
+		++ ret;
+		++ array;
+	}
+
+	return ret;
+}
+
+char** AppendStrArray(char** array, char* string) {
+	size_t len = StrArrayLength(array);
+
+	array          = SafeRealloc(array, (len + 2) * sizeof(char*));
+	array[len]     = string;
+	array[len + 1] = NULL;
+	return array;
+}
+
+bool StrArrayContains(char** array, char* string) {
+	while (true) {
+		if (*array == NULL) {
+			return false;
+		}
+		else if (strcmp(*array, string) == 0) {
+			return true;
+		}
+
+		++ array;
+	}
+}
+
+size_t StrArrayFind(char** array, char* string) {
+	for (size_t i = 0; array[i]; ++ i) {
+		if (strcmp(array[i], string) == 0) {
+			return i;
+		}
+	}
+
+	assert(0);
+}
+
+void FreeStrArray(char** array) {
+	for (size_t i = 0; array[i] != NULL; ++ i) {
+		free(array[i]);
+	}
+
+	free(array);
+}
+
+const char* BaseName(const char* path) {
+	char* ret = strrchr(path, '/');
+
+	if (ret) return ret + 1;
+
+	ret = strrchr(path, ':');
+
+	if (ret) return ret + 1;
+
+	return ret;
+}




diff --git a/source/util.h b/source/util.h
new file mode 100644
index 0000000000000000000000000000000000000000..359c5143f8c851ec23de79ac354f923d14a1ae73
--- /dev/null
+++ b/source/util.h
@@ -0,0 +1,48 @@
+#ifndef YT_UTIL_H
+#define YT_UTIL_H
+
+#include <math.h>
+#include <stdio.h>
+#include "log.h"
+#include "types.h"
+
+#define FUNCTION_POINTER(TYPE, NAME, ...) TYPE (*NAME)(__VA_ARGS__)
+#define MIN(A, B) (((A) < (B))? (A) : (B))
+#define MAX(A, B) (((A) > (B))? (A) : (B))
+
+#define SWAP(TYPE, A, B) \
+	do { \
+		TYPE* macro##a   = &(A); \
+		TYPE* macro##b   = &(B); \
+		TYPE  macro##tmp = *macro##a; \
+		*macro##a = *macro##b; \
+		*macro##b = macro##tmp; \
+	} while (0);
+
+bool   FileExists(const char* path);
+float  RadToDeg(float rad);
+float  DegToRad(float deg);
+float  Lerp(float a, float b, float t);
+FVec2  LerpVec(FVec2 a, FVec2 b, float t);
+float  CosDeg(float deg);
+float  SinDeg(float deg);
+bool   FloatEqual(float a, float b, float margin);
+float  Distance(FVec2 a, FVec2 b);
+float  DistanceI(Vec2 a, Vec2 b);
+float  GetAngle(FVec2 a, FVec2 b);
+float  PointLineSide(FVec2 p, FVec2 a, FVec2 b);
+FVec2  LineIntersect(FVec2 a1, FVec2 a2, FVec2 b1, FVec2 b2);
+bool   PointInLine(FVec2 p, FVec2 a, FVec2 b);
+bool   RectLineCollision(FVec2 a, FVec2 b, FRect rect, FVec2* res);
+float  LinePointDistance(FVec2 la, FVec2 lb, FVec2 point);
+char*  NewString(const char* src);
+char*  ConcatString(const char* first, const char* second);
+size_t StrArrayLength(char** array);
+char** AppendStrArray(char** array, char* string);
+bool   StrArrayContains(char** array, char* string);
+size_t StrArrayFind(char** array, char* string);
+void   FreeStrArray(char** array);
+
+const char* BaseName(const char* path);
+
+#endif




diff --git a/source/video.c b/source/video.c
new file mode 100644
index 0000000000000000000000000000000000000000..b3595cafb47ea9807b7caeec0d31ff66ab7ee5be
--- /dev/null
+++ b/source/video.c
@@ -0,0 +1,71 @@
+#include "util.h"
+#include "event.h"
+#include "video.h"
+#include "window.h"
+#include "backend.h"
+
+Video video;
+
+static void EventHandler(Event* e) {
+	Window* win = &video.windows[e->windowResize.window]; // TODO: check
+
+	win->aWidth  = e->windowResize.width;
+	win->aHeight = e->windowResize.height;
+
+	win->width  = win->aWidth;
+	win->height = win->aHeight;
+}
+
+void Video_Init(const char* windowName) {
+	Backend_Init(true);
+
+	// options
+	video.fog = false;
+
+	int w;
+	int h;
+
+	w = 640;
+	h = 480;
+
+	video.windows[0]        = Window_Create(windowName, w, h);
+	video.windows[0].width  = w;
+	video.windows[0].height = h;
+	Log("Created window 0");
+
+	Backend_Init(false);
+
+	Event_AddHandler(EVENT_WINDOW_RESIZE, &EventHandler);
+}
+
+void Video_Free(void) {
+	Backend_Free();
+	for (int i = 0; i < WIN_NUM; ++ i) {
+		Window_Free(&video.windows[i]);
+	}
+}
+
+Colour Video_MultiplyColour(Colour colour, float by) {
+	float r = ((float) colour.r) / 255.0;
+	float g = ((float) colour.g) / 255.0;
+	float b = ((float) colour.b) / 255.0;
+
+	r *= by;
+	g *= by;
+	b *= by;
+
+	if (r > 1.0) r = 1.0;
+	if (g > 1.0) g = 1.0;
+	if (b > 1.0) b = 1.0;
+
+	if (r < 0.0) r = 0.0;
+	if (g < 0.0) g = 0.0;
+	if (b < 0.0) b = 0.0;
+
+	return (Colour) {
+		(uint8_t) (r * 255.0),
+		(uint8_t) (g * 255.0),
+		(uint8_t) (b * 255.0),
+		colour.a
+	};
+}




diff --git a/source/video.h b/source/video.h
new file mode 100644
index 0000000000000000000000000000000000000000..17618f53bdf71f0b5a2d0fdb908d62488a8dffa5
--- /dev/null
+++ b/source/video.h
@@ -0,0 +1,27 @@
+#ifndef YT_VIDEO_H
+#define YT_VIDEO_H
+
+#include "common.h"
+#include "window.h"
+
+#undef main
+
+#define WIN_NUM 1
+
+typedef struct {
+	uint8_t r, g, b, a;
+} Colour;
+
+
+typedef struct {
+	Window windows[WIN_NUM];
+	bool   fog;
+} Video;
+
+extern Video video;
+
+void   Video_Init(const char* windowName);
+void   Video_Free(void);
+Colour Video_MultiplyColour(Colour colour, float by);
+
+#endif




diff --git a/source/window/sdl2.c b/source/window/sdl2.c
new file mode 100644
index 0000000000000000000000000000000000000000..9915b692813c27577b32273d9aec1d558fed8433
--- /dev/null
+++ b/source/window/sdl2.c
@@ -0,0 +1,72 @@
+#include "../util.h"
+#include "../window.h"
+
+#ifdef YT_WINDOW_SDL2
+#include "sdl2.h"
+
+const bool windowRequired = true;
+
+static bool cursorVisible     = true;
+static bool relativeMouseMode = false;
+
+void Window_Init(void) {
+	if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
+		Log("Failed to initialise SDL2: %s", SDL_GetError());
+		exit(1);
+	}
+}
+
+void Window_Quit(void) {
+	SDL_Quit();
+}
+
+Window Window_Create(const char* name, int width, int height) {
+	Window ret;
+	ret.window = SDL_CreateWindow(
+		name, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height,
+		SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
+	);
+	ret.aWidth  = width;
+	ret.aHeight = height;
+	ret.width   = width;
+	ret.height  = height;
+
+	if (ret.window == NULL) {
+		Log("Failed to create window: %s", SDL_GetError());
+		exit(1);
+	}
+
+	return ret;
+}
+
+void Window_Free(Window* window) {
+	SDL_DestroyWindow(window->window);
+}
+
+void Window_ShowCursor(bool show) {
+	if (cursorVisible != show) {
+		SDL_ShowCursor(show? SDL_ENABLE : SDL_DISABLE);
+	}
+
+	cursorVisible = show;
+}
+
+void Window_SetRelativeMouseMode(bool enable) {
+	if (relativeMouseMode != enable) {
+		SDL_SetRelativeMouseMode(enable? SDL_TRUE : SDL_FALSE);
+	}
+
+	relativeMouseMode = enable;
+}
+
+void Window_MessageBox(int type, const char* title, const char* contents) {
+	uint32_t flags;
+	switch (type) {
+		case WINDOW_MSG_WARNING: flags = SDL_MESSAGEBOX_WARNING; break;
+		case WINDOW_MSG_ERROR:   flags = SDL_MESSAGEBOX_ERROR;   break;
+	}
+
+	SDL_ShowSimpleMessageBox(flags, title, contents, NULL);
+}
+
+#endif




diff --git a/source/window/sdl2.h b/source/window/sdl2.h
new file mode 100644
index 0000000000000000000000000000000000000000..0ae0a0630016ec5d954ab9e235747ef3cfad1c1a
--- /dev/null
+++ b/source/window/sdl2.h
@@ -0,0 +1,12 @@
+#ifndef YT_WINDOW_SDL2_H
+#define YT_WINDOW_SDL2_H
+
+#include <SDL2/SDL.h>
+
+typedef struct {
+	WINDOW_COMMON;
+
+	SDL_Window* window;
+} Window;
+
+#endif




diff --git a/source/window.h b/source/window.h
new file mode 100644
index 0000000000000000000000000000000000000000..61094ff8edcd0d1d6e14df8e1233ab4fc626fe11
--- /dev/null
+++ b/source/window.h
@@ -0,0 +1,29 @@
+#ifndef YT_WINDOW_H
+#define YT_WINDOW_H
+
+#include "common.h"
+
+#define WINDOW_COMMON \
+	int width, height; \
+	int aWidth, aHeight
+
+#ifdef YT_WINDOW_SDL2
+	#include "window/sdl2.h"
+#elif defined(PLATFORM_3DS)
+	#include "window/ae3ds.h"
+#endif
+
+enum {
+	WINDOW_MSG_WARNING = 0,
+	WINDOW_MSG_ERROR
+};
+
+void   Window_Init(void);
+void   Window_Quit(void);
+Window Window_Create(const char* name, int width, int height);
+void   Window_Free(Window* window);
+void   Window_ShowCursor(bool show);
+void   Window_SetRelativeMouseMode(bool enable);
+void   Window_MessageBox(int type, const char* title, const char* contents);
+
+#endif