shithub: wipeout

Download patch

ref: 7cfaff0c7b618e8150bd97ad43fca9899f4a5514
author: Dominic Szablewski <dominic@phoboslab.org>
date: Thu Aug 10 09:34:04 EDT 2023

Initial

diff: cannot open b/src/libs//null: file does not exist: 'b/src/libs//null' diff: cannot open b/src/wipeout//null: file does not exist: 'b/src/wipeout//null' diff: cannot open b/src//null: file does not exist: 'b/src//null'
--- /dev/null
+++ b/.gitignore
@@ -1,0 +1,9 @@
+/wipeout.sublime-project
+/wipeout.sublime-workspace
+
+/wipeout/
+/build/
+
+/wipegame
+/save.dat
+.DS_STORE
--- /dev/null
+++ b/Makefile
@@ -1,0 +1,166 @@
+CC ?= gcc
+EMCC ?= emcc
+UNAME_S := $(shell uname -s)
+RENDERER ?= GL
+DEBUG ?= false
+
+L_FLAGS ?= -lm -rdynamic
+C_FLAGS ?= -std=gnu99 -Wall -Wno-unused-variable
+
+ifeq ($(DEBUG), true)
+	C_FLAGS := $(C_FLAGS) -g
+else
+	C_FLAGS := $(C_FLAGS) -O3
+endif
+
+
+# Rendeder ---------------------------------------------------------------------
+
+ifeq ($(RENDERER), GL)
+	RENDERER_SRC = src/render_gl.c
+	C_FLAGS := $(C_FLAGS) -DRENDERER_GL
+else
+$(error Unknown RENDERER)
+endif
+
+
+
+
+# macOS ------------------------------------------------------------------------
+
+ifeq ($(UNAME_S), Darwin)
+	C_FLAGS := $(C_FLAGS) -x objective-c -I/opt/homebrew/include -D_THREAD_SAFE -w
+	L_FLAGS := $(L_FLAGS) -L/opt/homebrew/lib -framework Foundation
+
+	ifeq ($(RENDERER), GL)
+		L_FLAGS := $(L_FLAGS) -lGLEW -GLU -framework OpenGL
+	endif
+
+	L_FLAGS_SDL = -lSDL2
+	L_FLAGS_SOKOL = -framework Cocoa -framework QuartzCore -framework AudioToolbox
+
+
+# Linux ------------------------------------------------------------------------
+
+else ifeq ($(UNAME_S), Linux)
+	ifeq ($(RENDERER), GL)
+		L_FLAGS := $(L_FLAGS) -lGLEW -lGL
+	endif
+
+	L_FLAGS_SDL = -lSDL2
+	L_FLAGS_SOKOL = -lX11 -lXcursor -pthread -lXi -ldl -lasound
+
+
+# Windows ----------------------------------------------------------------------
+
+else ifeq ($(OS), Windows_NT)
+$(error TODO: FLAGS for windows have not been set up. Please modify this makefile and send a PR!)
+
+
+
+else
+$(error Unknown environment)
+endif
+
+
+
+# Source files -----------------------------------------------------------------
+
+TARGET_NATIVE ?= wipegame
+BUILD_DIR = build/obj/native
+BUILD_DIR_WASM = build/obj/wasm
+
+WASM_RELEASE_DIR ?= build/wasm
+TARGET_WASM ?= $(WASM_RELEASE_DIR)/wipeout.js
+TARGET_WASM_MINIMAL ?= $(WASM_RELEASE_DIR)/wipeout-minimal.js
+
+COMMON_SRC = \
+	src/wipeout/race.c \
+	src/wipeout/camera.c \
+	src/wipeout/object.c \
+	src/wipeout/droid.c \
+	src/wipeout/ui.c \
+	src/wipeout/hud.c \
+	src/wipeout/image.c \
+	src/wipeout/game.c \
+	src/wipeout/menu.c \
+	src/wipeout/main_menu.c \
+	src/wipeout/ingame_menus.c \
+	src/wipeout/title.c \
+	src/wipeout/intro.c \
+	src/wipeout/scene.c \
+	src/wipeout/ship.c \
+	src/wipeout/ship_ai.c \
+	src/wipeout/ship_player.c \
+	src/wipeout/track.c \
+	src/wipeout/weapon.c \
+	src/wipeout/particle.c \
+	src/wipeout/sfx.c \
+	src/utils.c \
+	src/types.c \
+	src/system.c \
+	src/mem.c \
+	src/input.c \
+	$(RENDERER_SRC)
+
+
+# Targets native ---------------------------------------------------------------
+
+COMMON_OBJ = $(patsubst %.c, $(BUILD_DIR)/%.o, $(COMMON_SRC))
+
+sdl: $(BUILD_DIR)/src/platform_sdl.o
+sdl: $(COMMON_OBJ)
+	$(CC) $^ -o $(TARGET_NATIVE) $(L_FLAGS) $(L_FLAGS_SDL)
+
+sokol: $(BUILD_DIR)/src/platform_sokol.o
+sokol: $(COMMON_OBJ)
+	$(CC) $^ -o $(TARGET_NATIVE) $(L_FLAGS) $(L_FLAGS_SOKOL)
+
+$(BUILD_DIR):
+	mkdir -p $(BUILD_DIR)
+
+$(BUILD_DIR)/%.o: %.c
+	mkdir -p $(dir $@)
+	$(CC) $(C_FLAGS) -c $< -o $@
+
+
+# Targets wasm -----------------------------------------------------------------
+
+COMMON_OBJ_WASM = $(patsubst %.c, $(BUILD_DIR_WASM)/%.o, $(COMMON_SRC))
+wasm: wasm_full wasm_minimal
+	cp src/wasm-index.html $(WASM_RELEASE_DIR)/game.html
+
+
+wasm_full: $(BUILD_DIR_WASM)/src/platform_sokol.o
+wasm_full: $(COMMON_OBJ_WASM)
+	mkdir -p $(WASM_RELEASE_DIR)
+	$(EMCC) $^ -o $(TARGET_WASM) -lGLEW -lGL \
+		-s ALLOW_MEMORY_GROWTH=1 \
+		-s ENVIRONMENT=web \
+		--preload-file wipeout
+
+wasm_minimal: $(BUILD_DIR_WASM)/src/platform_sokol.o
+wasm_minimal: $(COMMON_OBJ_WASM)
+	mkdir -p $(WASM_RELEASE_DIR)
+	$(EMCC) $^ -o $(TARGET_WASM_MINIMAL) -lGLEW -lGL \
+		-s ALLOW_MEMORY_GROWTH=1 \
+		-s ENVIRONMENT=web \
+		--preload-file wipeout \
+		--exclude-file wipeout/music \
+		--exclude-file wipeout/intro.mpeg
+	
+$(BUILD_DIR_WASM):
+	mkdir -p $(BUILD_DIR_WASM)
+
+$(BUILD_DIR_WASM)/%.o: %.c
+	mkdir -p $(dir $@)
+	$(EMCC) $(C_FLAGS) -c $< -o $@
+
+
+
+
+
+
+.PHONY: clean
+clean:
+	$(RM) -rf $(BUILD_DIR) $(BUILD_DIR_WASM) $(WASM_RELEASE_DIR)
--- /dev/null
+++ b/README.md
@@ -1,0 +1,126 @@
+# wipEout Rewrite
+
+This is a re-implementation of the 1995 PSX game wipEout.
+
+Play here: https://phoboslab.org/wipegame/
+
+More info in my blog: https://phoboslab.org/log/2023/08/rewriting-wipeout
+
+
+⚠️ Work in progress. Expect bugs.
+
+
+## Building
+
+The game currently supports two different platform-backends: [SDL2](https://github.com/libsdl-org/SDL) and [Sokol](https://github.com/floooh/sokol). The only difference in features is that the SDL2 backend supports game controllers (joysticks, gamepads), while the Sokol backend does not.
+
+
+### Linux
+
+```
+# for SDL2 backend
+apt install libsdl2-dev
+make sdl
+```
+
+```
+# for Sokol backend
+apt install libx11-dev libxcursor-dev libxi-dev libasound2-dev
+make sokol
+```
+
+### macOS
+
+Currently only the SDL2 backend works. macOS is very picky about the GLSL shader version when compiling with Sokol and OpenGL3.3; it shouldn't be too difficult to get it working, but will probably require a bunch of `#ifdefs` for SDL and WASM. PRs welcome!
+
+```
+brew install sdl2
+make sdl
+```
+
+### Windows
+
+In theory both backends should work on Windows, but the Makefile is missing the proper compiler flags. Please send a PR!
+
+_todo_
+
+
+### WASM
+
+Install [emscripten](https://emscripten.org/) and activate emsdk, so that `emcc` is in your `PATH`. The WASM version automatically
+selects the Sokol backend. I'm not sure what needs to be done to make the SDL2 backend work with WASM.
+
+```
+make wasm
+```
+
+This builds the minimal version (no music, no intro) as well as the full version.
+
+
+## Running
+
+This repository does not contain the assets (textures, 3d models etc.) required to run the game. This code mostly assumes to have the PSX NTSC data, but some menu models from the PC version are loaded as well. Both of these can be easily found on archive.org and similar sites. The music (optional) needs to be provided in [QOA format](https://github.com/phoboslab/qoa). The intro video as MPEG1.
+
+The directory structure is assumed to be as follows
+
+```
+./wipegame # the executable
+./wipeout/textures/
+./wipeout/music/track01.qoa
+./wipeout/music/track02.qoa
+...
+```
+
+Note that the blog post announcing this project may or may not provide a link to a ZIP containing all files needed. Who knows!
+
+
+
+## Ideas for improvements
+
+PRs Welcome.
+
+### Not yet implemented
+
+Some things from the original game are not yet implemented in this rewrite. This includes
+
+- screen shake effect
+- game-end animations, formerly `Spline.cpp` (the end messages are just shown over the attract mode cameras)
+- viewing highscores in options menu
+- controller options menu
+- reverb for sfx and music when there's more than 4 track faces (tunnels and such)
+- some more? grep the source for `TODO` and `FIXME`
+
+### Gameplay, Visuals
+
+- less punishing physics for ship vs. ship collisions
+- less punishing physics for sideways ship vs. track collisions (i.e. wall grinding like in newer wipEouts)
+- somehow resolve the issue of inevitably running into an enemy that you just shot
+- add option to lessen the roll in the internal view
+- add additional external view that behaves more like in modern racing games
+- dynamic lighting on ships
+- allow lower resolutions and a drawing mode that resembles the PSX original
+- the scene geometry could use some touch-ups to make an infinite draw distance option less awkward
+- increase FOV when going over a boost
+- better menu models for game exit and video options
+- gamepad analog input feels like balancing an egg
+- fix collision issues on junctions (also present in the original)
+
+### Technical
+
+- implement frustum culling for scene geometry, the track and ships. Currently everything within the fadeout radius is drawn.
+- put all static geometry into a GPU-side buffer. Currently all triangles are constructed at draw time. Uploading geometry is complicated a bit by the fact that some scene animations and the ship's exhaust need to update geometry for each frame.
+- the menu system is... not great. It's better than the 5000 lines of spaghetti that it was before, but the different layouts need a lot of `if`s
+- the save data is just dumping the whole struct on disk. A textual format would be preferable.
+- since this whole thing is relying on some custom assembled assets anyway, maybe all SFX should be in QOA format too (like the music). Or switch everything to Vorbis.
+- a lot of functions assume that there's just one player. This needs to be fixed for a potential splitscreen mode.
+
+
+## License
+
+There is none. This code may or may not be based on the source code of the PC (ATI-Rage) version that was leaked in 2022. If it were, it would probably violate copyright law, but it may also fall under fair use ¯\\\_(ツ)\_/¯
+
+Working with this source code is probably fine, considering that this game was originally released 28 years ago (in 1995), that the current copyright holders historically didn't care about any wipEout related files or code being available on the net and that the game is currently not purchasable in any shape or form.
+
+In any case, you may NOT use this source code in a commercial release. A commercial release includes hosting it on a website that shows any forms of advertising.
+
+PS.: Hey Sony! If you're reading this, I would love to work on a proper, officially sanctioned remaster. Please get in touch <3
--- /dev/null
+++ b/src/input.c
@@ -1,0 +1,299 @@
+#include <string.h>
+
+#include "input.h"
+#include "utils.h"
+
+static const char *button_names[] = {
+	NULL, 
+	NULL, 
+	NULL, 
+	NULL,
+	[INPUT_KEY_A] = "a",
+	[INPUT_KEY_B] = "b",
+	[INPUT_KEY_C] = "c",
+	[INPUT_KEY_D] = "d",
+	[INPUT_KEY_E] = "e",
+	[INPUT_KEY_F] = "f",
+	[INPUT_KEY_G] = "g",
+	[INPUT_KEY_H] = "h",
+	[INPUT_KEY_I] = "i",
+	[INPUT_KEY_J] = "j",
+	[INPUT_KEY_K] = "k",
+	[INPUT_KEY_L] = "l",
+	[INPUT_KEY_M] = "m",
+	[INPUT_KEY_N] = "n",
+	[INPUT_KEY_O] = "o",
+	[INPUT_KEY_P] = "p",
+	[INPUT_KEY_Q] = "q",
+	[INPUT_KEY_R] = "r",
+	[INPUT_KEY_S] = "s",
+	[INPUT_KEY_T] = "t",
+	[INPUT_KEY_U] = "u",
+	[INPUT_KEY_V] = "v",
+	[INPUT_KEY_W] = "w",
+	[INPUT_KEY_X] = "x",
+	[INPUT_KEY_Y] = "y",
+	[INPUT_KEY_Z] = "z",
+	[INPUT_KEY_1] = "1",
+	[INPUT_KEY_2] = "2",
+	[INPUT_KEY_3] = "3",
+	[INPUT_KEY_4] = "4",
+	[INPUT_KEY_5] = "5",
+	[INPUT_KEY_6] = "6",
+	[INPUT_KEY_7] = "7",
+	[INPUT_KEY_8] = "8",
+	[INPUT_KEY_9] = "9",
+	[INPUT_KEY_0] = "0",
+	[INPUT_KEY_RETURN] = "return",
+	[INPUT_KEY_ESCAPE] = "escape",
+	[INPUT_KEY_BACKSPACE] = "backspace",
+	[INPUT_KEY_TAB] = "tab",
+	[INPUT_KEY_SPACE] = "space",
+	[INPUT_KEY_MINUS] = "minus",
+	[INPUT_KEY_EQUALS] = "equals",
+	[INPUT_KEY_LEFTBRACKET] = "left_bracket",
+	[INPUT_KEY_RIGHTBRACKET] = "right_bracket",
+	[INPUT_KEY_BACKSLASH] = "backslash",
+	[INPUT_KEY_HASH] = "hash",
+	[INPUT_KEY_SEMICOLON] = "semicolon",
+	[INPUT_KEY_APOSTROPHE] = "apostrophe",
+	[INPUT_KEY_TILDE] = "tilde",
+	[INPUT_KEY_COMMA] = "comma",
+	[INPUT_KEY_PERIOD] = "period",
+	[INPUT_KEY_SLASH] = "slash",
+	[INPUT_KEY_CAPSLOCK] = "capslock",
+	[INPUT_KEY_F1] = "f1",
+	[INPUT_KEY_F2] = "f2",
+	[INPUT_KEY_F3] = "f3",
+	[INPUT_KEY_F4] = "f4",
+	[INPUT_KEY_F5] = "f5",
+	[INPUT_KEY_F6] = "f6",
+	[INPUT_KEY_F7] = "f7",
+	[INPUT_KEY_F8] = "f8",
+	[INPUT_KEY_F9] = "f9",
+	[INPUT_KEY_F10] = "f10",
+	[INPUT_KEY_F11] = "f11",
+	[INPUT_KEY_F12] = "f12",
+	[INPUT_KEY_PRINTSCREEN] = "print_screen",
+	[INPUT_KEY_SCROLLLOCK] = "scroll_lock",
+	[INPUT_KEY_PAUSE] = "pause",
+	[INPUT_KEY_INSERT] = "insert",
+	[INPUT_KEY_HOME] = "home",
+	[INPUT_KEY_PAGEUP] = "page_up",
+	[INPUT_KEY_DELETE] = "delete",
+	[INPUT_KEY_END] = "end",
+	[INPUT_KEY_PAGEDOWN] = "page_down",
+	[INPUT_KEY_RIGHT] = "right",
+	[INPUT_KEY_LEFT] = "left",
+	[INPUT_KEY_DOWN] = "down",
+	[INPUT_KEY_UP] = "up",
+	[INPUT_KEY_NUMLOCK] = "num_lock",
+	[INPUT_KEY_KP_DIVIDE] = "keypad_divide",
+	[INPUT_KEY_KP_MULTIPLY] = "keypad_multiply",
+	[INPUT_KEY_KP_MINUS] = "keypad_minus",
+	[INPUT_KEY_KP_PLUS] = "keypad_plus",
+	[INPUT_KEY_KP_ENTER] = "keypad_enter",
+	[INPUT_KEY_KP_1] = "keypad_1",
+	[INPUT_KEY_KP_2] = "keypad_2",
+	[INPUT_KEY_KP_3] = "keypad_3",
+	[INPUT_KEY_KP_4] = "keypad_4",
+	[INPUT_KEY_KP_5] = "keypad_5",
+	[INPUT_KEY_KP_6] = "keypad_6",
+	[INPUT_KEY_KP_7] = "keypad_7",
+	[INPUT_KEY_KP_8] = "keypad_8",
+	[INPUT_KEY_KP_9] = "keypad_9",
+	[INPUT_KEY_KP_0] = "keypad_0",
+	[INPUT_KEY_KP_PERIOD] = "keypad_period",
+
+	[INPUT_KEY_LCTRL] = "left_ctrl",
+	[INPUT_KEY_LSHIFT] = "left_shift",
+	[INPUT_KEY_LALT] = "left_alt",
+	[INPUT_KEY_LGUI] = "left_gui",
+	[INPUT_KEY_RCTRL] = "right_ctrl",
+	[INPUT_KEY_RSHIFT] = "right_shift",
+	[INPUT_KEY_RALT] = "right_alt",
+	NULL,
+	[INPUT_GAMEPAD_A] = "gamepad_a",
+	[INPUT_GAMEPAD_Y] = "gamepad_y",
+	[INPUT_GAMEPAD_B] = "gamepad_b",
+	[INPUT_GAMEPAD_X] = "gamepad_x",
+	[INPUT_GAMEPAD_L_SHOULDER] = "gamepad_left_shoulder",
+	[INPUT_GAMEPAD_R_SHOULDER] = "gamepad_right_shoulder",
+	[INPUT_GAMEPAD_L_TRIGGER] = "gamepad_left_trigger",
+	[INPUT_GAMEPAD_R_TRIGGER] = "gamepad_right_trigger",
+	[INPUT_GAMEPAD_SELECT] = "gamepad_select",
+	[INPUT_GAMEPAD_START] = "gamepad_start",
+	[INPUT_GAMEPAD_L_STICK_PRESS] = "gamepad_left_stick_press",
+	[INPUT_GAMEPAD_R_STICK_PRESS] = "gamepad_right_stick_press",
+	[INPUT_GAMEPAD_DPAD_UP] = "gamepad_dpad_up",
+	[INPUT_GAMEPAD_DPAD_DOWN] = "gamepad_dpad_down",
+	[INPUT_GAMEPAD_DPAD_LEFT] = "gamepad_dpad_left",
+	[INPUT_GAMEPAD_DPAD_RIGHT] = "gamepad_dpad_right",
+	[INPUT_GAMEPAD_HOME] = "gamepad_home",
+	[INPUT_GAMEPAD_L_STICK_UP] = "gamepad_left_stick_up",
+	[INPUT_GAMEPAD_L_STICK_DOWN] = "gamepad_left_stick_down",
+	[INPUT_GAMEPAD_L_STICK_LEFT] = "gamepad_left_stick_left",
+	[INPUT_GAMEPAD_L_STICK_RIGHT] = "gamepad_left_stick_right",
+	[INPUT_GAMEPAD_R_STICK_UP] = "gamepad_right_stick_up",
+	[INPUT_GAMEPAD_R_STICK_DOWN] = "gamepad_right_stick_down",
+	[INPUT_GAMEPAD_R_STICK_LEFT] = "gamepad_right_stick_left",
+	[INPUT_GAMEPAD_R_STICK_RIGHT] = "gamepad_right_stick_right",
+	NULL,
+	[INPUT_MOUSE_LEFT] = "mouse_left",
+	[INPUT_MOUSE_MIDDLE] = "mouse_middle",
+	[INPUT_MOUSE_RIGHT] = "mouse_right",
+	[INPUT_MOUSE_WHEEL_UP] = "mouse_wheel_up",
+	[INPUT_MOUSE_WHEEL_DOWN] = "mouse_wheel_down",
+};
+
+static float actions_state[INPUT_ACTION_MAX];
+static bool actions_pressed[INPUT_ACTION_MAX];
+static bool actions_released[INPUT_ACTION_MAX];
+
+static uint8_t expected_button[INPUT_ACTION_MAX];
+static uint8_t bindings[INPUT_LAYER_MAX][INPUT_BUTTON_MAX];
+
+static input_capture_callback_t capture_callback;
+static void *capture_user;
+
+static int32_t mouse_x;
+static int32_t mouse_y;
+
+void input_init() {
+	input_unbind_all(INPUT_LAYER_SYSTEM);
+	input_unbind_all(INPUT_LAYER_USER);
+}
+
+void input_cleanup() {
+
+}
+
+void input_clear() {
+	clear(actions_pressed);
+	clear(actions_released);
+}
+
+void input_set_layer_button_state(input_layer_t layer, button_t button, float state) {
+	error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
+
+	uint8_t action = bindings[layer][button];
+	if (action == INPUT_ACTION_NONE) {
+		return;
+	}
+
+	uint8_t expected = expected_button[action];
+	if (!expected || expected == button) {
+		state = (state > INPUT_DEADZONE) ? state : 0;
+
+		if (state && !actions_state[action]) {
+			actions_pressed[action] = true;
+			expected_button[action] = button;
+		}
+		else if (!state && actions_state[action]) {
+			actions_released[action] = true;
+			expected_button[action] = INPUT_BUTTON_NONE;
+		}
+		actions_state[action] = state;
+	}
+}
+
+void input_set_button_state(button_t button, float state) {
+	error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
+
+	if (capture_callback) {
+		if (state) {
+			capture_callback(capture_user, button, 0);
+		}
+		return;
+	}
+
+	input_set_layer_button_state(INPUT_LAYER_SYSTEM, button, state);
+	input_set_layer_button_state(INPUT_LAYER_USER, button, state);
+}
+
+void input_set_mouse_pos(int32_t x, int32_t y) {
+	mouse_x = x;
+	mouse_y = y;
+}
+
+void input_capture(input_capture_callback_t cb, void *user) {
+	capture_callback = cb;
+	capture_user = user;
+	clear(actions_state);
+}
+
+void input_textinput(int32_t ascii_char) {
+	if (capture_callback) {
+		capture_callback(capture_user, 0, ascii_char);
+	}
+}
+
+void input_bind(input_layer_t layer, button_t button, uint8_t action) {
+	error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
+	error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
+	error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
+
+	bindings[layer][button] = action;
+}
+
+uint8_t input_bound_to_action(button_t button) {
+	error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
+	return bindings[INPUT_LAYER_USER][button];
+}
+
+void input_unbind(input_layer_t layer, button_t button) {
+	error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
+	error_if(button < 0 || button >= INPUT_BUTTON_MAX, "Invalid input button %d", button);
+
+	bindings[layer][button] = INPUT_ACTION_NONE;
+}
+
+void input_unbind_all(input_layer_t layer) {
+	error_if(layer < 0 || layer >= INPUT_LAYER_MAX, "Invalid input layer %d", layer);
+	
+	for (uint32_t button = 0; button < INPUT_BUTTON_MAX; button++) {
+		input_unbind(layer, button);
+	}
+}
+
+
+float input_state(uint8_t action) {
+	error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
+	return actions_state[action];
+}
+
+
+bool input_pressed(uint8_t action) {
+	error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
+	return actions_pressed[action];
+}
+
+
+bool input_released(uint8_t action) {
+	error_if(action < 0 || action >= INPUT_ACTION_MAX, "Invalid input action %d", action);
+	return actions_released[action];
+}
+
+vec2_t input_mouse_pos() {
+	return vec2(mouse_x, mouse_y);
+}
+
+
+button_t input_name_to_button(const char *name) {
+	for (int32_t i = 0; i < INPUT_BUTTON_MAX; i++) {
+		if (button_names[i] && strcmp(name, button_names[i]) == 0) {
+			return i;
+		}
+	}
+	return INPUT_INVALID;
+}
+
+const char *input_button_to_name(button_t button) {
+	if (
+		button < 0 || button >= INPUT_BUTTON_MAX ||
+		!button_names[button]
+	) {
+		return NULL;
+	}
+	return button_names[button];
+}
--- /dev/null
+++ b/src/input.h
@@ -1,0 +1,190 @@
+#ifndef INPUT_H
+#define INPUT_H
+
+#include "types.h"
+
+typedef enum {
+	INPUT_INVALID = 0,
+	INPUT_KEY_A = 4,
+	INPUT_KEY_B = 5,
+	INPUT_KEY_C = 6,
+	INPUT_KEY_D = 7,
+	INPUT_KEY_E = 8,
+	INPUT_KEY_F = 9,
+	INPUT_KEY_G = 10,
+	INPUT_KEY_H = 11,
+	INPUT_KEY_I = 12,
+	INPUT_KEY_J = 13,
+	INPUT_KEY_K = 14,
+	INPUT_KEY_L = 15,
+	INPUT_KEY_M = 16,
+	INPUT_KEY_N = 17,
+	INPUT_KEY_O = 18,
+	INPUT_KEY_P = 19,
+	INPUT_KEY_Q = 20,
+	INPUT_KEY_R = 21,
+	INPUT_KEY_S = 22,
+	INPUT_KEY_T = 23,
+	INPUT_KEY_U = 24,
+	INPUT_KEY_V = 25,
+	INPUT_KEY_W = 26,
+	INPUT_KEY_X = 27,
+	INPUT_KEY_Y = 28,
+	INPUT_KEY_Z = 29,
+	INPUT_KEY_1 = 30,
+	INPUT_KEY_2 = 31,
+	INPUT_KEY_3 = 32,
+	INPUT_KEY_4 = 33,
+	INPUT_KEY_5 = 34,
+	INPUT_KEY_6 = 35,
+	INPUT_KEY_7 = 36,
+	INPUT_KEY_8 = 37,
+	INPUT_KEY_9 = 38,
+	INPUT_KEY_0 = 39,
+	INPUT_KEY_RETURN = 40,
+	INPUT_KEY_ESCAPE = 41,
+	INPUT_KEY_BACKSPACE = 42,
+	INPUT_KEY_TAB = 43,
+	INPUT_KEY_SPACE = 44,
+	INPUT_KEY_MINUS = 45,
+	INPUT_KEY_EQUALS = 46,
+	INPUT_KEY_LEFTBRACKET = 47,
+	INPUT_KEY_RIGHTBRACKET = 48,
+	INPUT_KEY_BACKSLASH = 49,
+	INPUT_KEY_HASH = 50, 
+	INPUT_KEY_SEMICOLON = 51,
+	INPUT_KEY_APOSTROPHE = 52,
+	INPUT_KEY_TILDE = 53,
+	INPUT_KEY_COMMA = 54,
+	INPUT_KEY_PERIOD = 55,
+	INPUT_KEY_SLASH = 56,
+	INPUT_KEY_CAPSLOCK = 57,
+	INPUT_KEY_F1 = 58,
+	INPUT_KEY_F2 = 59,
+	INPUT_KEY_F3 = 60,
+	INPUT_KEY_F4 = 61,
+	INPUT_KEY_F5 = 62,
+	INPUT_KEY_F6 = 63,
+	INPUT_KEY_F7 = 64,
+	INPUT_KEY_F8 = 65,
+	INPUT_KEY_F9 = 66,
+	INPUT_KEY_F10 = 67,
+	INPUT_KEY_F11 = 68,
+	INPUT_KEY_F12 = 69,
+	INPUT_KEY_PRINTSCREEN = 70,
+	INPUT_KEY_SCROLLLOCK = 71,
+	INPUT_KEY_PAUSE = 72,
+	INPUT_KEY_INSERT = 73,
+	INPUT_KEY_HOME = 74,
+	INPUT_KEY_PAGEUP = 75,
+	INPUT_KEY_DELETE = 76,
+	INPUT_KEY_END = 77,
+	INPUT_KEY_PAGEDOWN = 78,
+	INPUT_KEY_RIGHT = 79,
+	INPUT_KEY_LEFT = 80,
+	INPUT_KEY_DOWN = 81,
+	INPUT_KEY_UP = 82,
+	INPUT_KEY_NUMLOCK = 83,
+	INPUT_KEY_KP_DIVIDE = 84,
+	INPUT_KEY_KP_MULTIPLY = 85,
+	INPUT_KEY_KP_MINUS = 86,
+	INPUT_KEY_KP_PLUS = 87,
+	INPUT_KEY_KP_ENTER = 88,
+	INPUT_KEY_KP_1 = 89,
+	INPUT_KEY_KP_2 = 90,
+	INPUT_KEY_KP_3 = 91,
+	INPUT_KEY_KP_4 = 92,
+	INPUT_KEY_KP_5 = 93,
+	INPUT_KEY_KP_6 = 94,
+	INPUT_KEY_KP_7 = 95,
+	INPUT_KEY_KP_8 = 96,
+	INPUT_KEY_KP_9 = 97,
+	INPUT_KEY_KP_0 = 98,
+	INPUT_KEY_KP_PERIOD = 99,
+
+ 	INPUT_KEY_LCTRL = 100,
+	INPUT_KEY_LSHIFT = 101,
+	INPUT_KEY_LALT = 102,
+	INPUT_KEY_LGUI = 103,
+	INPUT_KEY_RCTRL = 104,
+	INPUT_KEY_RSHIFT = 105,
+	INPUT_KEY_RALT = 106, 
+
+	INPUT_KEY_MAX = 107,
+
+	INPUT_GAMEPAD_A = 108,
+	INPUT_GAMEPAD_Y = 109,
+	INPUT_GAMEPAD_B = 110,
+	INPUT_GAMEPAD_X = 111,
+	INPUT_GAMEPAD_L_SHOULDER = 112,
+	INPUT_GAMEPAD_R_SHOULDER = 113,
+	INPUT_GAMEPAD_L_TRIGGER = 114,
+	INPUT_GAMEPAD_R_TRIGGER = 115,
+	INPUT_GAMEPAD_SELECT = 116,
+	INPUT_GAMEPAD_START = 117,
+	INPUT_GAMEPAD_L_STICK_PRESS = 118,
+	INPUT_GAMEPAD_R_STICK_PRESS = 119,
+	INPUT_GAMEPAD_DPAD_UP = 120,
+	INPUT_GAMEPAD_DPAD_DOWN = 121,
+	INPUT_GAMEPAD_DPAD_LEFT = 122,
+	INPUT_GAMEPAD_DPAD_RIGHT = 123,
+	INPUT_GAMEPAD_HOME = 124,
+	INPUT_GAMEPAD_L_STICK_UP = 125,
+	INPUT_GAMEPAD_L_STICK_DOWN = 126,
+	INPUT_GAMEPAD_L_STICK_LEFT = 127,
+	INPUT_GAMEPAD_L_STICK_RIGHT = 128,
+	INPUT_GAMEPAD_R_STICK_UP = 129,
+	INPUT_GAMEPAD_R_STICK_DOWN = 130,
+	INPUT_GAMEPAD_R_STICK_LEFT = 131,
+	INPUT_GAMEPAD_R_STICK_RIGHT = 132,
+
+	INPUT_MOUSE_LEFT = 134,
+	INPUT_MOUSE_MIDDLE = 135,
+	INPUT_MOUSE_RIGHT = 136,
+	INPUT_MOUSE_WHEEL_UP = 137,
+	INPUT_MOUSE_WHEEL_DOWN = 138,
+
+	INPUT_BUTTON_MAX = 139
+} button_t;
+
+typedef enum {
+	INPUT_LAYER_SYSTEM = 0,
+	INPUT_LAYER_USER = 1,
+	INPUT_LAYER_MAX = 2
+} input_layer_t;
+
+typedef void(*input_capture_callback_t)
+	(void *user, button_t button, int32_t ascii_char);
+
+void input_init();
+void input_cleanup();
+void input_clear();
+
+void input_bind(input_layer_t layer, button_t button, uint8_t action);
+void input_unbind(input_layer_t layer,button_t button);
+void input_unbind_all(input_layer_t layer);
+
+uint8_t input_bound_to_action(button_t button);
+
+void input_set_button_state(button_t button, float state);
+void input_set_mouse_pos(int32_t x, int32_t y);
+void input_textinput(int32_t ascii_char);
+
+void input_capture(input_capture_callback_t cb, void *user);
+
+float input_state(uint8_t action);
+bool input_pressed(uint8_t action);
+bool input_released(uint8_t action);
+vec2_t input_mouse_pos();
+
+button_t input_name_to_button(const char *name);
+const char *input_button_to_name(button_t button);
+
+
+#define INPUT_ACTION_COMMAND 31
+#define INPUT_ACTION_MAX 32
+#define INPUT_DEADZONE 0.1
+#define INPUT_ACTION_NONE 255
+#define INPUT_BUTTON_NONE 0
+
+#endif
--- /dev/null
+++ b/src/libs/pl_mpeg.h
@@ -1,0 +1,4274 @@
+/*
+PL_MPEG - MPEG1 Video decoder, MP2 Audio decoder, MPEG-PS demuxer
+
+Dominic Szablewski - https://phoboslab.org
+
+
+-- LICENSE: The MIT License(MIT)
+
+Copyright(c) 2019 Dominic Szablewski
+
+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.
+
+
+
+
+-- Synopsis
+
+// Define `PL_MPEG_IMPLEMENTATION` in *one* C/C++ file before including this
+// library to create the implementation.
+
+#define PL_MPEG_IMPLEMENTATION
+#include "plmpeg.h"
+
+// This function gets called for each decoded video frame
+void my_video_callback(plm_t *plm, plm_frame_t *frame, void *user) {
+	// Do something with frame->y.data, frame->cr.data, frame->cb.data
+}
+
+// This function gets called for each decoded audio frame
+void my_audio_callback(plm_t *plm, plm_samples_t *frame, void *user) {
+	// Do something with samples->interleaved
+}
+
+// Load a .mpg (MPEG Program Stream) file
+plm_t *plm = plm_create_with_filename("some-file.mpg");
+
+// Install the video & audio decode callbacks
+plm_set_video_decode_callback(plm, my_video_callback, my_data);
+plm_set_audio_decode_callback(plm, my_audio_callback, my_data);
+
+
+// Decode
+do {
+	plm_decode(plm, time_since_last_call);
+} while (!plm_has_ended(plm));
+
+// All done
+plm_destroy(plm);
+
+
+
+-- Documentation
+
+This library provides several interfaces to load, demux and decode MPEG video
+and audio data. A high-level API combines the demuxer, video & audio decoders
+in an easy to use wrapper.
+
+Lower-level APIs for accessing the demuxer, video decoder and audio decoder, 
+as well as providing different data sources are also available.
+
+Interfaces are written in an object oriented style, meaning you create object 
+instances via various different constructor functions (plm_*create()),
+do some work on them and later dispose them via plm_*destroy().
+
+plm_* ......... the high-level interface, combining demuxer and decoders
+plm_buffer_* .. the data source used by all interfaces
+plm_demux_* ... the MPEG-PS demuxer
+plm_video_* ... the MPEG1 Video ("mpeg1") decoder
+plm_audio_* ... the MPEG1 Audio Layer II ("mp2") decoder
+
+
+With the high-level interface you have two options to decode video & audio:
+
+ 1. Use plm_decode() and just hand over the delta time since the last call.
+    It will decode everything needed and call your callbacks (specified through
+    plm_set_{video|audio}_decode_callback()) any number of times.
+
+ 2. Use plm_decode_video() and plm_decode_audio() to decode exactly one
+    frame of video or audio data at a time. How you handle the synchronization 
+    of both streams is up to you.
+
+If you only want to decode video *or* audio through these functions, you should
+disable the other stream (plm_set_{video|audio}_enabled(FALSE))
+
+Video data is decoded into a struct with all 3 planes (Y, Cr, Cb) stored in
+separate buffers. You can either convert this to RGB on the CPU (slow) via the
+plm_frame_to_rgb() function or do it on the GPU with the following matrix:
+
+mat4 bt601 = mat4(
+	1.16438,  0.00000,  1.59603, -0.87079,
+	1.16438, -0.39176, -0.81297,  0.52959,
+	1.16438,  2.01723,  0.00000, -1.08139,
+	0, 0, 0, 1
+);
+gl_FragColor = vec4(y, cb, cr, 1.0) * bt601;
+
+Audio data is decoded into a struct with either one single float array with the
+samples for the left and right channel interleaved, or if the 
+PLM_AUDIO_SEPARATE_CHANNELS is defined *before* including this library, into
+two separate float arrays - one for each channel.
+
+
+Data can be supplied to the high level interface, the demuxer and the decoders
+in three different ways:
+
+ 1. Using plm_create_from_filename() or with a file handle with 
+    plm_create_from_file().
+
+ 2. Using plm_create_with_memory() and supplying a pointer to memory that
+    contains the whole file.
+
+ 3. Using plm_create_with_buffer(), supplying your own plm_buffer_t instance and
+    periodically writing to this buffer.
+
+When using your own plm_buffer_t instance, you can fill this buffer using 
+plm_buffer_write(). You can either monitor plm_buffer_get_remaining() and push 
+data when appropriate, or install a callback on the buffer with 
+plm_buffer_set_load_callback() that gets called whenever the buffer needs more 
+data.
+
+A buffer created with plm_buffer_create_with_capacity() is treated as a ring
+buffer, meaning that data that has already been read, will be discarded. In
+contrast, a buffer created with plm_buffer_create_for_appending() will keep all
+data written to it in memory. This enables seeking in the already loaded data.
+
+
+There should be no need to use the lower level plm_demux_*, plm_video_* and 
+plm_audio_* functions, if all you want to do is read/decode an MPEG-PS file.
+However, if you get raw mpeg1video data or raw mp2 audio data from a different
+source, these functions can be used to decode the raw data directly. Similarly, 
+if you only want to analyze an MPEG-PS file or extract raw video or audio
+packets from it, you can use the plm_demux_* functions.
+
+
+This library uses malloc(), realloc() and free() to manage memory. Typically 
+all allocation happens up-front when creating the interface. However, the
+default buffer size may be too small for certain inputs. In these cases plmpeg
+will realloc() the buffer with a larger size whenever needed. You can configure
+the default buffer size by defining PLM_BUFFER_DEFAULT_SIZE *before* 
+including this library.
+
+You can also define PLM_MALLOC, PLM_REALLOC and PLM_FREE to provide your own
+memory management functions.
+
+
+See below for detailed the API documentation.
+
+*/
+
+
+#ifndef PL_MPEG_H
+#define PL_MPEG_H
+
+#include <stdint.h>
+#include <stdio.h>
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// -----------------------------------------------------------------------------
+// Public Data Types
+
+
+// Object types for the various interfaces
+
+typedef struct plm_t plm_t;
+typedef struct plm_buffer_t plm_buffer_t;
+typedef struct plm_demux_t plm_demux_t;
+typedef struct plm_video_t plm_video_t;
+typedef struct plm_audio_t plm_audio_t;
+
+
+// Demuxed MPEG PS packet
+// The type maps directly to the various MPEG-PES start codes. PTS is the
+// presentation time stamp of the packet in seconds. Note that not all packets
+// have a PTS value, indicated by PLM_PACKET_INVALID_TS.
+
+#define PLM_PACKET_INVALID_TS -1
+
+typedef struct {
+	int type;
+	double pts;
+	size_t length;
+	uint8_t *data;
+} plm_packet_t;
+
+
+// Decoded Video Plane 
+// The byte length of the data is width * height. Note that different planes
+// have different sizes: the Luma plane (Y) is double the size of each of 
+// the two Chroma planes (Cr, Cb) - i.e. 4 times the byte length.
+// Also note that the size of the plane does *not* denote the size of the 
+// displayed frame. The sizes of planes are always rounded up to the nearest
+// macroblock (16px).
+
+typedef struct {
+	unsigned int width;
+	unsigned int height;
+	uint8_t *data;
+} plm_plane_t;
+
+
+// Decoded Video Frame
+// width and height denote the desired display size of the frame. This may be
+// different from the internal size of the 3 planes.
+
+typedef struct {
+	double time;
+	unsigned int width;
+	unsigned int height;
+	plm_plane_t y;
+	plm_plane_t cr;
+	plm_plane_t cb;
+} plm_frame_t;
+
+
+// Callback function type for decoded video frames used by the high-level
+// plm_* interface
+
+typedef void(*plm_video_decode_callback)
+	(plm_t *self, plm_frame_t *frame, void *user);
+
+
+// Decoded Audio Samples
+// Samples are stored as normalized (-1, 1) float either interleaved, or if
+// PLM_AUDIO_SEPARATE_CHANNELS is defined, in two separate arrays.
+// The `count` is always PLM_AUDIO_SAMPLES_PER_FRAME and just there for
+// convenience.
+
+#define PLM_AUDIO_SAMPLES_PER_FRAME 1152
+
+typedef struct {
+	double time;
+	unsigned int count;
+	#ifdef PLM_AUDIO_SEPARATE_CHANNELS
+		float left[PLM_AUDIO_SAMPLES_PER_FRAME];
+		float right[PLM_AUDIO_SAMPLES_PER_FRAME];
+	#else
+		float interleaved[PLM_AUDIO_SAMPLES_PER_FRAME * 2];
+	#endif
+} plm_samples_t;
+
+
+// Callback function type for decoded audio samples used by the high-level
+// plm_* interface
+
+typedef void(*plm_audio_decode_callback)
+	(plm_t *self, plm_samples_t *samples, void *user);
+
+
+// Callback function for plm_buffer when it needs more data
+
+typedef void(*plm_buffer_load_callback)(plm_buffer_t *self, void *user);
+
+
+
+// -----------------------------------------------------------------------------
+// plm_* public API
+// High-Level API for loading/demuxing/decoding MPEG-PS data
+
+
+// Create a plmpeg instance with a filename. Returns NULL if the file could not
+// be opened.
+
+plm_t *plm_create_with_filename(const char *filename);
+
+
+// Create a plmpeg instance with a file handle. Pass TRUE to close_when_done to
+// let plmpeg call fclose() on the handle when plm_destroy() is called.
+
+plm_t *plm_create_with_file(FILE *fh, int close_when_done);
+
+
+// Create a plmpeg instance with a pointer to memory as source. This assumes the
+// whole file is in memory. The memory is not copied. Pass TRUE to 
+// free_when_done to let plmpeg call free() on the pointer when plm_destroy() 
+// is called.
+
+plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done);
+
+
+// Create a plmpeg instance with a plm_buffer as source. Pass TRUE to
+// destroy_when_done to let plmpeg call plm_buffer_destroy() on the buffer when
+// plm_destroy() is called.
+
+plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done);
+
+
+// Destroy a plmpeg instance and free all data.
+
+void plm_destroy(plm_t *self);
+
+
+// Get whether we have headers on all available streams and we can accurately
+// report the number of video/audio streams, video dimensions, framerate and
+// audio samplerate.
+// This returns FALSE if the file is not an MPEG-PS file or - when not using a
+// file as source - when not enough data is available yet.
+
+int plm_has_headers(plm_t *self);
+
+
+// Get or set whether video decoding is enabled. Default TRUE.
+
+int plm_get_video_enabled(plm_t *self);
+void plm_set_video_enabled(plm_t *self, int enabled);
+
+
+// Get the number of video streams (0--1) reported in the system header.
+
+int plm_get_num_video_streams(plm_t *self);
+
+
+// Get the display width/height of the video stream.
+
+int plm_get_width(plm_t *self);
+int plm_get_height(plm_t *self);
+
+
+// Get the framerate of the video stream in frames per second.
+
+double plm_get_framerate(plm_t *self);
+
+
+// Get or set whether audio decoding is enabled. Default TRUE.
+
+int plm_get_audio_enabled(plm_t *self);
+void plm_set_audio_enabled(plm_t *self, int enabled);
+
+
+// Get the number of audio streams (0--4) reported in the system header.
+
+int plm_get_num_audio_streams(plm_t *self);
+
+
+// Set the desired audio stream (0--3). Default 0.
+
+void plm_set_audio_stream(plm_t *self, int stream_index);
+
+
+// Get the samplerate of the audio stream in samples per second.
+
+int plm_get_samplerate(plm_t *self);
+
+
+// Get or set the audio lead time in seconds - the time in which audio samples
+// are decoded in advance (or behind) the video decode time. Typically this
+// should be set to the duration of the buffer of the audio API that you use
+// for output. E.g. for SDL2: (SDL_AudioSpec.samples / samplerate)
+
+double plm_get_audio_lead_time(plm_t *self);
+void plm_set_audio_lead_time(plm_t *self, double lead_time);
+
+
+// Get the current internal time in seconds.
+
+double plm_get_time(plm_t *self);
+
+
+// Get the video duration of the underlying source in seconds.
+
+double plm_get_duration(plm_t *self);
+
+
+// Rewind all buffers back to the beginning.
+
+void plm_rewind(plm_t *self);
+
+
+// Get or set looping. Default FALSE.
+
+int plm_get_loop(plm_t *self);
+void plm_set_loop(plm_t *self, int loop);
+
+
+// Get whether the file has ended. If looping is enabled, this will always
+// return FALSE.
+
+int plm_has_ended(plm_t *self);
+
+
+// Set the callback for decoded video frames used with plm_decode(). If no 
+// callback is set, video data will be ignored and not be decoded. The *user
+// Parameter will be passed to your callback.
+
+void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user);
+
+
+// Set the callback for decoded audio samples used with plm_decode(). If no 
+// callback is set, audio data will be ignored and not be decoded. The *user
+// Parameter will be passed to your callback.
+
+void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user);
+
+
+// Advance the internal timer by seconds and decode video/audio up to this time.
+// This will call the video_decode_callback and audio_decode_callback any number
+// of times. A frame-skip is not implemented, i.e. everything up to current time
+// will be decoded.
+
+void plm_decode(plm_t *self, double seconds);
+
+
+// Decode and return one video frame. Returns NULL if no frame could be decoded
+// (either because the source ended or data is corrupt). If you only want to 
+// decode video, you should disable audio via plm_set_audio_enabled().
+// The returned plm_frame_t is valid until the next call to plm_decode_video() 
+// or until plm_destroy() is called.
+
+plm_frame_t *plm_decode_video(plm_t *self);
+
+
+// Decode and return one audio frame. Returns NULL if no frame could be decoded
+// (either because the source ended or data is corrupt). If you only want to 
+// decode audio, you should disable video via plm_set_video_enabled().
+// The returned plm_samples_t is valid until the next call to plm_decode_audio()
+// or until plm_destroy() is called.
+
+plm_samples_t *plm_decode_audio(plm_t *self);
+
+
+// Seek to the specified time, clamped between 0 -- duration. This can only be 
+// used when the underlying plm_buffer is seekable, i.e. for files, fixed 
+// memory buffers or _for_appending buffers. 
+// If seek_exact is TRUE this will seek to the exact time, otherwise it will 
+// seek to the last intra frame just before the desired time. Exact seeking can 
+// be slow, because all frames up to the seeked one have to be decoded on top of
+// the previous intra frame.
+// If seeking succeeds, this function will call the video_decode_callback 
+// exactly once with the target frame. If audio is enabled, it will also call
+// the audio_decode_callback any number of times, until the audio_lead_time is
+// satisfied.
+// Returns TRUE if seeking succeeded or FALSE if no frame could be found.
+
+int plm_seek(plm_t *self, double time, int seek_exact);
+
+
+// Similar to plm_seek(), but will not call the video_decode_callback,
+// audio_decode_callback or make any attempts to sync audio.
+// Returns the found frame or NULL if no frame could be found.
+
+plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact);
+
+
+
+// -----------------------------------------------------------------------------
+// plm_buffer public API
+// Provides the data source for all other plm_* interfaces
+
+
+// The default size for buffers created from files or by the high-level API
+
+#ifndef PLM_BUFFER_DEFAULT_SIZE
+#define PLM_BUFFER_DEFAULT_SIZE (128 * 1024)
+#endif
+
+
+// Create a buffer instance with a filename. Returns NULL if the file could not
+// be opened.
+
+plm_buffer_t *plm_buffer_create_with_filename(const char *filename);
+
+
+// Create a buffer instance with a file handle. Pass TRUE to close_when_done
+// to let plmpeg call fclose() on the handle when plm_destroy() is called.
+
+plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done);
+
+
+// Create a buffer instance with a pointer to memory as source. This assumes
+// the whole file is in memory. The bytes are not copied. Pass 1 to 
+// free_when_done to let plmpeg call free() on the pointer when plm_destroy() 
+// is called.
+
+plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done);
+
+
+// Create an empty buffer with an initial capacity. The buffer will grow
+// as needed. Data that has already been read, will be discarded.
+
+plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity);
+
+
+// Create an empty buffer with an initial capacity. The buffer will grow
+// as needed. Decoded data will *not* be discarded. This can be used when
+// loading a file over the network, without needing to throttle the download. 
+// It also allows for seeking in the already loaded data.
+
+plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity);
+
+
+// Destroy a buffer instance and free all data
+
+void plm_buffer_destroy(plm_buffer_t *self);
+
+
+// Copy data into the buffer. If the data to be written is larger than the 
+// available space, the buffer will realloc() with a larger capacity. 
+// Returns the number of bytes written. This will always be the same as the
+// passed in length, except when the buffer was created _with_memory() for
+// which _write() is forbidden.
+
+size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length);
+
+
+// Mark the current byte length as the end of this buffer and signal that no 
+// more data is expected to be written to it. This function should be called
+// just after the last plm_buffer_write().
+// For _with_capacity buffers, this is cleared on a plm_buffer_rewind().
+
+void plm_buffer_signal_end(plm_buffer_t *self);
+
+
+// Set a callback that is called whenever the buffer needs more data
+
+void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user);
+
+
+// Rewind the buffer back to the beginning. When loading from a file handle,
+// this also seeks to the beginning of the file.
+
+void plm_buffer_rewind(plm_buffer_t *self);
+
+
+// Get the total size. For files, this returns the file size. For all other 
+// types it returns the number of bytes currently in the buffer.
+
+size_t plm_buffer_get_size(plm_buffer_t *self);
+
+
+// Get the number of remaining (yet unread) bytes in the buffer. This can be
+// useful to throttle writing.
+
+size_t plm_buffer_get_remaining(plm_buffer_t *self);
+
+
+// Get whether the read position of the buffer is at the end and no more data 
+// is expected.
+
+int plm_buffer_has_ended(plm_buffer_t *self);
+
+
+
+// -----------------------------------------------------------------------------
+// plm_demux public API
+// Demux an MPEG Program Stream (PS) data into separate packages
+
+
+// Various Packet Types
+
+static const int PLM_DEMUX_PACKET_PRIVATE = 0xBD;
+static const int PLM_DEMUX_PACKET_AUDIO_1 = 0xC0;
+static const int PLM_DEMUX_PACKET_AUDIO_2 = 0xC1;
+static const int PLM_DEMUX_PACKET_AUDIO_3 = 0xC2;
+static const int PLM_DEMUX_PACKET_AUDIO_4 = 0xC2;
+static const int PLM_DEMUX_PACKET_VIDEO_1 = 0xE0;
+
+
+// Create a demuxer with a plm_buffer as source. This will also attempt to read
+// the pack and system headers from the buffer.
+
+plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done);
+
+
+// Destroy a demuxer and free all data.
+
+void plm_demux_destroy(plm_demux_t *self);
+
+
+// Returns TRUE/FALSE whether pack and system headers have been found. This will
+// attempt to read the headers if non are present yet.
+
+int plm_demux_has_headers(plm_demux_t *self);
+
+
+// Returns the number of video streams found in the system header. This will
+// attempt to read the system header if non is present yet.
+
+int plm_demux_get_num_video_streams(plm_demux_t *self);
+
+
+// Returns the number of audio streams found in the system header. This will
+// attempt to read the system header if non is present yet.
+
+int plm_demux_get_num_audio_streams(plm_demux_t *self);
+
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_demux_rewind(plm_demux_t *self);
+
+
+// Get whether the file has ended. This will be cleared on seeking or rewind.
+
+int plm_demux_has_ended(plm_demux_t *self);
+
+
+// Seek to a packet of the specified type with a PTS just before specified time.
+// If force_intra is TRUE, only packets containing an intra frame will be 
+// considered - this only makes sense when the type is PLM_DEMUX_PACKET_VIDEO_1.
+// Note that the specified time is considered 0-based, regardless of the first 
+// PTS in the data source.
+
+plm_packet_t *plm_demux_seek(plm_demux_t *self, double time, int type, int force_intra);
+
+
+// Get the PTS of the first packet of this type. Returns PLM_PACKET_INVALID_TS
+// if not packet of this packet type can be found.
+
+double plm_demux_get_start_time(plm_demux_t *self, int type);
+
+
+// Get the duration for the specified packet type - i.e. the span between the
+// the first PTS and the last PTS in the data source. This only makes sense when
+// the underlying data source is a file or fixed memory.
+
+double plm_demux_get_duration(plm_demux_t *self, int type);
+
+
+// Decode and return the next packet. The returned packet_t is valid until
+// the next call to plm_demux_decode() or until the demuxer is destroyed.
+
+plm_packet_t *plm_demux_decode(plm_demux_t *self);
+
+
+
+// -----------------------------------------------------------------------------
+// plm_video public API
+// Decode MPEG1 Video ("mpeg1") data into raw YCrCb frames
+
+
+// Create a video decoder with a plm_buffer as source.
+
+plm_video_t *plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done);
+
+
+// Destroy a video decoder and free all data.
+
+void plm_video_destroy(plm_video_t *self);
+
+
+// Get whether a sequence header was found and we can accurately report on
+// dimensions and framerate.
+
+int plm_video_has_header(plm_video_t *self);
+
+
+// Get the framerate in frames per second.
+
+double plm_video_get_framerate(plm_video_t *self);
+
+
+// Get the display width/height.
+
+int plm_video_get_width(plm_video_t *self);
+int plm_video_get_height(plm_video_t *self);
+
+
+// Set "no delay" mode. When enabled, the decoder assumes that the video does
+// *not* contain any B-Frames. This is useful for reducing lag when streaming.
+// The default is FALSE.
+
+void plm_video_set_no_delay(plm_video_t *self, int no_delay);
+
+
+// Get the current internal time in seconds.
+
+double plm_video_get_time(plm_video_t *self);
+
+
+// Set the current internal time in seconds. This is only useful when you
+// manipulate the underlying video buffer and want to enforce a correct
+// timestamps.
+
+void plm_video_set_time(plm_video_t *self, double time);
+
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_video_rewind(plm_video_t *self);
+
+
+// Get whether the file has ended. This will be cleared on rewind.
+
+int plm_video_has_ended(plm_video_t *self);
+
+
+// Decode and return one frame of video and advance the internal time by 
+// 1/framerate seconds. The returned frame_t is valid until the next call of
+// plm_video_decode() or until the video decoder is destroyed.
+
+plm_frame_t *plm_video_decode(plm_video_t *self);
+
+
+// Convert the YCrCb data of a frame into interleaved R G B data. The stride
+// specifies the width in bytes of the destination buffer. I.e. the number of
+// bytes from one line to the next. The stride must be at least 
+// (frame->width * bytes_per_pixel). The buffer pointed to by *dest must have a
+// size of at least (stride * frame->height).
+// Note that the alpha component of the dest buffer is always left untouched.
+
+void plm_frame_to_rgb(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_bgr(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_rgba(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_bgra(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_argb(plm_frame_t *frame, uint8_t *dest, int stride);
+void plm_frame_to_abgr(plm_frame_t *frame, uint8_t *dest, int stride);
+
+
+// -----------------------------------------------------------------------------
+// plm_audio public API
+// Decode MPEG-1 Audio Layer II ("mp2") data into raw samples
+
+
+// Create an audio decoder with a plm_buffer as source.
+
+plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done);
+
+
+// Destroy an audio decoder and free all data.
+
+void plm_audio_destroy(plm_audio_t *self);
+
+
+// Get whether a frame header was found and we can accurately report on
+// samplerate.
+
+int plm_audio_has_header(plm_audio_t *self);
+
+
+// Get the samplerate in samples per second.
+
+int plm_audio_get_samplerate(plm_audio_t *self);
+
+
+// Get the current internal time in seconds.
+
+double plm_audio_get_time(plm_audio_t *self);
+
+
+// Set the current internal time in seconds. This is only useful when you
+// manipulate the underlying video buffer and want to enforce a correct
+// timestamps.
+
+void plm_audio_set_time(plm_audio_t *self, double time);
+
+
+// Rewind the internal buffer. See plm_buffer_rewind().
+
+void plm_audio_rewind(plm_audio_t *self);
+
+
+// Get whether the file has ended. This will be cleared on rewind.
+
+int plm_audio_has_ended(plm_audio_t *self);
+
+
+// Decode and return one "frame" of audio and advance the internal time by 
+// (PLM_AUDIO_SAMPLES_PER_FRAME/samplerate) seconds. The returned samples_t 
+// is valid until the next call of plm_audio_decode() or until the audio
+// decoder is destroyed.
+
+plm_samples_t *plm_audio_decode(plm_audio_t *self);
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // PL_MPEG_H
+
+
+
+
+
+// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
+// IMPLEMENTATION
+
+#ifdef PL_MPEG_IMPLEMENTATION
+
+#include <string.h>
+#include <stdlib.h>
+
+#ifndef TRUE
+#define TRUE 1
+#define FALSE 0
+#endif
+
+#ifndef PLM_MALLOC
+	#define PLM_MALLOC(sz) malloc(sz)
+	#define PLM_FREE(p) free(p)
+	#define PLM_REALLOC(p, sz) realloc(p, sz)
+#endif
+
+#define PLM_UNUSED(expr) (void)(expr)
+
+
+// -----------------------------------------------------------------------------
+// plm (high-level interface) implementation
+
+struct plm_t {
+	plm_demux_t *demux;
+	double time;
+	int has_ended;
+	int loop;
+	int has_decoders;
+
+	int video_enabled;
+	int video_packet_type;
+	plm_buffer_t *video_buffer;
+	plm_video_t *video_decoder;
+
+	int audio_enabled;
+	int audio_stream_index;
+	int audio_packet_type;
+	double audio_lead_time;
+	plm_buffer_t *audio_buffer;
+	plm_audio_t *audio_decoder;
+
+	plm_video_decode_callback video_decode_callback;
+	void *video_decode_callback_user_data;
+
+	plm_audio_decode_callback audio_decode_callback;
+	void *audio_decode_callback_user_data;
+};
+
+int plm_init_decoders(plm_t *self);
+void plm_handle_end(plm_t *self);
+void plm_read_video_packet(plm_buffer_t *buffer, void *user);
+void plm_read_audio_packet(plm_buffer_t *buffer, void *user);
+void plm_read_packets(plm_t *self, int requested_type);
+
+plm_t *plm_create_with_filename(const char *filename) {
+	plm_buffer_t *buffer = plm_buffer_create_with_filename(filename);
+	if (!buffer) {
+		return NULL;
+	}
+	return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_file(FILE *fh, int close_when_done) {
+	plm_buffer_t *buffer = plm_buffer_create_with_file(fh, close_when_done);
+	return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) {
+	plm_buffer_t *buffer = plm_buffer_create_with_memory(bytes, length, free_when_done);
+	return plm_create_with_buffer(buffer, TRUE);
+}
+
+plm_t *plm_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) {
+	plm_t *self = (plm_t *)PLM_MALLOC(sizeof(plm_t));
+	memset(self, 0, sizeof(plm_t));
+
+	self->demux = plm_demux_create(buffer, destroy_when_done);
+	self->video_enabled = TRUE;
+	self->audio_enabled = TRUE;
+	plm_init_decoders(self);
+
+	return self;
+}
+
+int plm_init_decoders(plm_t *self) {
+	if (self->has_decoders) {
+		return TRUE;
+	}
+
+	if (!plm_demux_has_headers(self->demux)) {
+		return FALSE;
+	}
+
+	if (plm_demux_get_num_video_streams(self->demux) > 0) {
+		if (self->video_enabled) {
+			self->video_packet_type = PLM_DEMUX_PACKET_VIDEO_1;
+		}
+		self->video_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+		plm_buffer_set_load_callback(self->video_buffer, plm_read_video_packet, self);
+	}
+
+	if (plm_demux_get_num_audio_streams(self->demux) > 0) {
+		if (self->audio_enabled) {
+			self->audio_packet_type = PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index;
+		}
+		self->audio_buffer = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+		plm_buffer_set_load_callback(self->audio_buffer, plm_read_audio_packet, self);
+	}
+
+	if (self->video_buffer) {
+		self->video_decoder = plm_video_create_with_buffer(self->video_buffer, TRUE);
+	}
+
+	if (self->audio_buffer) {
+		self->audio_decoder = plm_audio_create_with_buffer(self->audio_buffer, TRUE);
+	}
+
+	self->has_decoders = TRUE;
+	return TRUE;
+}
+
+void plm_destroy(plm_t *self) {
+	if (self->video_decoder) {
+		plm_video_destroy(self->video_decoder);
+	}
+	if (self->audio_decoder) {
+		plm_audio_destroy(self->audio_decoder);
+	}
+
+	plm_demux_destroy(self->demux);
+	PLM_FREE(self);
+}
+
+int plm_get_audio_enabled(plm_t *self) {
+	return self->audio_enabled;
+}
+
+int plm_has_headers(plm_t *self) {
+	if (!plm_demux_has_headers(self->demux)) {
+		return FALSE;
+	}
+	
+	if (!plm_init_decoders(self)) {
+		return FALSE;
+	}
+
+	if (
+		(self->video_decoder && !plm_video_has_header(self->video_decoder)) ||
+		(self->audio_decoder && !plm_audio_has_header(self->audio_decoder))
+	) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+void plm_set_audio_enabled(plm_t *self, int enabled) {
+	self->audio_enabled = enabled;
+
+	if (!enabled) {
+		self->audio_packet_type = 0;
+		return;
+	}
+
+	self->audio_packet_type = (plm_init_decoders(self) && self->audio_decoder)
+		? PLM_DEMUX_PACKET_AUDIO_1 + self->audio_stream_index
+		: 0;
+}
+
+void plm_set_audio_stream(plm_t *self, int stream_index) {
+	if (stream_index < 0 || stream_index > 3) {
+		return;
+	}
+	self->audio_stream_index = stream_index;
+
+	// Set the correct audio_packet_type
+	plm_set_audio_enabled(self, self->audio_enabled);
+}
+
+int plm_get_video_enabled(plm_t *self) {
+	return self->video_enabled;
+}
+
+void plm_set_video_enabled(plm_t *self, int enabled) {
+	self->video_enabled = enabled;
+
+	if (!enabled) {
+		self->video_packet_type = 0;
+		return;
+	}
+
+	self->video_packet_type = (plm_init_decoders(self) && self->video_decoder)
+		? PLM_DEMUX_PACKET_VIDEO_1
+		: 0;
+}
+
+int plm_get_num_video_streams(plm_t *self) {
+	return plm_demux_get_num_video_streams(self->demux);
+}
+
+int plm_get_width(plm_t *self) {
+	return (plm_init_decoders(self) && self->video_decoder)
+		? plm_video_get_width(self->video_decoder)
+		: 0;
+}
+
+int plm_get_height(plm_t *self) {
+	return (plm_init_decoders(self) && self->video_decoder)
+		? plm_video_get_height(self->video_decoder)
+		: 0;
+}
+
+double plm_get_framerate(plm_t *self) {
+	return (plm_init_decoders(self) && self->video_decoder)
+		? plm_video_get_framerate(self->video_decoder)
+		: 0;
+}
+
+int plm_get_num_audio_streams(plm_t *self) {
+	return plm_demux_get_num_audio_streams(self->demux);
+}
+
+int plm_get_samplerate(plm_t *self) {
+	return (plm_init_decoders(self) && self->audio_decoder)
+		? plm_audio_get_samplerate(self->audio_decoder)
+		: 0;
+}
+
+double plm_get_audio_lead_time(plm_t *self) {
+	return self->audio_lead_time;
+}
+
+void plm_set_audio_lead_time(plm_t *self, double lead_time) {
+	self->audio_lead_time = lead_time;
+}
+
+double plm_get_time(plm_t *self) {
+	return self->time;
+}
+
+double plm_get_duration(plm_t *self) {
+	return plm_demux_get_duration(self->demux, PLM_DEMUX_PACKET_VIDEO_1);
+}
+
+void plm_rewind(plm_t *self) {
+	if (self->video_decoder) {
+		plm_video_rewind(self->video_decoder);
+	}
+
+	if (self->audio_decoder) {
+		plm_audio_rewind(self->audio_decoder);
+	}
+
+	plm_demux_rewind(self->demux);
+	self->time = 0;
+}
+
+int plm_get_loop(plm_t *self) {
+	return self->loop;
+}
+
+void plm_set_loop(plm_t *self, int loop) {
+	self->loop = loop;
+}
+
+int plm_has_ended(plm_t *self) {
+	return self->has_ended;
+}
+
+void plm_set_video_decode_callback(plm_t *self, plm_video_decode_callback fp, void *user) {
+	self->video_decode_callback = fp;
+	self->video_decode_callback_user_data = user;
+}
+
+void plm_set_audio_decode_callback(plm_t *self, plm_audio_decode_callback fp, void *user) {
+	self->audio_decode_callback = fp;
+	self->audio_decode_callback_user_data = user;
+}
+
+void plm_decode(plm_t *self, double tick) {
+	if (!plm_init_decoders(self)) {
+		return;
+	}
+
+	int decode_video = (self->video_decode_callback && self->video_packet_type);
+	int decode_audio = (self->audio_decode_callback && self->audio_packet_type);
+
+	if (!decode_video && !decode_audio) {
+		// Nothing to do here
+		return;
+	}
+
+	int did_decode = FALSE;
+	int decode_video_failed = FALSE;
+	int decode_audio_failed = FALSE;
+
+	double video_target_time = self->time + tick;
+	double audio_target_time = self->time + tick + self->audio_lead_time;
+
+	do {
+		did_decode = FALSE;
+		
+		if (decode_video && plm_video_get_time(self->video_decoder) < video_target_time) {
+			plm_frame_t *frame = plm_video_decode(self->video_decoder);
+			if (frame) {
+				self->video_decode_callback(self, frame, self->video_decode_callback_user_data);
+				did_decode = TRUE;
+			}
+			else {
+				decode_video_failed = TRUE;
+			}
+		}
+
+		if (decode_audio && plm_audio_get_time(self->audio_decoder) < audio_target_time) {
+			plm_samples_t *samples = plm_audio_decode(self->audio_decoder);
+			if (samples) {
+				self->audio_decode_callback(self, samples, self->audio_decode_callback_user_data);
+				did_decode = TRUE;
+			}
+			else {
+				decode_audio_failed = TRUE;
+			}
+		}
+	} while (did_decode);
+	
+	// Did all sources we wanted to decode fail and the demuxer is at the end?
+	if (
+		(!decode_video || decode_video_failed) && 
+		(!decode_audio || decode_audio_failed) &&
+		plm_demux_has_ended(self->demux)
+	) {
+		plm_handle_end(self);
+		return;
+	}
+
+	self->time += tick;
+}
+
+plm_frame_t *plm_decode_video(plm_t *self) {
+	if (!plm_init_decoders(self)) {
+		return NULL;
+	}
+
+	if (!self->video_packet_type) {
+		return NULL;
+	}
+
+	plm_frame_t *frame = plm_video_decode(self->video_decoder);
+	if (frame) {
+		self->time = frame->time;
+	}
+	else if (plm_demux_has_ended(self->demux)) {
+		plm_handle_end(self);
+	}
+	return frame;
+}
+
+plm_samples_t *plm_decode_audio(plm_t *self) {
+	if (!plm_init_decoders(self)) {
+		return NULL;
+	}
+
+	if (!self->audio_packet_type) {
+		return NULL;
+	}
+
+	plm_samples_t *samples = plm_audio_decode(self->audio_decoder);
+	if (samples) {
+		self->time = samples->time;
+	}
+	else if (plm_demux_has_ended(self->demux)) {
+		plm_handle_end(self);
+	}
+	return samples;
+}
+
+void plm_handle_end(plm_t *self) {
+	if (self->loop) {
+		plm_rewind(self);
+	}
+	else {
+		self->has_ended = TRUE;
+	}
+}
+
+void plm_read_video_packet(plm_buffer_t *buffer, void *user) {
+	PLM_UNUSED(buffer);
+	plm_t *self = (plm_t *)user;
+	plm_read_packets(self, self->video_packet_type);
+}
+
+void plm_read_audio_packet(plm_buffer_t *buffer, void *user) {
+	PLM_UNUSED(buffer);
+	plm_t *self = (plm_t *)user;
+	plm_read_packets(self, self->audio_packet_type);
+}
+
+void plm_read_packets(plm_t *self, int requested_type) {
+	plm_packet_t *packet;
+	while ((packet = plm_demux_decode(self->demux))) {
+		if (packet->type == self->video_packet_type) {
+			plm_buffer_write(self->video_buffer, packet->data, packet->length);
+		}
+		else if (packet->type == self->audio_packet_type) {
+			plm_buffer_write(self->audio_buffer, packet->data, packet->length);
+		}
+
+		if (packet->type == requested_type) {
+			return;
+		}
+	}
+
+	if (plm_demux_has_ended(self->demux)) {
+		if (self->video_buffer) {
+			plm_buffer_signal_end(self->video_buffer);
+		}
+		if (self->audio_buffer) {
+			plm_buffer_signal_end(self->audio_buffer);
+		}
+	}
+}
+
+plm_frame_t *plm_seek_frame(plm_t *self, double time, int seek_exact) {
+	if (!plm_init_decoders(self)) {
+		return NULL;
+	}
+
+	if (!self->video_packet_type) {
+		return NULL;
+	}
+
+	int type = self->video_packet_type;
+
+	double start_time = plm_demux_get_start_time(self->demux, type);
+	double duration = plm_demux_get_duration(self->demux, type);
+
+	if (time < 0) {
+		time = 0;
+	}
+	else if (time > duration) {
+		time = duration;
+	}
+	
+	plm_packet_t *packet = plm_demux_seek(self->demux, time, type, TRUE);
+	if (!packet) {
+		return NULL;
+	}
+
+	// Disable writing to the audio buffer while decoding video
+	int previous_audio_packet_type = self->audio_packet_type;
+	self->audio_packet_type = 0;
+
+	// Clear video buffer and decode the found packet
+	plm_video_rewind(self->video_decoder);
+	plm_video_set_time(self->video_decoder, packet->pts - start_time);
+	plm_buffer_write(self->video_buffer, packet->data, packet->length);
+	plm_frame_t *frame = plm_video_decode(self->video_decoder);	
+
+	// If we want to seek to an exact frame, we have to decode all frames
+	// on top of the intra frame we just jumped to.
+	if (seek_exact) {
+		while (frame && frame->time < time) {
+			frame = plm_video_decode(self->video_decoder);
+		}
+	}
+
+	// Enable writing to the audio buffer again?
+	self->audio_packet_type = previous_audio_packet_type;
+
+	if (frame) {
+		self->time = frame->time;
+	}
+
+	self->has_ended = FALSE;
+	return frame;
+}
+
+int plm_seek(plm_t *self, double time, int seek_exact) {
+	plm_frame_t *frame = plm_seek_frame(self, time, seek_exact);
+	
+	if (!frame) {
+		return FALSE;
+	}
+
+	if (self->video_decode_callback) {
+		self->video_decode_callback(self, frame, self->video_decode_callback_user_data);	
+	}
+
+	// If audio is not enabled we are done here.
+	if (!self->audio_packet_type) {
+		return TRUE;
+	}
+
+	// Sync up Audio. This demuxes more packets until the first audio packet
+	// with a PTS greater than the current time is found. plm_decode() is then
+	// called to decode enough audio data to satisfy the audio_lead_time.
+
+	double start_time = plm_demux_get_start_time(self->demux, self->video_packet_type);
+	plm_audio_rewind(self->audio_decoder);
+
+	plm_packet_t *packet = NULL;
+	while ((packet = plm_demux_decode(self->demux))) {
+		if (packet->type == self->video_packet_type) {
+			plm_buffer_write(self->video_buffer, packet->data, packet->length);
+		}
+		else if (
+			packet->type == self->audio_packet_type &&
+			packet->pts - start_time > self->time
+		) {
+			plm_audio_set_time(self->audio_decoder, packet->pts - start_time);
+			plm_buffer_write(self->audio_buffer, packet->data, packet->length);
+			plm_decode(self, 0);
+			break;
+		}
+	}	
+	
+	return TRUE;
+}
+
+
+
+// -----------------------------------------------------------------------------
+// plm_buffer implementation
+
+enum plm_buffer_mode {
+	PLM_BUFFER_MODE_FILE,
+	PLM_BUFFER_MODE_FIXED_MEM,
+	PLM_BUFFER_MODE_RING,
+	PLM_BUFFER_MODE_APPEND
+};
+
+struct plm_buffer_t {
+	size_t bit_index;
+	size_t capacity;
+	size_t length;
+	size_t total_size;
+	int discard_read_bytes;
+	int has_ended;
+	int free_when_done;
+	int close_when_done;
+	FILE *fh;
+	plm_buffer_load_callback load_callback;
+	void *load_callback_user_data;
+	uint8_t *bytes;
+	enum plm_buffer_mode mode;
+};
+
+typedef struct {
+	int16_t index;
+	int16_t value;
+} plm_vlc_t;
+
+typedef struct {
+	int16_t index;
+	uint16_t value;
+} plm_vlc_uint_t;
+
+
+void plm_buffer_seek(plm_buffer_t *self, size_t pos);
+size_t plm_buffer_tell(plm_buffer_t *self);
+void plm_buffer_discard_read_bytes(plm_buffer_t *self);
+void plm_buffer_load_file_callback(plm_buffer_t *self, void *user);
+
+int plm_buffer_has(plm_buffer_t *self, size_t count);
+int plm_buffer_read(plm_buffer_t *self, int count);
+void plm_buffer_align(plm_buffer_t *self);
+void plm_buffer_skip(plm_buffer_t *self, size_t count);
+int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v);
+int plm_buffer_next_start_code(plm_buffer_t *self);
+int plm_buffer_find_start_code(plm_buffer_t *self, int code);
+int plm_buffer_no_start_code(plm_buffer_t *self);
+int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table);
+uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table);
+
+plm_buffer_t *plm_buffer_create_with_filename(const char *filename) {
+	FILE *fh = fopen(filename, "rb");
+	if (!fh) {
+		return NULL;
+	}
+	return plm_buffer_create_with_file(fh, TRUE);
+}
+
+plm_buffer_t *plm_buffer_create_with_file(FILE *fh, int close_when_done) {
+	plm_buffer_t *self = plm_buffer_create_with_capacity(PLM_BUFFER_DEFAULT_SIZE);
+	self->fh = fh;
+	self->close_when_done = close_when_done;
+	self->mode = PLM_BUFFER_MODE_FILE;
+	self->discard_read_bytes = TRUE;
+	
+	fseek(self->fh, 0, SEEK_END);
+	self->total_size = ftell(self->fh);
+	fseek(self->fh, 0, SEEK_SET);
+
+	plm_buffer_set_load_callback(self, plm_buffer_load_file_callback, NULL);
+	return self;
+}
+
+plm_buffer_t *plm_buffer_create_with_memory(uint8_t *bytes, size_t length, int free_when_done) {
+	plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t));
+	memset(self, 0, sizeof(plm_buffer_t));
+	self->capacity = length;
+	self->length = length;
+	self->total_size = length;
+	self->free_when_done = free_when_done;
+	self->bytes = bytes;
+	self->mode = PLM_BUFFER_MODE_FIXED_MEM;
+	self->discard_read_bytes = FALSE;
+	return self;
+}
+
+plm_buffer_t *plm_buffer_create_with_capacity(size_t capacity) {
+	plm_buffer_t *self = (plm_buffer_t *)PLM_MALLOC(sizeof(plm_buffer_t));
+	memset(self, 0, sizeof(plm_buffer_t));
+	self->capacity = capacity;
+	self->free_when_done = TRUE;
+	self->bytes = (uint8_t *)PLM_MALLOC(capacity);
+	self->mode = PLM_BUFFER_MODE_RING;
+	self->discard_read_bytes = TRUE;
+	return self;
+}
+
+plm_buffer_t *plm_buffer_create_for_appending(size_t initial_capacity) {
+	plm_buffer_t *self = plm_buffer_create_with_capacity(initial_capacity);
+	self->mode = PLM_BUFFER_MODE_APPEND;
+	self->discard_read_bytes = FALSE;
+	return self;
+}
+
+void plm_buffer_destroy(plm_buffer_t *self) {
+	if (self->fh && self->close_when_done) {
+		fclose(self->fh);
+	}
+	if (self->free_when_done) {
+		PLM_FREE(self->bytes);
+	}
+	PLM_FREE(self);
+}
+
+size_t plm_buffer_get_size(plm_buffer_t *self) {
+	return (self->mode == PLM_BUFFER_MODE_FILE)
+		? self->total_size
+		: self->length;
+}
+
+size_t plm_buffer_get_remaining(plm_buffer_t *self) {
+	return self->length - (self->bit_index >> 3);
+}
+
+size_t plm_buffer_write(plm_buffer_t *self, uint8_t *bytes, size_t length) {
+	if (self->mode == PLM_BUFFER_MODE_FIXED_MEM) {
+		return 0;
+	}
+
+	if (self->discard_read_bytes) {
+		// This should be a ring buffer, but instead it just shifts all unread 
+		// data to the beginning of the buffer and appends new data at the end. 
+		// Seems to be good enough.
+
+		plm_buffer_discard_read_bytes(self);
+		if (self->mode == PLM_BUFFER_MODE_RING) {
+			self->total_size = 0;
+		}
+	}
+
+	// Do we have to resize to fit the new data?
+	size_t bytes_available = self->capacity - self->length;
+	if (bytes_available < length) {
+		size_t new_size = self->capacity;
+		do {
+			new_size *= 2;
+		} while (new_size - self->length < length);
+		self->bytes = (uint8_t *)PLM_REALLOC(self->bytes, new_size);
+		self->capacity = new_size;
+	}
+
+	memcpy(self->bytes + self->length, bytes, length);
+	self->length += length;
+	self->has_ended = FALSE;
+	return length;
+}
+
+void plm_buffer_signal_end(plm_buffer_t *self) {
+	self->total_size = self->length;
+}
+
+void plm_buffer_set_load_callback(plm_buffer_t *self, plm_buffer_load_callback fp, void *user) {
+	self->load_callback = fp;
+	self->load_callback_user_data = user;
+}
+
+void plm_buffer_rewind(plm_buffer_t *self) {
+	plm_buffer_seek(self, 0);
+}
+
+void plm_buffer_seek(plm_buffer_t *self, size_t pos) {
+	self->has_ended = FALSE;
+
+	if (self->mode == PLM_BUFFER_MODE_FILE) {
+		fseek(self->fh, pos, SEEK_SET);
+		self->bit_index = 0;
+		self->length = 0;
+	}
+	else if (self->mode == PLM_BUFFER_MODE_RING) {
+		if (pos != 0) {
+			// Seeking to non-0 is forbidden for dynamic-mem buffers
+			return; 
+		}
+		self->bit_index = 0;
+		self->length = 0;
+		self->total_size = 0;
+	}
+	else if (pos < self->length) {
+		self->bit_index = pos << 3;
+	}
+}
+
+size_t plm_buffer_tell(plm_buffer_t *self) {
+	return self->mode == PLM_BUFFER_MODE_FILE
+		? ftell(self->fh) + (self->bit_index >> 3) - self->length
+		: self->bit_index >> 3;
+}
+
+void plm_buffer_discard_read_bytes(plm_buffer_t *self) {
+	size_t byte_pos = self->bit_index >> 3;
+	if (byte_pos == self->length) {
+		self->bit_index = 0;
+		self->length = 0;
+	}
+	else if (byte_pos > 0) {
+		memmove(self->bytes, self->bytes + byte_pos, self->length - byte_pos);
+		self->bit_index -= byte_pos << 3;
+		self->length -= byte_pos;
+	}
+}
+
+void plm_buffer_load_file_callback(plm_buffer_t *self, void *user) {
+	PLM_UNUSED(user);
+	
+	if (self->discard_read_bytes) {
+		plm_buffer_discard_read_bytes(self);
+	}
+
+	size_t bytes_available = self->capacity - self->length;
+	size_t bytes_read = fread(self->bytes + self->length, 1, bytes_available, self->fh);
+	self->length += bytes_read;
+
+	if (bytes_read == 0) {
+		self->has_ended = TRUE;
+	}
+}
+
+int plm_buffer_has_ended(plm_buffer_t *self) {
+	return self->has_ended;
+}
+
+int plm_buffer_has(plm_buffer_t *self, size_t count) {
+	if (((self->length << 3) - self->bit_index) >= count) {
+		return TRUE;
+	}
+
+	if (self->load_callback) {
+		self->load_callback(self, self->load_callback_user_data);
+		
+		if (((self->length << 3) - self->bit_index) >= count) {
+			return TRUE;
+		}
+	}	
+	
+	if (self->total_size != 0 && self->length == self->total_size) {
+		self->has_ended = TRUE;
+	}
+	return FALSE;
+}
+
+int plm_buffer_read(plm_buffer_t *self, int count) {
+	if (!plm_buffer_has(self, count)) {
+		return 0;
+	}
+
+	int value = 0;
+	while (count) {
+		int current_byte = self->bytes[self->bit_index >> 3];
+
+		int remaining = 8 - (self->bit_index & 7); // Remaining bits in byte
+		int read = remaining < count ? remaining : count; // Bits in self run
+		int shift = remaining - read;
+		int mask = (0xff >> (8 - read));
+
+		value = (value << read) | ((current_byte & (mask << shift)) >> shift);
+
+		self->bit_index += read;
+		count -= read;
+	}
+
+	return value;
+}
+
+void plm_buffer_align(plm_buffer_t *self) {
+	self->bit_index = ((self->bit_index + 7) >> 3) << 3; // Align to next byte
+}
+
+void plm_buffer_skip(plm_buffer_t *self, size_t count) {
+	if (plm_buffer_has(self, count)) {
+		self->bit_index += count;
+	}
+}
+
+int plm_buffer_skip_bytes(plm_buffer_t *self, uint8_t v) {
+	plm_buffer_align(self);
+	int skipped = 0;
+	while (plm_buffer_has(self, 8) && self->bytes[self->bit_index >> 3] == v) {
+		self->bit_index += 8;
+		skipped++;
+	}
+	return skipped;
+}
+
+int plm_buffer_next_start_code(plm_buffer_t *self) {
+	plm_buffer_align(self);
+
+	while (plm_buffer_has(self, (5 << 3))) {
+		size_t byte_index = (self->bit_index) >> 3;
+		if (
+			self->bytes[byte_index] == 0x00 &&
+			self->bytes[byte_index + 1] == 0x00 &&
+			self->bytes[byte_index + 2] == 0x01
+		) {
+			self->bit_index = (byte_index + 4) << 3;
+			return self->bytes[byte_index + 3];
+		}
+		self->bit_index += 8;
+	}
+	return -1;
+}
+
+int plm_buffer_find_start_code(plm_buffer_t *self, int code) {
+	int current = 0;
+	while (TRUE) {
+		current = plm_buffer_next_start_code(self);
+		if (current == code || current == -1) {
+			return current;
+		}
+	}
+	return -1;
+}
+
+int plm_buffer_has_start_code(plm_buffer_t *self, int code) {
+	size_t previous_bit_index = self->bit_index;
+	int previous_discard_read_bytes = self->discard_read_bytes;
+	
+	self->discard_read_bytes = FALSE;
+	int current = plm_buffer_find_start_code(self, code);
+
+	self->bit_index = previous_bit_index;
+	self->discard_read_bytes = previous_discard_read_bytes;
+	return current;
+}
+
+int plm_buffer_peek_non_zero(plm_buffer_t *self, int bit_count) {
+	if (!plm_buffer_has(self, bit_count)) {
+		return FALSE;
+	}
+
+	int val = plm_buffer_read(self, bit_count);
+	self->bit_index -= bit_count;
+	return val != 0;
+}
+
+int16_t plm_buffer_read_vlc(plm_buffer_t *self, const plm_vlc_t *table) {
+	plm_vlc_t state = {0, 0};
+	do {
+		state = table[state.index + plm_buffer_read(self, 1)];
+	} while (state.index > 0);
+	return state.value;
+}
+
+uint16_t plm_buffer_read_vlc_uint(plm_buffer_t *self, const plm_vlc_uint_t *table) {
+	return (uint16_t)plm_buffer_read_vlc(self, (const plm_vlc_t *)table);
+}
+
+
+
+// ----------------------------------------------------------------------------
+// plm_demux implementation
+
+static const int PLM_START_PACK = 0xBA;
+static const int PLM_START_END = 0xB9;
+static const int PLM_START_SYSTEM = 0xBB;
+
+struct plm_demux_t {
+	plm_buffer_t *buffer;
+	int destroy_buffer_when_done;
+	double system_clock_ref;
+
+	size_t last_file_size;
+	double last_decoded_pts;
+	double start_time;
+	double duration;
+
+	int start_code;
+	int has_pack_header;
+	int has_system_header;
+	int has_headers;
+
+	int num_audio_streams;
+	int num_video_streams;
+	plm_packet_t current_packet;
+	plm_packet_t next_packet;
+};
+
+
+void plm_demux_buffer_seek(plm_demux_t *self, size_t pos);
+double plm_demux_decode_time(plm_demux_t *self);
+plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type);
+plm_packet_t *plm_demux_get_packet(plm_demux_t *self);
+
+plm_demux_t *plm_demux_create(plm_buffer_t *buffer, int destroy_when_done) {
+	plm_demux_t *self = (plm_demux_t *)PLM_MALLOC(sizeof(plm_demux_t));
+	memset(self, 0, sizeof(plm_demux_t));
+
+	self->buffer = buffer;
+	self->destroy_buffer_when_done = destroy_when_done;
+
+	self->start_time = PLM_PACKET_INVALID_TS;
+	self->duration = PLM_PACKET_INVALID_TS;
+	self->start_code = -1;
+
+	plm_demux_has_headers(self);
+	return self;
+}
+
+void plm_demux_destroy(plm_demux_t *self) {
+	if (self->destroy_buffer_when_done) {
+		plm_buffer_destroy(self->buffer);
+	}
+	PLM_FREE(self);
+}
+
+int plm_demux_has_headers(plm_demux_t *self) {
+	if (self->has_headers) {
+		return TRUE;
+	}
+
+	// Decode pack header
+	if (!self->has_pack_header) {
+		if (
+			self->start_code != PLM_START_PACK &&
+			plm_buffer_find_start_code(self->buffer, PLM_START_PACK) == -1
+		) {
+			return FALSE;
+		}
+
+		self->start_code = PLM_START_PACK;
+		if (!plm_buffer_has(self->buffer, 64)) {
+			return FALSE;
+		}
+		self->start_code = -1;
+
+		if (plm_buffer_read(self->buffer, 4) != 0x02) {
+			return FALSE;
+		}
+
+		self->system_clock_ref = plm_demux_decode_time(self);
+		plm_buffer_skip(self->buffer, 1);
+		plm_buffer_skip(self->buffer, 22); // mux_rate * 50
+		plm_buffer_skip(self->buffer, 1);
+
+		self->has_pack_header = TRUE;
+	}
+
+	// Decode system header
+	if (!self->has_system_header) {
+		if (
+			self->start_code != PLM_START_SYSTEM &&
+			plm_buffer_find_start_code(self->buffer, PLM_START_SYSTEM) == -1
+		) {
+			return FALSE;
+		}
+
+		self->start_code = PLM_START_SYSTEM;
+		if (!plm_buffer_has(self->buffer, 56)) {
+			return FALSE;
+		}
+		self->start_code = -1;
+
+		plm_buffer_skip(self->buffer, 16); // header_length
+		plm_buffer_skip(self->buffer, 24); // rate bound
+		self->num_audio_streams = plm_buffer_read(self->buffer, 6);
+		plm_buffer_skip(self->buffer, 5); // misc flags
+		self->num_video_streams = plm_buffer_read(self->buffer, 5);
+
+		self->has_system_header = TRUE;
+	}
+
+	self->has_headers = TRUE;
+	return TRUE;
+}
+
+int plm_demux_get_num_video_streams(plm_demux_t *self) {
+	return plm_demux_has_headers(self)
+		? self->num_video_streams
+		: 0;
+}
+
+int plm_demux_get_num_audio_streams(plm_demux_t *self) {
+	return plm_demux_has_headers(self)
+		? self->num_audio_streams
+		: 0;
+}
+
+void plm_demux_rewind(plm_demux_t *self) {
+	plm_buffer_rewind(self->buffer);
+	self->current_packet.length = 0;
+	self->next_packet.length = 0;
+	self->start_code = -1;
+}
+
+int plm_demux_has_ended(plm_demux_t *self) {
+	return plm_buffer_has_ended(self->buffer);
+}
+
+void plm_demux_buffer_seek(plm_demux_t *self, size_t pos) {
+	plm_buffer_seek(self->buffer, pos);
+	self->current_packet.length = 0;
+	self->next_packet.length = 0;
+	self->start_code = -1;
+}
+
+double plm_demux_get_start_time(plm_demux_t *self, int type) {
+	if (self->start_time != PLM_PACKET_INVALID_TS) {
+		return self->start_time;
+	}
+
+	int previous_pos = plm_buffer_tell(self->buffer);
+	int previous_start_code = self->start_code;
+	
+	// Find first video PTS
+	plm_demux_rewind(self);
+	do {
+		plm_packet_t *packet = plm_demux_decode(self);
+		if (!packet) {
+			break;
+		}
+		if (packet->type == type) {
+			self->start_time = packet->pts;
+		}
+	} while (self->start_time == PLM_PACKET_INVALID_TS);
+
+	plm_demux_buffer_seek(self, previous_pos);
+	self->start_code = previous_start_code;
+	return self->start_time;
+}
+
+double plm_demux_get_duration(plm_demux_t *self, int type) {
+	size_t file_size = plm_buffer_get_size(self->buffer);
+
+	if (
+		self->duration != PLM_PACKET_INVALID_TS &&
+		self->last_file_size == file_size
+	) {
+		return self->duration;
+	}
+
+	size_t previous_pos = plm_buffer_tell(self->buffer);
+	int previous_start_code = self->start_code;
+	
+	// Find last video PTS. Start searching 64kb from the end and go further 
+	// back if needed.
+	long start_range = 64 * 1024;
+	long max_range = 4096 * 1024;
+	for (long range = start_range; range <= max_range; range *= 2) {
+		long seek_pos = file_size - range;
+		if (seek_pos < 0) {
+			seek_pos = 0;
+			range = max_range; // Make sure to bail after this round
+		}
+		plm_demux_buffer_seek(self, seek_pos);
+		self->current_packet.length = 0;
+
+		double last_pts = PLM_PACKET_INVALID_TS;
+		plm_packet_t *packet = NULL;
+		while ((packet = plm_demux_decode(self))) {
+			if (packet->pts != PLM_PACKET_INVALID_TS && packet->type == type) {
+				last_pts = packet->pts;
+			}
+		}
+		if (last_pts != PLM_PACKET_INVALID_TS) {
+			self->duration = last_pts - plm_demux_get_start_time(self, type);
+			break;
+		}
+	}
+
+	plm_demux_buffer_seek(self, previous_pos);
+	self->start_code = previous_start_code;
+	self->last_file_size = file_size;
+	return self->duration;
+}
+
+plm_packet_t *plm_demux_seek(plm_demux_t *self, double seek_time, int type, int force_intra) {
+	if (!plm_demux_has_headers(self)) {
+		return NULL;
+	}
+
+	// Using the current time, current byte position and the average bytes per
+	// second for this file, try to jump to a byte position that hopefully has
+	// packets containing timestamps within one second before to the desired 
+	// seek_time.
+
+	// If we hit close to the seek_time scan through all packets to find the
+	// last one (just before the seek_time) containing an intra frame.
+	// Otherwise we should at least be closer than before. Calculate the bytes
+	// per second for the jumped range and jump again.
+
+	// The number of retries here is hard-limited to a generous amount. Usually
+	// the correct range is found after 1--5 jumps, even for files with very 
+	// variable bitrates. If significantly more jumps are needed, there's
+	// probably something wrong with the file and we just avoid getting into an
+	// infinite loop. 32 retries should be enough for anybody.
+
+	double duration = plm_demux_get_duration(self, type);
+	long file_size = plm_buffer_get_size(self->buffer);
+	long byterate = file_size / duration;
+
+	double cur_time = self->last_decoded_pts;
+	double scan_span = 1;
+
+	if (seek_time > duration) {
+		seek_time = duration;
+	}
+	else if (seek_time < 0) {
+		seek_time = 0;
+	}
+	seek_time += self->start_time;
+
+	for (int retry = 0; retry < 32; retry++) {
+		int found_packet_with_pts = FALSE;
+		int found_packet_in_range = FALSE;
+		long last_valid_packet_start = -1;
+		double first_packet_time = PLM_PACKET_INVALID_TS;
+
+		long cur_pos = plm_buffer_tell(self->buffer);
+
+		// Estimate byte offset and jump to it.
+		long offset = (seek_time - cur_time - scan_span) * byterate;
+		long seek_pos = cur_pos + offset;
+		if (seek_pos < 0) {
+			seek_pos = 0;
+		}
+		else if (seek_pos > file_size - 256) {
+			seek_pos = file_size - 256;
+		}
+
+		plm_demux_buffer_seek(self, seek_pos);
+
+		// Scan through all packets up to the seek_time to find the last packet
+		// containing an intra frame.
+		while (plm_buffer_find_start_code(self->buffer, type) != -1) {
+			long packet_start = plm_buffer_tell(self->buffer);
+			plm_packet_t *packet = plm_demux_decode_packet(self, type);
+
+			// Skip packet if it has no PTS
+			if (!packet || packet->pts == PLM_PACKET_INVALID_TS) {
+				continue;
+			}
+
+			// Bail scanning through packets if we hit one that is outside
+			// seek_time - scan_span.
+			// We also adjust the cur_time and byterate values here so the next 
+			// iteration can be a bit more precise.
+			if (packet->pts > seek_time || packet->pts < seek_time - scan_span) {
+				found_packet_with_pts = TRUE;
+				byterate = (seek_pos - cur_pos) / (packet->pts - cur_time);
+				cur_time = packet->pts;
+				break;
+			}
+
+			// If we are still here, it means this packet is in close range to
+			// the seek_time. If this is the first packet for this jump position
+			// record the PTS. If we later have to back off, when there was no
+			// intra frame in this range, we can lower the seek_time to not scan
+			// this range again.
+			if (!found_packet_in_range) {
+				found_packet_in_range = TRUE;
+				first_packet_time = packet->pts;
+			}
+
+			// Check if this is an intra frame packet. If so, record the buffer
+			// position of the start of this packet. We want to jump back to it 
+			// later, when we know it's the last intra frame before desired
+			// seek time.
+			if (force_intra) {
+				for (size_t i = 0; i < packet->length - 6; i++) {
+					// Find the START_PICTURE code
+					if (
+						packet->data[i] == 0x00 &&
+						packet->data[i + 1] == 0x00 &&
+						packet->data[i + 2] == 0x01 &&
+						packet->data[i + 3] == 0x00
+					) {
+						// Bits 11--13 in the picture header contain the frame 
+						// type, where 1=Intra
+						if ((packet->data[i + 5] & 0x38) == 8) {
+							last_valid_packet_start = packet_start;
+						}
+						break;
+					}
+				}
+			}
+
+			// If we don't want intra frames, just use the last PTS found.
+			else {
+				last_valid_packet_start = packet_start;
+			}
+		}
+
+		// If there was at least one intra frame in the range scanned above,
+		// our search is over. Jump back to the packet and decode it again.
+		if (last_valid_packet_start != -1) {
+			plm_demux_buffer_seek(self, last_valid_packet_start);
+			return plm_demux_decode_packet(self, type);
+		}
+
+		// If we hit the right range, but still found no intra frame, we have
+		// to increases the scan_span. This is done exponentially to also handle
+		// video files with very few intra frames.
+		else if (found_packet_in_range) {
+			scan_span *= 2;
+			seek_time = first_packet_time;
+		}
+
+		// If we didn't find any packet with a PTS, it probably means we reached
+		// the end of the file. Estimate byterate and cur_time accordingly.
+		else if (!found_packet_with_pts) {
+			byterate = (seek_pos - cur_pos) / (duration - cur_time);
+			cur_time = duration;
+		}
+	}
+
+	return NULL;
+}
+
+plm_packet_t *plm_demux_decode(plm_demux_t *self) {
+	if (!plm_demux_has_headers(self)) {
+		return NULL;
+	}
+
+	if (self->current_packet.length) {
+		size_t bits_till_next_packet = self->current_packet.length << 3;
+		if (!plm_buffer_has(self->buffer, bits_till_next_packet)) {
+			return NULL;
+		}
+		plm_buffer_skip(self->buffer, bits_till_next_packet);
+		self->current_packet.length = 0;
+	}
+
+	// Pending packet waiting for data?
+	if (self->next_packet.length) {
+		return plm_demux_get_packet(self);
+	}
+
+	// Pending packet waiting for header?
+	if (self->start_code != -1) {
+		return plm_demux_decode_packet(self, self->start_code);
+	}
+
+	do {
+		self->start_code = plm_buffer_next_start_code(self->buffer);
+		if (
+			self->start_code == PLM_DEMUX_PACKET_VIDEO_1 || 
+			self->start_code == PLM_DEMUX_PACKET_PRIVATE || (
+				self->start_code >= PLM_DEMUX_PACKET_AUDIO_1 && 
+				self->start_code <= PLM_DEMUX_PACKET_AUDIO_4
+			)
+		) {
+			return plm_demux_decode_packet(self, self->start_code);
+		}
+	} while (self->start_code != -1);
+
+	return NULL;
+}
+
+double plm_demux_decode_time(plm_demux_t *self) {
+	int64_t clock = plm_buffer_read(self->buffer, 3) << 30;
+	plm_buffer_skip(self->buffer, 1);
+	clock |= plm_buffer_read(self->buffer, 15) << 15;
+	plm_buffer_skip(self->buffer, 1);
+	clock |= plm_buffer_read(self->buffer, 15);
+	plm_buffer_skip(self->buffer, 1);
+	return (double)clock / 90000.0;
+}
+
+plm_packet_t *plm_demux_decode_packet(plm_demux_t *self, int type) {
+	if (!plm_buffer_has(self->buffer, 16 << 3)) {
+		return NULL;
+	}
+
+	self->start_code = -1;
+
+	self->next_packet.type = type;
+	self->next_packet.length = plm_buffer_read(self->buffer, 16);
+	self->next_packet.length -= plm_buffer_skip_bytes(self->buffer, 0xff); // stuffing
+
+	// skip P-STD
+	if (plm_buffer_read(self->buffer, 2) == 0x01) {
+		plm_buffer_skip(self->buffer, 16);
+		self->next_packet.length -= 2;
+	}
+
+	int pts_dts_marker = plm_buffer_read(self->buffer, 2);
+	if (pts_dts_marker == 0x03) {
+		self->next_packet.pts = plm_demux_decode_time(self);
+		self->last_decoded_pts = self->next_packet.pts;
+		plm_buffer_skip(self->buffer, 40); // skip dts
+		self->next_packet.length -= 10;
+	}
+	else if (pts_dts_marker == 0x02) {
+		self->next_packet.pts = plm_demux_decode_time(self);
+		self->last_decoded_pts = self->next_packet.pts;
+		self->next_packet.length -= 5;
+	}
+	else if (pts_dts_marker == 0x00) {
+		self->next_packet.pts = PLM_PACKET_INVALID_TS;
+		plm_buffer_skip(self->buffer, 4);
+		self->next_packet.length -= 1;
+	}
+	else {
+		return NULL; // invalid
+	}
+	
+	return plm_demux_get_packet(self);
+}
+
+plm_packet_t *plm_demux_get_packet(plm_demux_t *self) {
+	if (!plm_buffer_has(self->buffer, self->next_packet.length << 3)) {
+		return NULL;
+	}
+
+	self->current_packet.data = self->buffer->bytes + (self->buffer->bit_index >> 3);
+	self->current_packet.length = self->next_packet.length;
+	self->current_packet.type = self->next_packet.type;
+	self->current_packet.pts = self->next_packet.pts;
+
+	self->next_packet.length = 0;
+	return &self->current_packet;
+}
+
+
+
+// -----------------------------------------------------------------------------
+// plm_video implementation
+
+// Inspired by Java MPEG-1 Video Decoder and Player by Zoltan Korandi 
+// https://sourceforge.net/projects/javampeg1video/
+
+static const int PLM_VIDEO_PICTURE_TYPE_INTRA = 1;
+static const int PLM_VIDEO_PICTURE_TYPE_PREDICTIVE = 2;
+static const int PLM_VIDEO_PICTURE_TYPE_B = 3;
+
+static const int PLM_START_SEQUENCE = 0xB3;
+static const int PLM_START_SLICE_FIRST = 0x01;
+static const int PLM_START_SLICE_LAST = 0xAF;
+static const int PLM_START_PICTURE = 0x00;
+static const int PLM_START_EXTENSION = 0xB5;
+static const int PLM_START_USER_DATA = 0xB2;
+
+#define PLM_START_IS_SLICE(c) \
+	(c >= PLM_START_SLICE_FIRST && c <= PLM_START_SLICE_LAST)
+
+static const double PLM_VIDEO_PICTURE_RATE[] = {
+	0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940,
+	60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000
+};
+
+static const uint8_t PLM_VIDEO_ZIG_ZAG[] = {
+	 0,  1,  8, 16,  9,  2,  3, 10,
+	17, 24, 32, 25, 18, 11,  4,  5,
+	12, 19, 26, 33, 40, 48, 41, 34,
+	27, 20, 13,  6,  7, 14, 21, 28,
+	35, 42, 49, 56, 57, 50, 43, 36,
+	29, 22, 15, 23, 30, 37, 44, 51,
+	58, 59, 52, 45, 38, 31, 39, 46,
+	53, 60, 61, 54, 47, 55, 62, 63
+};
+
+static const uint8_t PLM_VIDEO_INTRA_QUANT_MATRIX[] = {
+	 8, 16, 19, 22, 26, 27, 29, 34,
+	16, 16, 22, 24, 27, 29, 34, 37,
+	19, 22, 26, 27, 29, 34, 34, 38,
+	22, 22, 26, 27, 29, 34, 37, 40,
+	22, 26, 27, 29, 32, 35, 40, 48,
+	26, 27, 29, 32, 35, 40, 48, 58,
+	26, 27, 29, 34, 38, 46, 56, 69,
+	27, 29, 35, 38, 46, 56, 69, 83
+};
+
+static const uint8_t PLM_VIDEO_NON_INTRA_QUANT_MATRIX[] = {
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16,
+	16, 16, 16, 16, 16, 16, 16, 16
+};
+
+static const uint8_t PLM_VIDEO_PREMULTIPLIER_MATRIX[] = {
+	32, 44, 42, 38, 32, 25, 17,  9,
+	44, 62, 58, 52, 44, 35, 24, 12,
+	42, 58, 55, 49, 42, 33, 23, 12,
+	38, 52, 49, 44, 38, 30, 20, 10,
+	32, 44, 42, 38, 32, 25, 17,  9,
+	25, 35, 33, 30, 25, 20, 14,  7,
+	17, 24, 23, 20, 17, 14,  9,  5,
+	 9, 12, 12, 10,  9,  7,  5,  2
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT[] = {
+	{  1 << 1,    0}, {       0,    1},  //   0: x
+	{  2 << 1,    0}, {  3 << 1,    0},  //   1: 0x
+	{  4 << 1,    0}, {  5 << 1,    0},  //   2: 00x
+	{       0,    3}, {       0,    2},  //   3: 01x
+	{  6 << 1,    0}, {  7 << 1,    0},  //   4: 000x
+	{       0,    5}, {       0,    4},  //   5: 001x
+	{  8 << 1,    0}, {  9 << 1,    0},  //   6: 0000x
+	{       0,    7}, {       0,    6},  //   7: 0001x
+	{ 10 << 1,    0}, { 11 << 1,    0},  //   8: 0000 0x
+	{ 12 << 1,    0}, { 13 << 1,    0},  //   9: 0000 1x
+	{ 14 << 1,    0}, { 15 << 1,    0},  //  10: 0000 00x
+	{ 16 << 1,    0}, { 17 << 1,    0},  //  11: 0000 01x
+	{ 18 << 1,    0}, { 19 << 1,    0},  //  12: 0000 10x
+	{       0,    9}, {       0,    8},  //  13: 0000 11x
+	{      -1,    0}, { 20 << 1,    0},  //  14: 0000 000x
+	{      -1,    0}, { 21 << 1,    0},  //  15: 0000 001x
+	{ 22 << 1,    0}, { 23 << 1,    0},  //  16: 0000 010x
+	{       0,   15}, {       0,   14},  //  17: 0000 011x
+	{       0,   13}, {       0,   12},  //  18: 0000 100x
+	{       0,   11}, {       0,   10},  //  19: 0000 101x
+	{ 24 << 1,    0}, { 25 << 1,    0},  //  20: 0000 0001x
+	{ 26 << 1,    0}, { 27 << 1,    0},  //  21: 0000 0011x
+	{ 28 << 1,    0}, { 29 << 1,    0},  //  22: 0000 0100x
+	{ 30 << 1,    0}, { 31 << 1,    0},  //  23: 0000 0101x
+	{ 32 << 1,    0}, {      -1,    0},  //  24: 0000 0001 0x
+	{      -1,    0}, { 33 << 1,    0},  //  25: 0000 0001 1x
+	{ 34 << 1,    0}, { 35 << 1,    0},  //  26: 0000 0011 0x
+	{ 36 << 1,    0}, { 37 << 1,    0},  //  27: 0000 0011 1x
+	{ 38 << 1,    0}, { 39 << 1,    0},  //  28: 0000 0100 0x
+	{       0,   21}, {       0,   20},  //  29: 0000 0100 1x
+	{       0,   19}, {       0,   18},  //  30: 0000 0101 0x
+	{       0,   17}, {       0,   16},  //  31: 0000 0101 1x
+	{       0,   35}, {      -1,    0},  //  32: 0000 0001 00x
+	{      -1,    0}, {       0,   34},  //  33: 0000 0001 11x
+	{       0,   33}, {       0,   32},  //  34: 0000 0011 00x
+	{       0,   31}, {       0,   30},  //  35: 0000 0011 01x
+	{       0,   29}, {       0,   28},  //  36: 0000 0011 10x
+	{       0,   27}, {       0,   26},  //  37: 0000 0011 11x
+	{       0,   25}, {       0,   24},  //  38: 0000 0100 00x
+	{       0,   23}, {       0,   22},  //  39: 0000 0100 01x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_INTRA[] = {
+	{  1 << 1,    0}, {       0,  0x01},  //   0: x
+	{      -1,    0}, {       0,  0x11},  //   1: 0x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE[] = {
+	{  1 << 1,    0}, {       0, 0x0a},  //   0: x
+	{  2 << 1,    0}, {       0, 0x02},  //   1: 0x
+	{  3 << 1,    0}, {       0, 0x08},  //   2: 00x
+	{  4 << 1,    0}, {  5 << 1,    0},  //   3: 000x
+	{  6 << 1,    0}, {       0, 0x12},  //   4: 0000x
+	{       0, 0x1a}, {       0, 0x01},  //   5: 0001x
+	{      -1,    0}, {       0, 0x11},  //   6: 0000 0x
+};
+
+static const plm_vlc_t PLM_VIDEO_MACROBLOCK_TYPE_B[] = {
+	{  1 << 1,    0}, {  2 << 1,    0},  //   0: x
+	{  3 << 1,    0}, {  4 << 1,    0},  //   1: 0x
+	{       0, 0x0c}, {       0, 0x0e},  //   2: 1x
+	{  5 << 1,    0}, {  6 << 1,    0},  //   3: 00x
+	{       0, 0x04}, {       0, 0x06},  //   4: 01x
+	{  7 << 1,    0}, {  8 << 1,    0},  //   5: 000x
+	{       0, 0x08}, {       0, 0x0a},  //   6: 001x
+	{  9 << 1,    0}, { 10 << 1,    0},  //   7: 0000x
+	{       0, 0x1e}, {       0, 0x01},  //   8: 0001x
+	{      -1,    0}, {       0, 0x11},  //   9: 0000 0x
+	{       0, 0x16}, {       0, 0x1a},  //  10: 0000 1x
+};
+
+static const plm_vlc_t *PLM_VIDEO_MACROBLOCK_TYPE[] = {
+	NULL,
+	PLM_VIDEO_MACROBLOCK_TYPE_INTRA,
+	PLM_VIDEO_MACROBLOCK_TYPE_PREDICTIVE,
+	PLM_VIDEO_MACROBLOCK_TYPE_B
+};
+
+static const plm_vlc_t PLM_VIDEO_CODE_BLOCK_PATTERN[] = {
+	{  1 << 1,    0}, {  2 << 1,    0},  //   0: x
+	{  3 << 1,    0}, {  4 << 1,    0},  //   1: 0x
+	{  5 << 1,    0}, {  6 << 1,    0},  //   2: 1x
+	{  7 << 1,    0}, {  8 << 1,    0},  //   3: 00x
+	{  9 << 1,    0}, { 10 << 1,    0},  //   4: 01x
+	{ 11 << 1,    0}, { 12 << 1,    0},  //   5: 10x
+	{ 13 << 1,    0}, {       0,   60},  //   6: 11x
+	{ 14 << 1,    0}, { 15 << 1,    0},  //   7: 000x
+	{ 16 << 1,    0}, { 17 << 1,    0},  //   8: 001x
+	{ 18 << 1,    0}, { 19 << 1,    0},  //   9: 010x
+	{ 20 << 1,    0}, { 21 << 1,    0},  //  10: 011x
+	{ 22 << 1,    0}, { 23 << 1,    0},  //  11: 100x
+	{       0,   32}, {       0,   16},  //  12: 101x
+	{       0,    8}, {       0,    4},  //  13: 110x
+	{ 24 << 1,    0}, { 25 << 1,    0},  //  14: 0000x
+	{ 26 << 1,    0}, { 27 << 1,    0},  //  15: 0001x
+	{ 28 << 1,    0}, { 29 << 1,    0},  //  16: 0010x
+	{ 30 << 1,    0}, { 31 << 1,    0},  //  17: 0011x
+	{       0,   62}, {       0,    2},  //  18: 0100x
+	{       0,   61}, {       0,    1},  //  19: 0101x
+	{       0,   56}, {       0,   52},  //  20: 0110x
+	{       0,   44}, {       0,   28},  //  21: 0111x
+	{       0,   40}, {       0,   20},  //  22: 1000x
+	{       0,   48}, {       0,   12},  //  23: 1001x
+	{ 32 << 1,    0}, { 33 << 1,    0},  //  24: 0000 0x
+	{ 34 << 1,    0}, { 35 << 1,    0},  //  25: 0000 1x
+	{ 36 << 1,    0}, { 37 << 1,    0},  //  26: 0001 0x
+	{ 38 << 1,    0}, { 39 << 1,    0},  //  27: 0001 1x
+	{ 40 << 1,    0}, { 41 << 1,    0},  //  28: 0010 0x
+	{ 42 << 1,    0}, { 43 << 1,    0},  //  29: 0010 1x
+	{       0,   63}, {       0,    3},  //  30: 0011 0x
+	{       0,   36}, {       0,   24},  //  31: 0011 1x
+	{ 44 << 1,    0}, { 45 << 1,    0},  //  32: 0000 00x
+	{ 46 << 1,    0}, { 47 << 1,    0},  //  33: 0000 01x
+	{ 48 << 1,    0}, { 49 << 1,    0},  //  34: 0000 10x
+	{ 50 << 1,    0}, { 51 << 1,    0},  //  35: 0000 11x
+	{ 52 << 1,    0}, { 53 << 1,    0},  //  36: 0001 00x
+	{ 54 << 1,    0}, { 55 << 1,    0},  //  37: 0001 01x
+	{ 56 << 1,    0}, { 57 << 1,    0},  //  38: 0001 10x
+	{ 58 << 1,    0}, { 59 << 1,    0},  //  39: 0001 11x
+	{       0,   34}, {       0,   18},  //  40: 0010 00x
+	{       0,   10}, {       0,    6},  //  41: 0010 01x
+	{       0,   33}, {       0,   17},  //  42: 0010 10x
+	{       0,    9}, {       0,    5},  //  43: 0010 11x
+	{      -1,    0}, { 60 << 1,    0},  //  44: 0000 000x
+	{ 61 << 1,    0}, { 62 << 1,    0},  //  45: 0000 001x
+	{       0,   58}, {       0,   54},  //  46: 0000 010x
+	{       0,   46}, {       0,   30},  //  47: 0000 011x
+	{       0,   57}, {       0,   53},  //  48: 0000 100x
+	{       0,   45}, {       0,   29},  //  49: 0000 101x
+	{       0,   38}, {       0,   26},  //  50: 0000 110x
+	{       0,   37}, {       0,   25},  //  51: 0000 111x
+	{       0,   43}, {       0,   23},  //  52: 0001 000x
+	{       0,   51}, {       0,   15},  //  53: 0001 001x
+	{       0,   42}, {       0,   22},  //  54: 0001 010x
+	{       0,   50}, {       0,   14},  //  55: 0001 011x
+	{       0,   41}, {       0,   21},  //  56: 0001 100x
+	{       0,   49}, {       0,   13},  //  57: 0001 101x
+	{       0,   35}, {       0,   19},  //  58: 0001 110x
+	{       0,   11}, {       0,    7},  //  59: 0001 111x
+	{       0,   39}, {       0,   27},  //  60: 0000 0001x
+	{       0,   59}, {       0,   55},  //  61: 0000 0010x
+	{       0,   47}, {       0,   31},  //  62: 0000 0011x
+};
+
+static const plm_vlc_t PLM_VIDEO_MOTION[] = {
+	{  1 << 1,    0}, {       0,    0},  //   0: x
+	{  2 << 1,    0}, {  3 << 1,    0},  //   1: 0x
+	{  4 << 1,    0}, {  5 << 1,    0},  //   2: 00x
+	{       0,    1}, {       0,   -1},  //   3: 01x
+	{  6 << 1,    0}, {  7 << 1,    0},  //   4: 000x
+	{       0,    2}, {       0,   -2},  //   5: 001x
+	{  8 << 1,    0}, {  9 << 1,    0},  //   6: 0000x
+	{       0,    3}, {       0,   -3},  //   7: 0001x
+	{ 10 << 1,    0}, { 11 << 1,    0},  //   8: 0000 0x
+	{ 12 << 1,    0}, { 13 << 1,    0},  //   9: 0000 1x
+	{      -1,    0}, { 14 << 1,    0},  //  10: 0000 00x
+	{ 15 << 1,    0}, { 16 << 1,    0},  //  11: 0000 01x
+	{ 17 << 1,    0}, { 18 << 1,    0},  //  12: 0000 10x
+	{       0,    4}, {       0,   -4},  //  13: 0000 11x
+	{      -1,    0}, { 19 << 1,    0},  //  14: 0000 001x
+	{ 20 << 1,    0}, { 21 << 1,    0},  //  15: 0000 010x
+	{       0,    7}, {       0,   -7},  //  16: 0000 011x
+	{       0,    6}, {       0,   -6},  //  17: 0000 100x
+	{       0,    5}, {       0,   -5},  //  18: 0000 101x
+	{ 22 << 1,    0}, { 23 << 1,    0},  //  19: 0000 0011x
+	{ 24 << 1,    0}, { 25 << 1,    0},  //  20: 0000 0100x
+	{ 26 << 1,    0}, { 27 << 1,    0},  //  21: 0000 0101x
+	{ 28 << 1,    0}, { 29 << 1,    0},  //  22: 0000 0011 0x
+	{ 30 << 1,    0}, { 31 << 1,    0},  //  23: 0000 0011 1x
+	{ 32 << 1,    0}, { 33 << 1,    0},  //  24: 0000 0100 0x
+	{       0,   10}, {       0,  -10},  //  25: 0000 0100 1x
+	{       0,    9}, {       0,   -9},  //  26: 0000 0101 0x
+	{       0,    8}, {       0,   -8},  //  27: 0000 0101 1x
+	{       0,   16}, {       0,  -16},  //  28: 0000 0011 00x
+	{       0,   15}, {       0,  -15},  //  29: 0000 0011 01x
+	{       0,   14}, {       0,  -14},  //  30: 0000 0011 10x
+	{       0,   13}, {       0,  -13},  //  31: 0000 0011 11x
+	{       0,   12}, {       0,  -12},  //  32: 0000 0100 00x
+	{       0,   11}, {       0,  -11},  //  33: 0000 0100 01x
+};
+
+static const plm_vlc_t PLM_VIDEO_DCT_SIZE_LUMINANCE[] = {
+	{  1 << 1,    0}, {  2 << 1,    0},  //   0: x
+	{       0,    1}, {       0,    2},  //   1: 0x
+	{  3 << 1,    0}, {  4 << 1,    0},  //   2: 1x
+	{       0,    0}, {       0,    3},  //   3: 10x
+	{       0,    4}, {  5 << 1,    0},  //   4: 11x
+	{       0,    5}, {  6 << 1,    0},  //   5: 111x
+	{       0,    6}, {  7 << 1,    0},  //   6: 1111x
+	{       0,    7}, {  8 << 1,    0},  //   7: 1111 1x
+	{       0,    8}, {      -1,    0},  //   8: 1111 11x
+};
+
+static const plm_vlc_t PLM_VIDEO_DCT_SIZE_CHROMINANCE[] = {
+	{  1 << 1,    0}, {  2 << 1,    0},  //   0: x
+	{       0,    0}, {       0,    1},  //   1: 0x
+	{       0,    2}, {  3 << 1,    0},  //   2: 1x
+	{       0,    3}, {  4 << 1,    0},  //   3: 11x
+	{       0,    4}, {  5 << 1,    0},  //   4: 111x
+	{       0,    5}, {  6 << 1,    0},  //   5: 1111x
+	{       0,    6}, {  7 << 1,    0},  //   6: 1111 1x
+	{       0,    7}, {  8 << 1,    0},  //   7: 1111 11x
+	{       0,    8}, {      -1,    0},  //   8: 1111 111x
+};
+
+static const plm_vlc_t *PLM_VIDEO_DCT_SIZE[] = {
+	PLM_VIDEO_DCT_SIZE_LUMINANCE,
+	PLM_VIDEO_DCT_SIZE_CHROMINANCE,
+	PLM_VIDEO_DCT_SIZE_CHROMINANCE
+};
+
+
+//  dct_coeff bitmap:
+//    0xff00  run
+//    0x00ff  level
+
+//  Decoded values are unsigned. Sign bit follows in the stream.
+
+static const plm_vlc_uint_t PLM_VIDEO_DCT_COEFF[] = {
+	{  1 << 1,        0}, {       0,   0x0001},  //   0: x
+	{  2 << 1,        0}, {  3 << 1,        0},  //   1: 0x
+	{  4 << 1,        0}, {  5 << 1,        0},  //   2: 00x
+	{  6 << 1,        0}, {       0,   0x0101},  //   3: 01x
+	{  7 << 1,        0}, {  8 << 1,        0},  //   4: 000x
+	{  9 << 1,        0}, { 10 << 1,        0},  //   5: 001x
+	{       0,   0x0002}, {       0,   0x0201},  //   6: 010x
+	{ 11 << 1,        0}, { 12 << 1,        0},  //   7: 0000x
+	{ 13 << 1,        0}, { 14 << 1,        0},  //   8: 0001x
+	{ 15 << 1,        0}, {       0,   0x0003},  //   9: 0010x
+	{       0,   0x0401}, {       0,   0x0301},  //  10: 0011x
+	{ 16 << 1,        0}, {       0,   0xffff},  //  11: 0000 0x
+	{ 17 << 1,        0}, { 18 << 1,        0},  //  12: 0000 1x
+	{       0,   0x0701}, {       0,   0x0601},  //  13: 0001 0x
+	{       0,   0x0102}, {       0,   0x0501},  //  14: 0001 1x
+	{ 19 << 1,        0}, { 20 << 1,        0},  //  15: 0010 0x
+	{ 21 << 1,        0}, { 22 << 1,        0},  //  16: 0000 00x
+	{       0,   0x0202}, {       0,   0x0901},  //  17: 0000 10x
+	{       0,   0x0004}, {       0,   0x0801},  //  18: 0000 11x
+	{ 23 << 1,        0}, { 24 << 1,        0},  //  19: 0010 00x
+	{ 25 << 1,        0}, { 26 << 1,        0},  //  20: 0010 01x
+	{ 27 << 1,        0}, { 28 << 1,        0},  //  21: 0000 000x
+	{ 29 << 1,        0}, { 30 << 1,        0},  //  22: 0000 001x
+	{       0,   0x0d01}, {       0,   0x0006},  //  23: 0010 000x
+	{       0,   0x0c01}, {       0,   0x0b01},  //  24: 0010 001x
+	{       0,   0x0302}, {       0,   0x0103},  //  25: 0010 010x
+	{       0,   0x0005}, {       0,   0x0a01},  //  26: 0010 011x
+	{ 31 << 1,        0}, { 32 << 1,        0},  //  27: 0000 0000x
+	{ 33 << 1,        0}, { 34 << 1,        0},  //  28: 0000 0001x
+	{ 35 << 1,        0}, { 36 << 1,        0},  //  29: 0000 0010x
+	{ 37 << 1,        0}, { 38 << 1,        0},  //  30: 0000 0011x
+	{ 39 << 1,        0}, { 40 << 1,        0},  //  31: 0000 0000 0x
+	{ 41 << 1,        0}, { 42 << 1,        0},  //  32: 0000 0000 1x
+	{ 43 << 1,        0}, { 44 << 1,        0},  //  33: 0000 0001 0x
+	{ 45 << 1,        0}, { 46 << 1,        0},  //  34: 0000 0001 1x
+	{       0,   0x1001}, {       0,   0x0502},  //  35: 0000 0010 0x
+	{       0,   0x0007}, {       0,   0x0203},  //  36: 0000 0010 1x
+	{       0,   0x0104}, {       0,   0x0f01},  //  37: 0000 0011 0x
+	{       0,   0x0e01}, {       0,   0x0402},  //  38: 0000 0011 1x
+	{ 47 << 1,        0}, { 48 << 1,        0},  //  39: 0000 0000 00x
+	{ 49 << 1,        0}, { 50 << 1,        0},  //  40: 0000 0000 01x
+	{ 51 << 1,        0}, { 52 << 1,        0},  //  41: 0000 0000 10x
+	{ 53 << 1,        0}, { 54 << 1,        0},  //  42: 0000 0000 11x
+	{ 55 << 1,        0}, { 56 << 1,        0},  //  43: 0000 0001 00x
+	{ 57 << 1,        0}, { 58 << 1,        0},  //  44: 0000 0001 01x
+	{ 59 << 1,        0}, { 60 << 1,        0},  //  45: 0000 0001 10x
+	{ 61 << 1,        0}, { 62 << 1,        0},  //  46: 0000 0001 11x
+	{      -1,        0}, { 63 << 1,        0},  //  47: 0000 0000 000x
+	{ 64 << 1,        0}, { 65 << 1,        0},  //  48: 0000 0000 001x
+	{ 66 << 1,        0}, { 67 << 1,        0},  //  49: 0000 0000 010x
+	{ 68 << 1,        0}, { 69 << 1,        0},  //  50: 0000 0000 011x
+	{ 70 << 1,        0}, { 71 << 1,        0},  //  51: 0000 0000 100x
+	{ 72 << 1,        0}, { 73 << 1,        0},  //  52: 0000 0000 101x
+	{ 74 << 1,        0}, { 75 << 1,        0},  //  53: 0000 0000 110x
+	{ 76 << 1,        0}, { 77 << 1,        0},  //  54: 0000 0000 111x
+	{       0,   0x000b}, {       0,   0x0802},  //  55: 0000 0001 000x
+	{       0,   0x0403}, {       0,   0x000a},  //  56: 0000 0001 001x
+	{       0,   0x0204}, {       0,   0x0702},  //  57: 0000 0001 010x
+	{       0,   0x1501}, {       0,   0x1401},  //  58: 0000 0001 011x
+	{       0,   0x0009}, {       0,   0x1301},  //  59: 0000 0001 100x
+	{       0,   0x1201}, {       0,   0x0105},  //  60: 0000 0001 101x
+	{       0,   0x0303}, {       0,   0x0008},  //  61: 0000 0001 110x
+	{       0,   0x0602}, {       0,   0x1101},  //  62: 0000 0001 111x
+	{ 78 << 1,        0}, { 79 << 1,        0},  //  63: 0000 0000 0001x
+	{ 80 << 1,        0}, { 81 << 1,        0},  //  64: 0000 0000 0010x
+	{ 82 << 1,        0}, { 83 << 1,        0},  //  65: 0000 0000 0011x
+	{ 84 << 1,        0}, { 85 << 1,        0},  //  66: 0000 0000 0100x
+	{ 86 << 1,        0}, { 87 << 1,        0},  //  67: 0000 0000 0101x
+	{ 88 << 1,        0}, { 89 << 1,        0},  //  68: 0000 0000 0110x
+	{ 90 << 1,        0}, { 91 << 1,        0},  //  69: 0000 0000 0111x
+	{       0,   0x0a02}, {       0,   0x0902},  //  70: 0000 0000 1000x
+	{       0,   0x0503}, {       0,   0x0304},  //  71: 0000 0000 1001x
+	{       0,   0x0205}, {       0,   0x0107},  //  72: 0000 0000 1010x
+	{       0,   0x0106}, {       0,   0x000f},  //  73: 0000 0000 1011x
+	{       0,   0x000e}, {       0,   0x000d},  //  74: 0000 0000 1100x
+	{       0,   0x000c}, {       0,   0x1a01},  //  75: 0000 0000 1101x
+	{       0,   0x1901}, {       0,   0x1801},  //  76: 0000 0000 1110x
+	{       0,   0x1701}, {       0,   0x1601},  //  77: 0000 0000 1111x
+	{ 92 << 1,        0}, { 93 << 1,        0},  //  78: 0000 0000 0001 0x
+	{ 94 << 1,        0}, { 95 << 1,        0},  //  79: 0000 0000 0001 1x
+	{ 96 << 1,        0}, { 97 << 1,        0},  //  80: 0000 0000 0010 0x
+	{ 98 << 1,        0}, { 99 << 1,        0},  //  81: 0000 0000 0010 1x
+	{100 << 1,        0}, {101 << 1,        0},  //  82: 0000 0000 0011 0x
+	{102 << 1,        0}, {103 << 1,        0},  //  83: 0000 0000 0011 1x
+	{       0,   0x001f}, {       0,   0x001e},  //  84: 0000 0000 0100 0x
+	{       0,   0x001d}, {       0,   0x001c},  //  85: 0000 0000 0100 1x
+	{       0,   0x001b}, {       0,   0x001a},  //  86: 0000 0000 0101 0x
+	{       0,   0x0019}, {       0,   0x0018},  //  87: 0000 0000 0101 1x
+	{       0,   0x0017}, {       0,   0x0016},  //  88: 0000 0000 0110 0x
+	{       0,   0x0015}, {       0,   0x0014},  //  89: 0000 0000 0110 1x
+	{       0,   0x0013}, {       0,   0x0012},  //  90: 0000 0000 0111 0x
+	{       0,   0x0011}, {       0,   0x0010},  //  91: 0000 0000 0111 1x
+	{104 << 1,        0}, {105 << 1,        0},  //  92: 0000 0000 0001 00x
+	{106 << 1,        0}, {107 << 1,        0},  //  93: 0000 0000 0001 01x
+	{108 << 1,        0}, {109 << 1,        0},  //  94: 0000 0000 0001 10x
+	{110 << 1,        0}, {111 << 1,        0},  //  95: 0000 0000 0001 11x
+	{       0,   0x0028}, {       0,   0x0027},  //  96: 0000 0000 0010 00x
+	{       0,   0x0026}, {       0,   0x0025},  //  97: 0000 0000 0010 01x
+	{       0,   0x0024}, {       0,   0x0023},  //  98: 0000 0000 0010 10x
+	{       0,   0x0022}, {       0,   0x0021},  //  99: 0000 0000 0010 11x
+	{       0,   0x0020}, {       0,   0x010e},  // 100: 0000 0000 0011 00x
+	{       0,   0x010d}, {       0,   0x010c},  // 101: 0000 0000 0011 01x
+	{       0,   0x010b}, {       0,   0x010a},  // 102: 0000 0000 0011 10x
+	{       0,   0x0109}, {       0,   0x0108},  // 103: 0000 0000 0011 11x
+	{       0,   0x0112}, {       0,   0x0111},  // 104: 0000 0000 0001 000x
+	{       0,   0x0110}, {       0,   0x010f},  // 105: 0000 0000 0001 001x
+	{       0,   0x0603}, {       0,   0x1002},  // 106: 0000 0000 0001 010x
+	{       0,   0x0f02}, {       0,   0x0e02},  // 107: 0000 0000 0001 011x
+	{       0,   0x0d02}, {       0,   0x0c02},  // 108: 0000 0000 0001 100x
+	{       0,   0x0b02}, {       0,   0x1f01},  // 109: 0000 0000 0001 101x
+	{       0,   0x1e01}, {       0,   0x1d01},  // 110: 0000 0000 0001 110x
+	{       0,   0x1c01}, {       0,   0x1b01},  // 111: 0000 0000 0001 111x
+};
+
+typedef struct {
+	int full_px;
+	int is_set;
+	int r_size;
+	int h;
+	int v;
+} plm_video_motion_t;
+
+struct plm_video_t {
+	double framerate;
+	double time;
+	int frames_decoded;
+	int width;
+	int height;
+	int mb_width;
+	int mb_height;
+	int mb_size;
+
+	int luma_width;
+	int luma_height;
+
+	int chroma_width;
+	int chroma_height;
+
+	int start_code;
+	int picture_type;
+
+	plm_video_motion_t motion_forward;
+	plm_video_motion_t motion_backward;
+
+	int has_sequence_header;
+
+	int quantizer_scale;
+	int slice_begin;
+	int macroblock_address;
+
+	int mb_row;
+	int mb_col;
+
+	int macroblock_type;
+	int macroblock_intra;
+
+	int dc_predictor[3];
+
+	plm_buffer_t *buffer;
+	int destroy_buffer_when_done;
+
+	plm_frame_t frame_current;
+	plm_frame_t frame_forward;
+	plm_frame_t frame_backward;
+
+	uint8_t *frames_data;
+
+	int block_data[64];
+	uint8_t intra_quant_matrix[64];
+	uint8_t non_intra_quant_matrix[64];
+
+	int has_reference_frame;
+	int assume_no_b_frames;
+};
+
+static inline uint8_t plm_clamp(int n) {
+	if (n > 255) {
+		n = 255;
+	}
+	else if (n < 0) {
+		n = 0;
+	}
+	return n;
+}
+
+int plm_video_decode_sequence_header(plm_video_t *self);
+void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base);
+void plm_video_decode_picture(plm_video_t *self);
+void plm_video_decode_slice(plm_video_t *self, int slice);
+void plm_video_decode_macroblock(plm_video_t *self);
+void plm_video_decode_motion_vectors(plm_video_t *self);
+int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion);
+void plm_video_predict_macroblock(plm_video_t *self);
+void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v);
+void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v);
+void plm_video_process_macroblock(plm_video_t *self, uint8_t *s, uint8_t *d, int mh, int mb, int bs, int interp);
+void plm_video_decode_block(plm_video_t *self, int block);
+void plm_video_idct(int *block);
+
+plm_video_t * plm_video_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) {
+	plm_video_t *self = (plm_video_t *)PLM_MALLOC(sizeof(plm_video_t));
+	memset(self, 0, sizeof(plm_video_t));
+	
+	self->buffer = buffer;
+	self->destroy_buffer_when_done = destroy_when_done;
+
+	// Attempt to decode the sequence header
+	self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE);
+	if (self->start_code != -1) {
+		plm_video_decode_sequence_header(self);
+	}
+	return self;
+}
+
+void plm_video_destroy(plm_video_t *self) {
+	if (self->destroy_buffer_when_done) {
+		plm_buffer_destroy(self->buffer);
+	}
+
+	if (self->has_sequence_header) {
+		PLM_FREE(self->frames_data);
+	}
+
+	PLM_FREE(self);
+}
+
+double plm_video_get_framerate(plm_video_t *self) {
+	return plm_video_has_header(self)
+		? self->framerate
+		: 0;
+}
+
+int plm_video_get_width(plm_video_t *self) {
+	return plm_video_has_header(self)
+		? self->width
+		: 0;
+}
+
+int plm_video_get_height(plm_video_t *self) {
+	return plm_video_has_header(self)
+		? self->height
+		: 0;
+}
+
+void plm_video_set_no_delay(plm_video_t *self, int no_delay) {
+	self->assume_no_b_frames = no_delay;
+}
+
+double plm_video_get_time(plm_video_t *self) {
+	return self->time;
+}
+
+void plm_video_set_time(plm_video_t *self, double time) {
+	self->frames_decoded = self->framerate * time;
+	self->time = time;
+}
+
+void plm_video_rewind(plm_video_t *self) {
+	plm_buffer_rewind(self->buffer);
+	self->time = 0;
+	self->frames_decoded = 0;
+	self->has_reference_frame = FALSE;
+	self->start_code = -1;
+}
+
+int plm_video_has_ended(plm_video_t *self) {
+	return plm_buffer_has_ended(self->buffer);
+}
+
+plm_frame_t *plm_video_decode(plm_video_t *self) {
+	if (!plm_video_has_header(self)) {
+		return NULL;
+	}
+	
+	plm_frame_t *frame = NULL;
+	do {
+		if (self->start_code != PLM_START_PICTURE) {
+			self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_PICTURE);
+			
+			if (self->start_code == -1) {
+				// If we reached the end of the file and the previously decoded
+				// frame was a reference frame, we still have to return it.
+				if (
+					self->has_reference_frame &&
+					!self->assume_no_b_frames &&
+					plm_buffer_has_ended(self->buffer) && (
+						self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+						self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE
+					)
+				) {
+					self->has_reference_frame = FALSE;
+					frame = &self->frame_backward;
+					break;
+				}
+
+				return NULL;
+			}
+		}
+
+		// Make sure we have a full picture in the buffer before attempting to
+		// decode it. Sadly, this can only be done by seeking for the start code
+		// of the next picture. Also, if we didn't find the start code for the
+		// next picture, but the source has ended, we assume that this last
+		// picture is in the buffer.
+		if (
+			plm_buffer_has_start_code(self->buffer, PLM_START_PICTURE) == -1 &&
+			!plm_buffer_has_ended(self->buffer)
+		) {
+			return NULL;
+		}
+		plm_buffer_discard_read_bytes(self->buffer);
+		
+		plm_video_decode_picture(self);
+
+		if (self->assume_no_b_frames) {
+			frame = &self->frame_backward;
+		}
+		else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+			frame = &self->frame_current;
+		}
+		else if (self->has_reference_frame) {
+			frame = &self->frame_forward;
+		}
+		else {
+			self->has_reference_frame = TRUE;
+		}
+	} while (!frame);
+	
+	frame->time = self->time;
+	self->frames_decoded++;
+	self->time = (double)self->frames_decoded / self->framerate;
+	
+	return frame;
+}
+
+int plm_video_has_header(plm_video_t *self) {
+	if (self->has_sequence_header) {
+		return TRUE;
+	}
+
+	if (self->start_code != PLM_START_SEQUENCE) {
+		self->start_code = plm_buffer_find_start_code(self->buffer, PLM_START_SEQUENCE);
+	}
+	if (self->start_code == -1) {
+		return FALSE;
+	}
+	
+	if (!plm_video_decode_sequence_header(self)) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+int plm_video_decode_sequence_header(plm_video_t *self) {
+	int max_header_size = 64 + 2 * 64 * 8; // 64 bit header + 2x 64 byte matrix
+	if (!plm_buffer_has(self->buffer, max_header_size)) {
+		return FALSE;
+	}
+
+	self->width = plm_buffer_read(self->buffer, 12);
+	self->height = plm_buffer_read(self->buffer, 12);
+
+	if (self->width <= 0 || self->height <= 0) {
+		return FALSE;
+	}
+
+	// Skip pixel aspect ratio
+	plm_buffer_skip(self->buffer, 4);
+
+	self->framerate = PLM_VIDEO_PICTURE_RATE[plm_buffer_read(self->buffer, 4)];
+
+	// Skip bit_rate, marker, buffer_size and constrained bit
+	plm_buffer_skip(self->buffer, 18 + 1 + 10 + 1);
+
+	// Load custom intra quant matrix?
+	if (plm_buffer_read(self->buffer, 1)) { 
+		for (int i = 0; i < 64; i++) {
+			int idx = PLM_VIDEO_ZIG_ZAG[i];
+			self->intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8);
+		}
+	}
+	else {
+		memcpy(self->intra_quant_matrix, PLM_VIDEO_INTRA_QUANT_MATRIX, 64);
+	}
+
+	// Load custom non intra quant matrix?
+	if (plm_buffer_read(self->buffer, 1)) { 
+		for (int i = 0; i < 64; i++) {
+			int idx = PLM_VIDEO_ZIG_ZAG[i];
+			self->non_intra_quant_matrix[idx] = plm_buffer_read(self->buffer, 8);
+		}
+	}
+	else {
+		memcpy(self->non_intra_quant_matrix, PLM_VIDEO_NON_INTRA_QUANT_MATRIX, 64);
+	}
+
+	self->mb_width = (self->width + 15) >> 4;
+	self->mb_height = (self->height + 15) >> 4;
+	self->mb_size = self->mb_width * self->mb_height;
+
+	self->luma_width = self->mb_width << 4;
+	self->luma_height = self->mb_height << 4;
+
+	self->chroma_width = self->mb_width << 3;
+	self->chroma_height = self->mb_height << 3;
+
+
+	// Allocate one big chunk of data for all 3 frames = 9 planes
+	size_t luma_plane_size = self->luma_width * self->luma_height;
+	size_t chroma_plane_size = self->chroma_width * self->chroma_height;
+	size_t frame_data_size = (luma_plane_size + 2 * chroma_plane_size);
+
+	self->frames_data = (uint8_t*)PLM_MALLOC(frame_data_size * 3);
+	plm_video_init_frame(self, &self->frame_current, self->frames_data + frame_data_size * 0);
+	plm_video_init_frame(self, &self->frame_forward, self->frames_data + frame_data_size * 1);
+	plm_video_init_frame(self, &self->frame_backward, self->frames_data + frame_data_size * 2);
+
+	self->has_sequence_header = TRUE;
+	return TRUE;
+}
+
+void plm_video_init_frame(plm_video_t *self, plm_frame_t *frame, uint8_t *base) {
+	size_t luma_plane_size = self->luma_width * self->luma_height;
+	size_t chroma_plane_size = self->chroma_width * self->chroma_height;
+
+	frame->width = self->width;
+	frame->height = self->height;
+	frame->y.width = self->luma_width;
+	frame->y.height = self->luma_height;
+	frame->y.data = base;
+
+	frame->cr.width = self->chroma_width;
+	frame->cr.height = self->chroma_height;
+	frame->cr.data = base + luma_plane_size;
+
+	frame->cb.width = self->chroma_width;
+	frame->cb.height = self->chroma_height;
+	frame->cb.data = base + luma_plane_size + chroma_plane_size;
+}
+
+void plm_video_decode_picture(plm_video_t *self) {
+	plm_buffer_skip(self->buffer, 10); // skip temporalReference
+	self->picture_type = plm_buffer_read(self->buffer, 3);
+	plm_buffer_skip(self->buffer, 16); // skip vbv_delay
+
+	// D frames or unknown coding type
+	if (self->picture_type <= 0 || self->picture_type > PLM_VIDEO_PICTURE_TYPE_B) {
+		return;
+	}
+
+	// Forward full_px, f_code
+	if (
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE ||
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_B
+	) {
+		self->motion_forward.full_px = plm_buffer_read(self->buffer, 1);
+		int f_code = plm_buffer_read(self->buffer, 3);
+		if (f_code == 0) {
+			// Ignore picture with zero f_code
+			return;
+		}
+		self->motion_forward.r_size = f_code - 1;
+	}
+
+	// Backward full_px, f_code
+	if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+		self->motion_backward.full_px = plm_buffer_read(self->buffer, 1);
+		int f_code = plm_buffer_read(self->buffer, 3);
+		if (f_code == 0) {
+			// Ignore picture with zero f_code
+			return;
+		}
+		self->motion_backward.r_size = f_code - 1;
+	}
+
+	plm_frame_t frame_temp = self->frame_forward;
+	if (
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE
+	) {
+		self->frame_forward = self->frame_backward;
+	}
+
+
+	// Find first slice start code; skip extension and user data
+	do {
+		self->start_code = plm_buffer_next_start_code(self->buffer);
+	} while (
+		self->start_code == PLM_START_EXTENSION || 
+		self->start_code == PLM_START_USER_DATA
+	);
+
+	// Decode all slices
+	while (PLM_START_IS_SLICE(self->start_code)) {
+		plm_video_decode_slice(self, self->start_code & 0x000000FF);
+		if (self->macroblock_address >= self->mb_size - 2) {
+			break;
+		}
+		self->start_code = plm_buffer_next_start_code(self->buffer);
+	}
+
+	// If this is a reference picture rotate the prediction pointers
+	if (
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_INTRA ||
+		self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE
+	) {
+		self->frame_backward = self->frame_current;
+		self->frame_current = frame_temp;
+	}
+}
+
+void plm_video_decode_slice(plm_video_t *self, int slice) {
+	self->slice_begin = TRUE;
+	self->macroblock_address = (slice - 1) * self->mb_width - 1;
+
+	// Reset motion vectors and DC predictors
+	self->motion_backward.h = self->motion_forward.h = 0;
+	self->motion_backward.v = self->motion_forward.v = 0;
+	self->dc_predictor[0] = 128;
+	self->dc_predictor[1] = 128;
+	self->dc_predictor[2] = 128;
+
+	self->quantizer_scale = plm_buffer_read(self->buffer, 5);
+
+	// Skip extra
+	while (plm_buffer_read(self->buffer, 1)) {
+		plm_buffer_skip(self->buffer, 8);
+	}
+
+	do {
+		plm_video_decode_macroblock(self);
+	} while (
+		self->macroblock_address < self->mb_size - 1 &&
+		plm_buffer_peek_non_zero(self->buffer, 23)
+	);
+}
+
+void plm_video_decode_macroblock(plm_video_t *self) {
+	// Decode increment
+	int increment = 0;
+	int t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+
+	while (t == 34) {
+		// macroblock_stuffing
+		t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+	}
+	while (t == 35) {
+		// macroblock_escape
+		increment += 33;
+		t = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MACROBLOCK_ADDRESS_INCREMENT);
+	}
+	increment += t;
+
+	// Process any skipped macroblocks
+	if (self->slice_begin) {
+		// The first increment of each slice is relative to beginning of the
+		// previous row, not the previous macroblock
+		self->slice_begin = FALSE;
+		self->macroblock_address += increment;
+	}
+	else {
+		if (self->macroblock_address + increment >= self->mb_size) {
+			return; // invalid
+		}
+		if (increment > 1) {
+			// Skipped macroblocks reset DC predictors
+			self->dc_predictor[0] = 128;
+			self->dc_predictor[1] = 128;
+			self->dc_predictor[2] = 128;
+
+			// Skipped macroblocks in P-pictures reset motion vectors
+			if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+				self->motion_forward.h = 0;
+				self->motion_forward.v = 0;
+			}
+		}
+
+		// Predict skipped macroblocks
+		while (increment > 1) {
+			self->macroblock_address++;
+			self->mb_row = self->macroblock_address / self->mb_width;
+			self->mb_col = self->macroblock_address % self->mb_width;
+
+			plm_video_predict_macroblock(self);
+			increment--;
+		}
+		self->macroblock_address++;
+	}
+
+	self->mb_row = self->macroblock_address / self->mb_width;
+	self->mb_col = self->macroblock_address % self->mb_width;
+
+	if (self->mb_col >= self->mb_width || self->mb_row >= self->mb_height) {
+		return; // corrupt stream;
+	}
+
+	// Process the current macroblock
+	const plm_vlc_t *table = PLM_VIDEO_MACROBLOCK_TYPE[self->picture_type];
+	self->macroblock_type = plm_buffer_read_vlc(self->buffer, table);
+
+	self->macroblock_intra = (self->macroblock_type & 0x01);
+	self->motion_forward.is_set = (self->macroblock_type & 0x08);
+	self->motion_backward.is_set = (self->macroblock_type & 0x04);
+
+	// Quantizer scale
+	if ((self->macroblock_type & 0x10) != 0) {
+		self->quantizer_scale = plm_buffer_read(self->buffer, 5);
+	}
+
+	if (self->macroblock_intra) {
+		// Intra-coded macroblocks reset motion vectors
+		self->motion_backward.h = self->motion_forward.h = 0;
+		self->motion_backward.v = self->motion_forward.v = 0;
+	}
+	else {
+		// Non-intra macroblocks reset DC predictors
+		self->dc_predictor[0] = 128;
+		self->dc_predictor[1] = 128;
+		self->dc_predictor[2] = 128;
+
+		plm_video_decode_motion_vectors(self);
+		plm_video_predict_macroblock(self);
+	}
+
+	// Decode blocks
+	int cbp = ((self->macroblock_type & 0x02) != 0)
+		? plm_buffer_read_vlc(self->buffer, PLM_VIDEO_CODE_BLOCK_PATTERN)
+		: (self->macroblock_intra ? 0x3f : 0);
+
+	for (int block = 0, mask = 0x20; block < 6; block++) {
+		if ((cbp & mask) != 0) {
+			plm_video_decode_block(self, block);
+		}
+		mask >>= 1;
+	}
+}
+
+void plm_video_decode_motion_vectors(plm_video_t *self) {
+
+	// Forward
+	if (self->motion_forward.is_set) {
+		int r_size = self->motion_forward.r_size;
+		self->motion_forward.h = plm_video_decode_motion_vector(self, r_size, self->motion_forward.h);
+		self->motion_forward.v = plm_video_decode_motion_vector(self, r_size, self->motion_forward.v);
+	}
+	else if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_PREDICTIVE) {
+		// No motion information in P-picture, reset vectors
+		self->motion_forward.h = 0;
+		self->motion_forward.v = 0;
+	}
+
+	if (self->motion_backward.is_set) {
+		int r_size = self->motion_backward.r_size;
+		self->motion_backward.h = plm_video_decode_motion_vector(self, r_size, self->motion_backward.h);
+		self->motion_backward.v = plm_video_decode_motion_vector(self, r_size, self->motion_backward.v);
+	}
+}
+
+int plm_video_decode_motion_vector(plm_video_t *self, int r_size, int motion) {
+	int fscale = 1 << r_size;
+	int m_code = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_MOTION);
+	int r = 0;
+	int d;
+
+	if ((m_code != 0) && (fscale != 1)) {
+		r = plm_buffer_read(self->buffer, r_size);
+		d = ((abs(m_code) - 1) << r_size) + r + 1;
+		if (m_code < 0) {
+			d = -d;
+		}
+	}
+	else {
+		d = m_code;
+	}
+
+	motion += d;
+	if (motion > (fscale << 4) - 1) {
+		motion -= fscale << 5;
+	}
+	else if (motion < ((-fscale) << 4)) {
+		motion += fscale << 5;
+	}
+
+	return motion;
+}
+
+void plm_video_predict_macroblock(plm_video_t *self) {
+	int fw_h = self->motion_forward.h;
+	int fw_v = self->motion_forward.v;
+
+	if (self->motion_forward.full_px) {
+		fw_h <<= 1;
+		fw_v <<= 1;
+	}
+
+	if (self->picture_type == PLM_VIDEO_PICTURE_TYPE_B) {
+		int bw_h = self->motion_backward.h;
+		int bw_v = self->motion_backward.v;
+
+		if (self->motion_backward.full_px) {
+			bw_h <<= 1;
+			bw_v <<= 1;
+		}
+
+		if (self->motion_forward.is_set) {
+			plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v);
+			if (self->motion_backward.is_set) {
+				plm_video_interpolate_macroblock(self, &self->frame_backward, bw_h, bw_v);
+			}
+		}
+		else {
+			plm_video_copy_macroblock(self, &self->frame_backward, bw_h, bw_v);
+		}
+	}
+	else {
+		plm_video_copy_macroblock(self, &self->frame_forward, fw_h, fw_v);
+	}
+}
+
+void plm_video_copy_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) {
+	plm_frame_t *d = &self->frame_current;
+	plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, FALSE);
+	plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, FALSE);
+	plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, FALSE);
+}
+
+void plm_video_interpolate_macroblock(plm_video_t *self, plm_frame_t *s, int motion_h, int motion_v) {
+	plm_frame_t *d = &self->frame_current;
+	plm_video_process_macroblock(self, s->y.data, d->y.data, motion_h, motion_v, 16, TRUE);
+	plm_video_process_macroblock(self, s->cr.data, d->cr.data, motion_h / 2, motion_v / 2, 8, TRUE);
+	plm_video_process_macroblock(self, s->cb.data, d->cb.data, motion_h / 2, motion_v / 2, 8, TRUE);
+}
+
+#define PLM_BLOCK_SET(DEST, DEST_INDEX, DEST_WIDTH, SOURCE_INDEX, SOURCE_WIDTH, BLOCK_SIZE, OP) do { \
+	int dest_scan = DEST_WIDTH - BLOCK_SIZE; \
+	int source_scan = SOURCE_WIDTH - BLOCK_SIZE; \
+	for (int y = 0; y < BLOCK_SIZE; y++) { \
+		for (int x = 0; x < BLOCK_SIZE; x++) { \
+			DEST[DEST_INDEX] = OP; \
+			SOURCE_INDEX++; DEST_INDEX++; \
+		} \
+		SOURCE_INDEX += source_scan; \
+		DEST_INDEX += dest_scan; \
+	}} while(FALSE)
+
+void plm_video_process_macroblock(
+	plm_video_t *self, uint8_t *s, uint8_t *d,
+	int motion_h, int motion_v, int block_size, int interpolate
+) {
+	int dw = self->mb_width * block_size;
+
+	int hp = motion_h >> 1;
+	int vp = motion_v >> 1;
+	int odd_h = (motion_h & 1) == 1;
+	int odd_v = (motion_v & 1) == 1;
+
+	unsigned int si = ((self->mb_row * block_size) + vp) * dw + (self->mb_col * block_size) + hp;
+	unsigned int di = (self->mb_row * dw + self->mb_col) * block_size;
+	
+	unsigned int max_address = (dw * (self->mb_height * block_size - block_size + 1) - block_size);
+	if (si > max_address || di > max_address) {
+		return; // corrupt video
+	}
+
+	#define PLM_MB_CASE(INTERPOLATE, ODD_H, ODD_V, OP) \
+		case ((INTERPOLATE << 2) | (ODD_H << 1) | (ODD_V)): \
+			PLM_BLOCK_SET(d, di, dw, si, dw, block_size, OP); \
+			break
+
+	switch ((interpolate << 2) | (odd_h << 1) | (odd_v)) {
+		PLM_MB_CASE(0, 0, 0, (s[si]));
+		PLM_MB_CASE(0, 0, 1, (s[si] + s[si + dw] + 1) >> 1);
+		PLM_MB_CASE(0, 1, 0, (s[si] + s[si + 1] + 1) >> 1);
+		PLM_MB_CASE(0, 1, 1, (s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2);
+
+		PLM_MB_CASE(1, 0, 0, (d[di] + (s[si]) + 1) >> 1);
+		PLM_MB_CASE(1, 0, 1, (d[di] + ((s[si] + s[si + dw] + 1) >> 1) + 1) >> 1);
+		PLM_MB_CASE(1, 1, 0, (d[di] + ((s[si] + s[si + 1] + 1) >> 1) + 1) >> 1);
+		PLM_MB_CASE(1, 1, 1, (d[di] + ((s[si] + s[si + 1] + s[si + dw] + s[si + dw + 1] + 2) >> 2) + 1) >> 1);
+	}
+
+	#undef PLM_MB_CASE
+}
+
+void plm_video_decode_block(plm_video_t *self, int block) {
+
+	int n = 0;
+	uint8_t *quant_matrix;
+
+	// Decode DC coefficient of intra-coded blocks
+	if (self->macroblock_intra) {
+		int predictor;
+		int dct_size;
+
+		// DC prediction
+		int plane_index = block > 3 ? block - 3 : 0;
+		predictor = self->dc_predictor[plane_index];
+		dct_size = plm_buffer_read_vlc(self->buffer, PLM_VIDEO_DCT_SIZE[plane_index]);
+
+		// Read DC coeff
+		if (dct_size > 0) {
+			int differential = plm_buffer_read(self->buffer, dct_size);
+			if ((differential & (1 << (dct_size - 1))) != 0) {
+				self->block_data[0] = predictor + differential;
+			}
+			else {
+				self->block_data[0] = predictor + (-(1 << dct_size) | (differential + 1));
+			}
+		}
+		else {
+			self->block_data[0] = predictor;
+		}
+
+		// Save predictor value
+		self->dc_predictor[plane_index] = self->block_data[0];
+
+		// Dequantize + premultiply
+		self->block_data[0] <<= (3 + 5);
+
+		quant_matrix = self->intra_quant_matrix;
+		n = 1;
+	}
+	else {
+		quant_matrix = self->non_intra_quant_matrix;
+	}
+
+	// Decode AC coefficients (+DC for non-intra)
+	int level = 0;
+	while (TRUE) {
+		int run = 0;
+		uint16_t coeff = plm_buffer_read_vlc_uint(self->buffer, PLM_VIDEO_DCT_COEFF);
+
+		if ((coeff == 0x0001) && (n > 0) && (plm_buffer_read(self->buffer, 1) == 0)) {
+			// end_of_block
+			break;
+		}
+		if (coeff == 0xffff) {
+			// escape
+			run = plm_buffer_read(self->buffer, 6);
+			level = plm_buffer_read(self->buffer, 8);
+			if (level == 0) {
+				level = plm_buffer_read(self->buffer, 8);
+			}
+			else if (level == 128) {
+				level = plm_buffer_read(self->buffer, 8) - 256;
+			}
+			else if (level > 128) {
+				level = level - 256;
+			}
+		}
+		else {
+			run = coeff >> 8;
+			level = coeff & 0xff;
+			if (plm_buffer_read(self->buffer, 1)) {
+				level = -level;
+			}
+		}
+
+		n += run;
+		if (n < 0 || n >= 64) {
+			return; // invalid
+		}
+
+		int de_zig_zagged = PLM_VIDEO_ZIG_ZAG[n];
+		n++;
+
+		// Dequantize, oddify, clip
+		level <<= 1;
+		if (!self->macroblock_intra) {
+			level += (level < 0 ? -1 : 1);
+		}
+		level = (level * self->quantizer_scale * quant_matrix[de_zig_zagged]) >> 4;
+		if ((level & 1) == 0) {
+			level -= level > 0 ? 1 : -1;
+		}
+		if (level > 2047) {
+			level = 2047;
+		}
+		else if (level < -2048) {
+			level = -2048;
+		}
+
+		// Save premultiplied coefficient
+		self->block_data[de_zig_zagged] = level * PLM_VIDEO_PREMULTIPLIER_MATRIX[de_zig_zagged];
+	}
+
+	// Move block to its place
+	uint8_t *d;
+	int dw;
+	int di;
+
+	if (block < 4) {
+		d = self->frame_current.y.data;
+		dw = self->luma_width;
+		di = (self->mb_row * self->luma_width + self->mb_col) << 4;
+		if ((block & 1) != 0) {
+			di += 8;
+		}
+		if ((block & 2) != 0) {
+			di += self->luma_width << 3;
+		}
+	}
+	else {
+		d = (block == 4) ? self->frame_current.cb.data : self->frame_current.cr.data;
+		dw = self->chroma_width;
+		di = ((self->mb_row * self->luma_width) << 2) + (self->mb_col << 3);
+	}
+
+	int *s = self->block_data;
+	int si = 0;
+	if (self->macroblock_intra) {
+		// Overwrite (no prediction)
+		if (n == 1) {
+			int clamped = plm_clamp((s[0] + 128) >> 8);
+			PLM_BLOCK_SET(d, di, dw, si, 8, 8, clamped);
+			s[0] = 0;
+		}
+		else {
+			plm_video_idct(s);
+			PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(s[si]));
+			memset(self->block_data, 0, sizeof(self->block_data));
+		}
+	}
+	else {
+		// Add data to the predicted macroblock
+		if (n == 1) {
+			int value = (s[0] + 128) >> 8;
+			PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + value));
+			s[0] = 0;
+		}
+		else {
+			plm_video_idct(s);
+			PLM_BLOCK_SET(d, di, dw, si, 8, 8, plm_clamp(d[di] + s[si]));
+			memset(self->block_data, 0, sizeof(self->block_data));
+		}
+	}
+}
+
+void plm_video_idct(int *block) {
+	int
+		b1, b3, b4, b6, b7, tmp1, tmp2, m0,
+		x0, x1, x2, x3, x4, y3, y4, y5, y6, y7;
+
+	// Transform columns
+	for (int i = 0; i < 8; ++i) {
+		b1 = block[4 * 8 + i];
+		b3 = block[2 * 8 + i] + block[6 * 8 + i];
+		b4 = block[5 * 8 + i] - block[3 * 8 + i];
+		tmp1 = block[1 * 8 + i] + block[7 * 8 + i];
+		tmp2 = block[3 * 8 + i] + block[5 * 8 + i];
+		b6 = block[1 * 8 + i] - block[7 * 8 + i];
+		b7 = tmp1 + tmp2;
+		m0 = block[0 * 8 + i];
+		x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7;
+		x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8);
+		x1 = m0 - b1;
+		x2 = (((block[2 * 8 + i] - block[6 * 8 + i]) * 362 + 128) >> 8) - b3;
+		x3 = m0 + b1;
+		y3 = x1 + x2;
+		y4 = x3 + b3;
+		y5 = x1 - x2;
+		y6 = x3 - b3;
+		y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8);
+		block[0 * 8 + i] = b7 + y4;
+		block[1 * 8 + i] = x4 + y3;
+		block[2 * 8 + i] = y5 - x0;
+		block[3 * 8 + i] = y6 - y7;
+		block[4 * 8 + i] = y6 + y7;
+		block[5 * 8 + i] = x0 + y5;
+		block[6 * 8 + i] = y3 - x4;
+		block[7 * 8 + i] = y4 - b7;
+	}
+
+	// Transform rows
+	for (int i = 0; i < 64; i += 8) {
+		b1 = block[4 + i];
+		b3 = block[2 + i] + block[6 + i];
+		b4 = block[5 + i] - block[3 + i];
+		tmp1 = block[1 + i] + block[7 + i];
+		tmp2 = block[3 + i] + block[5 + i];
+		b6 = block[1 + i] - block[7 + i];
+		b7 = tmp1 + tmp2;
+		m0 = block[0 + i];
+		x4 = ((b6 * 473 - b4 * 196 + 128) >> 8) - b7;
+		x0 = x4 - (((tmp1 - tmp2) * 362 + 128) >> 8);
+		x1 = m0 - b1;
+		x2 = (((block[2 + i] - block[6 + i]) * 362 + 128) >> 8) - b3;
+		x3 = m0 + b1;
+		y3 = x1 + x2;
+		y4 = x3 + b3;
+		y5 = x1 - x2;
+		y6 = x3 - b3;
+		y7 = -x0 - ((b4 * 473 + b6 * 196 + 128) >> 8);
+		block[0 + i] = (b7 + y4 + 128) >> 8;
+		block[1 + i] = (x4 + y3 + 128) >> 8;
+		block[2 + i] = (y5 - x0 + 128) >> 8;
+		block[3 + i] = (y6 - y7 + 128) >> 8;
+		block[4 + i] = (y6 + y7 + 128) >> 8;
+		block[5 + i] = (x0 + y5 + 128) >> 8;
+		block[6 + i] = (y3 - x4 + 128) >> 8;
+		block[7 + i] = (y4 - b7 + 128) >> 8;
+	}
+}
+
+// YCbCr conversion following the BT.601 standard:
+// https://infogalactic.com/info/YCbCr#ITU-R_BT.601_conversion
+
+#define PLM_PUT_PIXEL(RI, GI, BI, Y_OFFSET, DEST_OFFSET) \
+	y = ((frame->y.data[y_index + Y_OFFSET]-16) * 76309) >> 16; \
+	dest[d_index + DEST_OFFSET + RI] = plm_clamp(y + r); \
+	dest[d_index + DEST_OFFSET + GI] = plm_clamp(y - g); \
+	dest[d_index + DEST_OFFSET + BI] = plm_clamp(y + b);
+
+#define PLM_DEFINE_FRAME_CONVERT_FUNCTION(NAME, BYTES_PER_PIXEL, RI, GI, BI) \
+	void NAME(plm_frame_t *frame, uint8_t *dest, int stride) { \
+		int cols = frame->width >> 1; \
+		int rows = frame->height >> 1; \
+		int yw = frame->y.width; \
+		int cw = frame->cb.width; \
+		for (int row = 0; row < rows; row++) { \
+			int c_index = row * cw; \
+			int y_index = row * 2 * yw; \
+			int d_index = row * 2 * stride; \
+			for (int col = 0; col < cols; col++) { \
+				int y; \
+				int cr = frame->cr.data[c_index] - 128; \
+				int cb = frame->cb.data[c_index] - 128; \
+				int r = (cr * 104597) >> 16; \
+				int g = (cb * 25674 + cr * 53278) >> 16; \
+				int b = (cb * 132201) >> 16; \
+				PLM_PUT_PIXEL(RI, GI, BI, 0,      0); \
+				PLM_PUT_PIXEL(RI, GI, BI, 1,      BYTES_PER_PIXEL); \
+				PLM_PUT_PIXEL(RI, GI, BI, yw,     stride); \
+				PLM_PUT_PIXEL(RI, GI, BI, yw + 1, stride + BYTES_PER_PIXEL); \
+				c_index += 1; \
+				y_index += 2; \
+				d_index += 2 * BYTES_PER_PIXEL; \
+			} \
+		} \
+	}
+
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgb,  3, 0, 1, 2)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgr,  3, 2, 1, 0)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_rgba, 4, 0, 1, 2)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_bgra, 4, 2, 1, 0)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_argb, 4, 1, 2, 3)
+PLM_DEFINE_FRAME_CONVERT_FUNCTION(plm_frame_to_abgr, 4, 3, 2, 1)
+
+
+#undef PLM_PUT_PIXEL
+#undef PLM_DEFINE_FRAME_CONVERT_FUNCTION
+
+
+
+// -----------------------------------------------------------------------------
+// plm_audio implementation
+
+// Based on kjmp2 by Martin J. Fiedler
+// http://keyj.emphy.de/kjmp2/
+
+static const int PLM_AUDIO_FRAME_SYNC = 0x7ff;
+
+static const int PLM_AUDIO_MPEG_2_5 = 0x0;
+static const int PLM_AUDIO_MPEG_2 = 0x2;
+static const int PLM_AUDIO_MPEG_1 = 0x3;
+
+static const int PLM_AUDIO_LAYER_III = 0x1;
+static const int PLM_AUDIO_LAYER_II = 0x2;
+static const int PLM_AUDIO_LAYER_I = 0x3;
+
+static const int PLM_AUDIO_MODE_STEREO = 0x0;
+static const int PLM_AUDIO_MODE_JOINT_STEREO = 0x1;
+static const int PLM_AUDIO_MODE_DUAL_CHANNEL = 0x2;
+static const int PLM_AUDIO_MODE_MONO = 0x3;
+
+static const unsigned short PLM_AUDIO_SAMPLE_RATE[] = {
+	44100, 48000, 32000, 0, // MPEG-1
+	22050, 24000, 16000, 0  // MPEG-2
+};
+
+static const short PLM_AUDIO_BIT_RATE[] = {
+	32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, // MPEG-1
+	 8, 16, 24, 32, 40, 48,  56,  64,  80,  96, 112, 128, 144, 160  // MPEG-2
+};
+
+static const int PLM_AUDIO_SCALEFACTOR_BASE[] = {
+	0x02000000, 0x01965FEA, 0x01428A30
+};
+
+static const float PLM_AUDIO_SYNTHESIS_WINDOW[] = {
+	     0.0,     -0.5,     -0.5,     -0.5,     -0.5,     -0.5,
+	    -0.5,     -1.0,     -1.0,     -1.0,     -1.0,     -1.5,
+	    -1.5,     -2.0,     -2.0,     -2.5,     -2.5,     -3.0,
+	    -3.5,     -3.5,     -4.0,     -4.5,     -5.0,     -5.5,
+	    -6.5,     -7.0,     -8.0,     -8.5,     -9.5,    -10.5,
+	   -12.0,    -13.0,    -14.5,    -15.5,    -17.5,    -19.0,
+	   -20.5,    -22.5,    -24.5,    -26.5,    -29.0,    -31.5,
+	   -34.0,    -36.5,    -39.5,    -42.5,    -45.5,    -48.5,
+	   -52.0,    -55.5,    -58.5,    -62.5,    -66.0,    -69.5,
+	   -73.5,    -77.0,    -80.5,    -84.5,    -88.0,    -91.5,
+	   -95.0,    -98.0,   -101.0,   -104.0,    106.5,    109.0,
+	   111.0,    112.5,    113.5,    114.0,    114.0,    113.5,
+	   112.0,    110.5,    107.5,    104.0,    100.0,     94.5,
+	    88.5,     81.5,     73.0,     63.5,     53.0,     41.5,
+	    28.5,     14.5,     -1.0,    -18.0,    -36.0,    -55.5,
+	   -76.5,    -98.5,   -122.0,   -147.0,   -173.5,   -200.5,
+	  -229.5,   -259.5,   -290.5,   -322.5,   -355.5,   -389.5,
+	  -424.0,   -459.5,   -495.5,   -532.0,   -568.5,   -605.0,
+	  -641.5,   -678.0,   -714.0,   -749.0,   -783.5,   -817.0,
+	  -849.0,   -879.5,   -908.5,   -935.0,   -959.5,   -981.0,
+	 -1000.5,  -1016.0,  -1028.5,  -1037.5,  -1042.5,  -1043.5,
+	 -1040.0,  -1031.5,   1018.5,   1000.0,    976.0,    946.5,
+	   911.0,    869.5,    822.0,    767.5,    707.0,    640.0,
+	   565.5,    485.0,    397.0,    302.5,    201.0,     92.5,
+	   -22.5,   -144.0,   -272.5,   -407.0,   -547.5,   -694.0,
+	  -846.0,  -1003.0,  -1165.0,  -1331.5,  -1502.0,  -1675.5,
+	 -1852.5,  -2031.5,  -2212.5,  -2394.0,  -2576.5,  -2758.5,
+	 -2939.5,  -3118.5,  -3294.5,  -3467.5,  -3635.5,  -3798.5,
+	 -3955.0,  -4104.5,  -4245.5,  -4377.5,  -4499.0,  -4609.5,
+	 -4708.0,  -4792.5,  -4863.5,  -4919.0,  -4958.0,  -4979.5,
+	 -4983.0,  -4967.5,  -4931.5,  -4875.0,  -4796.0,  -4694.5,
+	 -4569.5,  -4420.0,  -4246.0,  -4046.0,  -3820.0,  -3567.0,
+	  3287.0,   2979.5,   2644.0,   2280.5,   1888.0,   1467.5,
+	  1018.5,    541.0,     35.0,   -499.0,  -1061.0,  -1650.0,
+	 -2266.5,  -2909.0,  -3577.0,  -4270.0,  -4987.5,  -5727.5,
+	 -6490.0,  -7274.0,  -8077.5,  -8899.5,  -9739.0, -10594.5,
+	-11464.5, -12347.0, -13241.0, -14144.5, -15056.0, -15973.5,
+	-16895.5, -17820.0, -18744.5, -19668.0, -20588.0, -21503.0,
+	-22410.5, -23308.5, -24195.0, -25068.5, -25926.5, -26767.0,
+	-27589.0, -28389.0, -29166.5, -29919.0, -30644.5, -31342.0,
+	-32009.5, -32645.0, -33247.0, -33814.5, -34346.0, -34839.5,
+	-35295.0, -35710.0, -36084.5, -36417.5, -36707.5, -36954.0,
+	-37156.5, -37315.0, -37428.0, -37496.0,  37519.0,  37496.0,
+	 37428.0,  37315.0,  37156.5,  36954.0,  36707.5,  36417.5,
+	 36084.5,  35710.0,  35295.0,  34839.5,  34346.0,  33814.5,
+	 33247.0,  32645.0,  32009.5,  31342.0,  30644.5,  29919.0,
+	 29166.5,  28389.0,  27589.0,  26767.0,  25926.5,  25068.5,
+	 24195.0,  23308.5,  22410.5,  21503.0,  20588.0,  19668.0,
+	 18744.5,  17820.0,  16895.5,  15973.5,  15056.0,  14144.5,
+	 13241.0,  12347.0,  11464.5,  10594.5,   9739.0,   8899.5,
+	  8077.5,   7274.0,   6490.0,   5727.5,   4987.5,   4270.0,
+	  3577.0,   2909.0,   2266.5,   1650.0,   1061.0,    499.0,
+	   -35.0,   -541.0,  -1018.5,  -1467.5,  -1888.0,  -2280.5,
+	 -2644.0,  -2979.5,   3287.0,   3567.0,   3820.0,   4046.0,
+	  4246.0,   4420.0,   4569.5,   4694.5,   4796.0,   4875.0,
+	  4931.5,   4967.5,   4983.0,   4979.5,   4958.0,   4919.0,
+	  4863.5,   4792.5,   4708.0,   4609.5,   4499.0,   4377.5,
+	  4245.5,   4104.5,   3955.0,   3798.5,   3635.5,   3467.5,
+	  3294.5,   3118.5,   2939.5,   2758.5,   2576.5,   2394.0,
+	  2212.5,   2031.5,   1852.5,   1675.5,   1502.0,   1331.5,
+	  1165.0,   1003.0,    846.0,    694.0,    547.5,    407.0,
+	   272.5,    144.0,     22.5,    -92.5,   -201.0,   -302.5,
+	  -397.0,   -485.0,   -565.5,   -640.0,   -707.0,   -767.5,
+	  -822.0,   -869.5,   -911.0,   -946.5,   -976.0,  -1000.0,
+	  1018.5,   1031.5,   1040.0,   1043.5,   1042.5,   1037.5,
+	  1028.5,   1016.0,   1000.5,    981.0,    959.5,    935.0,
+	   908.5,    879.5,    849.0,    817.0,    783.5,    749.0,
+	   714.0,    678.0,    641.5,    605.0,    568.5,    532.0,
+	   495.5,    459.5,    424.0,    389.5,    355.5,    322.5,
+	   290.5,    259.5,    229.5,    200.5,    173.5,    147.0,
+	   122.0,     98.5,     76.5,     55.5,     36.0,     18.0,
+	     1.0,    -14.5,    -28.5,    -41.5,    -53.0,    -63.5,
+	   -73.0,    -81.5,    -88.5,    -94.5,   -100.0,   -104.0,
+	  -107.5,   -110.5,   -112.0,   -113.5,   -114.0,   -114.0,
+	  -113.5,   -112.5,   -111.0,   -109.0,    106.5,    104.0,
+	   101.0,     98.0,     95.0,     91.5,     88.0,     84.5,
+	    80.5,     77.0,     73.5,     69.5,     66.0,     62.5,
+	    58.5,     55.5,     52.0,     48.5,     45.5,     42.5,
+	    39.5,     36.5,     34.0,     31.5,     29.0,     26.5,
+	    24.5,     22.5,     20.5,     19.0,     17.5,     15.5,
+	    14.5,     13.0,     12.0,     10.5,      9.5,      8.5,
+	     8.0,      7.0,      6.5,      5.5,      5.0,      4.5,
+	     4.0,      3.5,      3.5,      3.0,      2.5,      2.5,
+	     2.0,      2.0,      1.5,      1.5,      1.0,      1.0,
+	     1.0,      1.0,      0.5,      0.5,      0.5,      0.5,
+	     0.5,      0.5
+};
+
+// Quantizer lookup, step 1: bitrate classes
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_1[2][16] = {
+	// 32, 48, 56, 64, 80, 96,112,128,160,192,224,256,320,384 <- bitrate
+	{ 0,  0,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2 }, // mono
+	// 16, 24, 28, 32, 40, 48, 56, 64, 80, 96,112,128,160,192 <- bitrate / chan
+	{ 0,  0,  0,  0,  0,  0,  1,  1,  1,  2,  2,  2,  2,  2 } // stereo
+};
+
+// Quantizer lookup, step 2: bitrate class, sample rate -> B2 table idx, sblimit
+#define PLM_AUDIO_QUANT_TAB_A (27 | 64)   // Table 3-B.2a: high-rate, sblimit = 27
+#define PLM_AUDIO_QUANT_TAB_B (30 | 64)   // Table 3-B.2b: high-rate, sblimit = 30
+#define PLM_AUDIO_QUANT_TAB_C 8           // Table 3-B.2c:  low-rate, sblimit =  8
+#define PLM_AUDIO_QUANT_TAB_D 12          // Table 3-B.2d:  low-rate, sblimit = 12
+
+static const uint8_t QUANT_LUT_STEP_2[3][3] = {
+	//44.1 kHz,              48 kHz,                32 kHz
+	{ PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_C, PLM_AUDIO_QUANT_TAB_D }, // 32 - 48 kbit/sec/ch
+	{ PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_A }, // 56 - 80 kbit/sec/ch
+	{ PLM_AUDIO_QUANT_TAB_B, PLM_AUDIO_QUANT_TAB_A, PLM_AUDIO_QUANT_TAB_B }  // 96+	 kbit/sec/ch
+};
+
+// Quantizer lookup, step 3: B2 table, subband -> nbal, row index
+// (upper 4 bits: nbal, lower 4 bits: row index)
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_3[3][32] = {
+	// Low-rate table (3-B.2c and 3-B.2d)
+	{
+		0x44,0x44,
+		0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34,0x34
+	},
+	// High-rate table (3-B.2a and 3-B.2b)
+	{
+		0x43,0x43,0x43,
+		0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x42,
+		0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,
+		0x20,0x20,0x20,0x20,0x20,0x20,0x20
+	},
+	// MPEG-2 LSR table (B.2 in ISO 13818-3)
+	{
+		0x45,0x45,0x45,0x45,
+		0x34,0x34,0x34,0x34,0x34,0x34,0x34,
+		0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,
+		0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24,0x24
+	}
+};
+
+// Quantizer lookup, step 4: table row, allocation[] value -> quant table index
+static const uint8_t PLM_AUDIO_QUANT_LUT_STEP_4[6][16] = {
+	{ 0, 1, 2, 17 },
+	{ 0, 1, 2,  3, 4, 5, 6, 17 },
+	{ 0, 1, 2,  3, 4, 5, 6,  7,  8,  9, 10, 11, 12, 13, 14, 17 },
+	{ 0, 1, 3,  5, 6, 7, 8,  9, 10, 11, 12, 13, 14, 15, 16, 17 },
+	{ 0, 1, 2,  4, 5, 6, 7,  8,  9, 10, 11, 12, 13, 14, 15, 17 },
+	{ 0, 1, 2,  3, 4, 5, 6,  7,  8,  9, 10, 11, 12, 13, 14, 15 }
+};
+
+typedef struct plm_quantizer_spec_t {
+	unsigned short levels;
+	unsigned char group;
+	unsigned char bits;
+} plm_quantizer_spec_t;
+
+static const plm_quantizer_spec_t PLM_AUDIO_QUANT_TAB[] = {
+	{     3, 1,  5 },  //  1
+	{     5, 1,  7 },  //  2
+	{     7, 0,  3 },  //  3
+	{     9, 1, 10 },  //  4
+	{    15, 0,  4 },  //  5
+	{    31, 0,  5 },  //  6
+	{    63, 0,  6 },  //  7
+	{   127, 0,  7 },  //  8
+	{   255, 0,  8 },  //  9
+	{   511, 0,  9 },  // 10
+	{  1023, 0, 10 },  // 11
+	{  2047, 0, 11 },  // 12
+	{  4095, 0, 12 },  // 13
+	{  8191, 0, 13 },  // 14
+	{ 16383, 0, 14 },  // 15
+	{ 32767, 0, 15 },  // 16
+	{ 65535, 0, 16 }   // 17
+};
+
+struct plm_audio_t {
+	double time;
+	int samples_decoded;
+	int samplerate_index;
+	int bitrate_index;
+	int version;
+	int layer;
+	int mode;
+	int bound;
+	int v_pos;
+	int next_frame_data_size;
+	int has_header;
+	
+	plm_buffer_t *buffer;
+	int destroy_buffer_when_done;
+
+	const plm_quantizer_spec_t *allocation[2][32];
+	uint8_t scale_factor_info[2][32];
+	int scale_factor[2][32][3];
+	int sample[2][32][3];
+
+	plm_samples_t samples;
+	float D[1024];
+	float V[2][1024];
+	float U[32];
+};
+
+int plm_audio_find_frame_sync(plm_audio_t *self);
+int plm_audio_decode_header(plm_audio_t *self);
+void plm_audio_decode_frame(plm_audio_t *self);
+const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3);
+void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part); 
+void plm_audio_idct36(int s[32][3], int ss, float *d, int dp);
+
+plm_audio_t *plm_audio_create_with_buffer(plm_buffer_t *buffer, int destroy_when_done) {
+	plm_audio_t *self = (plm_audio_t *)PLM_MALLOC(sizeof(plm_audio_t));
+	memset(self, 0, sizeof(plm_audio_t));
+
+	self->samples.count = PLM_AUDIO_SAMPLES_PER_FRAME;
+	self->buffer = buffer;
+	self->destroy_buffer_when_done = destroy_when_done;
+	self->samplerate_index = 3; // Indicates 0
+
+	memcpy(self->D, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float));
+	memcpy(self->D + 512, PLM_AUDIO_SYNTHESIS_WINDOW, 512 * sizeof(float));
+
+	// Attempt to decode first header
+	self->next_frame_data_size = plm_audio_decode_header(self);
+
+	return self;
+}
+
+void plm_audio_destroy(plm_audio_t *self) {
+	if (self->destroy_buffer_when_done) {
+		plm_buffer_destroy(self->buffer);
+	}
+	PLM_FREE(self);
+}
+
+int plm_audio_has_header(plm_audio_t *self) {
+	if (self->has_header) {
+		return TRUE;
+	}
+	
+	self->next_frame_data_size = plm_audio_decode_header(self);
+	return self->has_header;
+}
+
+int plm_audio_get_samplerate(plm_audio_t *self) {
+	return plm_audio_has_header(self)
+		? PLM_AUDIO_SAMPLE_RATE[self->samplerate_index]
+		: 0;
+}
+
+double plm_audio_get_time(plm_audio_t *self) {
+	return self->time;
+}
+
+void plm_audio_set_time(plm_audio_t *self, double time) {
+	self->samples_decoded = time * 
+		(double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+	self->time = time;
+}
+
+void plm_audio_rewind(plm_audio_t *self) {
+	plm_buffer_rewind(self->buffer);
+	self->time = 0;
+	self->samples_decoded = 0;
+	self->next_frame_data_size = 0;
+}
+
+int plm_audio_has_ended(plm_audio_t *self) {
+	return plm_buffer_has_ended(self->buffer);
+}
+
+plm_samples_t *plm_audio_decode(plm_audio_t *self) {
+	// Do we have at least enough information to decode the frame header?
+	if (!self->next_frame_data_size) {
+		if (!plm_buffer_has(self->buffer, 48)) {
+			return NULL;
+		}
+		self->next_frame_data_size = plm_audio_decode_header(self);
+	}
+
+	if (
+		self->next_frame_data_size == 0 ||
+		!plm_buffer_has(self->buffer, self->next_frame_data_size << 3)
+	) {
+		return NULL;
+	}
+
+	plm_audio_decode_frame(self);
+	self->next_frame_data_size = 0;
+	
+	self->samples.time = self->time;
+
+	self->samples_decoded += PLM_AUDIO_SAMPLES_PER_FRAME;
+	self->time = (double)self->samples_decoded / 
+		(double)PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+	
+	return &self->samples;
+}
+
+int plm_audio_find_frame_sync(plm_audio_t *self) {
+	size_t i;
+	for (i = self->buffer->bit_index >> 3; i < self->buffer->length-1; i++) {
+		if (
+			self->buffer->bytes[i] == 0xFF &&
+			(self->buffer->bytes[i+1] & 0xFE) == 0xFC
+		) {
+			self->buffer->bit_index = ((i+1) << 3) + 3;
+			return TRUE;
+		}
+	}
+	self->buffer->bit_index = (i + 1) << 3;
+	return FALSE;
+}
+
+int plm_audio_decode_header(plm_audio_t *self) {
+	if (!plm_buffer_has(self->buffer, 48)) {
+		return 0;
+	}
+
+	plm_buffer_skip_bytes(self->buffer, 0x00);
+	int sync = plm_buffer_read(self->buffer, 11);
+
+
+	// Attempt to resync if no syncword was found. This sucks balls. The MP2 
+	// stream contains a syncword just before every frame (11 bits set to 1).
+	// However, this syncword is not guaranteed to not occur elsewhere in the
+	// stream. So, if we have to resync, we also have to check if the header 
+	// (samplerate, bitrate) differs from the one we had before. This all
+	// may still lead to garbage data being decoded :/
+
+	if (sync != PLM_AUDIO_FRAME_SYNC && !plm_audio_find_frame_sync(self)) {
+		return 0;
+	}
+
+	self->version = plm_buffer_read(self->buffer, 2);
+	self->layer = plm_buffer_read(self->buffer, 2);
+	int hasCRC = !plm_buffer_read(self->buffer, 1);
+
+	if (
+		self->version != PLM_AUDIO_MPEG_1 ||
+		self->layer != PLM_AUDIO_LAYER_II
+	) {
+		return 0;
+	}
+
+	int bitrate_index = plm_buffer_read(self->buffer, 4) - 1;
+	if (bitrate_index > 13) {
+		return 0;
+	}
+
+	int samplerate_index = plm_buffer_read(self->buffer, 2);
+	if (samplerate_index == 3) {
+		return 0;
+	}
+
+	int padding = plm_buffer_read(self->buffer, 1);
+	plm_buffer_skip(self->buffer, 1); // f_private
+	int mode = plm_buffer_read(self->buffer, 2);
+
+	// If we already have a header, make sure the samplerate, bitrate and mode
+	// are still the same, otherwise we might have missed sync.
+	if (
+		self->has_header && (
+			self->bitrate_index != bitrate_index ||
+			self->samplerate_index != samplerate_index ||
+			self->mode != mode
+		)
+	) {
+		return 0;
+	}
+
+	self->bitrate_index = bitrate_index;
+	self->samplerate_index = samplerate_index;
+	self->mode = mode;
+	self->has_header = TRUE;
+
+	// Parse the mode_extension, set up the stereo bound
+	if (mode == PLM_AUDIO_MODE_JOINT_STEREO) {
+		self->bound = (plm_buffer_read(self->buffer, 2) + 1) << 2;
+	}
+	else {
+		plm_buffer_skip(self->buffer, 2);
+		self->bound = (mode == PLM_AUDIO_MODE_MONO) ? 0 : 32;
+	}
+
+	// Discard the last 4 bits of the header and the CRC value, if present
+	plm_buffer_skip(self->buffer, 4); // copyright(1), original(1), emphasis(2)
+	if (hasCRC) {
+		plm_buffer_skip(self->buffer, 16);
+	}
+
+	// Compute frame size, check if we have enough data to decode the whole
+	// frame.
+	int bitrate = PLM_AUDIO_BIT_RATE[self->bitrate_index];
+	int samplerate = PLM_AUDIO_SAMPLE_RATE[self->samplerate_index];
+	int frame_size = (144000 * bitrate / samplerate) + padding;
+	return frame_size - (hasCRC ? 6 : 4);
+}
+
+void plm_audio_decode_frame(plm_audio_t *self) {
+	// Prepare the quantizer table lookups
+	int tab3 = 0;
+	int sblimit = 0;
+	
+	int tab1 = (self->mode == PLM_AUDIO_MODE_MONO) ? 0 : 1;
+	int tab2 = PLM_AUDIO_QUANT_LUT_STEP_1[tab1][self->bitrate_index];
+	tab3 = QUANT_LUT_STEP_2[tab2][self->samplerate_index];
+	sblimit = tab3 & 63;
+	tab3 >>= 6;
+
+	if (self->bound > sblimit) {
+		self->bound = sblimit;
+	}
+
+	// Read the allocation information
+	for (int sb = 0; sb < self->bound; sb++) {
+		self->allocation[0][sb] = plm_audio_read_allocation(self, sb, tab3);
+		self->allocation[1][sb] = plm_audio_read_allocation(self, sb, tab3);
+	}
+
+	for (int sb = self->bound; sb < sblimit; sb++) {
+		self->allocation[0][sb] =
+			self->allocation[1][sb] =
+			plm_audio_read_allocation(self, sb, tab3);
+	}
+
+	// Read scale factor selector information
+	int channels = (self->mode == PLM_AUDIO_MODE_MONO) ? 1 : 2;
+	for (int sb = 0; sb < sblimit; sb++) {
+		for (int ch = 0; ch < channels; ch++) {
+			if (self->allocation[ch][sb]) {
+				self->scale_factor_info[ch][sb] = plm_buffer_read(self->buffer, 2);
+			}
+		}
+		if (self->mode == PLM_AUDIO_MODE_MONO) {
+			self->scale_factor_info[1][sb] = self->scale_factor_info[0][sb];
+		}
+	}
+
+	// Read scale factors
+	for (int sb = 0; sb < sblimit; sb++) {
+		for (int ch = 0; ch < channels; ch++) {
+			if (self->allocation[ch][sb]) {
+				int *sf = self->scale_factor[ch][sb];
+				switch (self->scale_factor_info[ch][sb]) {
+					case 0:
+						sf[0] = plm_buffer_read(self->buffer, 6);
+						sf[1] = plm_buffer_read(self->buffer, 6);
+						sf[2] = plm_buffer_read(self->buffer, 6);
+						break;
+					case 1:
+						sf[0] = 
+						sf[1] = plm_buffer_read(self->buffer, 6);
+						sf[2] = plm_buffer_read(self->buffer, 6);
+						break;
+					case 2:
+						sf[0] = 
+						sf[1] = 
+						sf[2] = plm_buffer_read(self->buffer, 6);
+						break;
+					case 3:
+						sf[0] = plm_buffer_read(self->buffer, 6);
+						sf[1] = 
+						sf[2] = plm_buffer_read(self->buffer, 6);
+						break;
+				}
+			}
+		}
+		if (self->mode == PLM_AUDIO_MODE_MONO) {
+			self->scale_factor[1][sb][0] = self->scale_factor[0][sb][0];
+			self->scale_factor[1][sb][1] = self->scale_factor[0][sb][1];
+			self->scale_factor[1][sb][2] = self->scale_factor[0][sb][2];
+		}
+	}
+
+	// Coefficient input and reconstruction
+	int out_pos = 0;
+	for (int part = 0; part < 3; part++) {
+		for (int granule = 0; granule < 4; granule++) {
+
+			// Read the samples
+			for (int sb = 0; sb < self->bound; sb++) {
+				plm_audio_read_samples(self, 0, sb, part);
+				plm_audio_read_samples(self, 1, sb, part);
+			}
+			for (int sb = self->bound; sb < sblimit; sb++) {
+				plm_audio_read_samples(self, 0, sb, part);
+				self->sample[1][sb][0] = self->sample[0][sb][0];
+				self->sample[1][sb][1] = self->sample[0][sb][1];
+				self->sample[1][sb][2] = self->sample[0][sb][2];
+			}
+			for (int sb = sblimit; sb < 32; sb++) {
+				self->sample[0][sb][0] = 0;
+				self->sample[0][sb][1] = 0;
+				self->sample[0][sb][2] = 0;
+				self->sample[1][sb][0] = 0;
+				self->sample[1][sb][1] = 0;
+				self->sample[1][sb][2] = 0;
+			}
+
+			// Synthesis loop
+			for (int p = 0; p < 3; p++) {
+				// Shifting step
+				self->v_pos = (self->v_pos - 64) & 1023;
+
+				for (int ch = 0; ch < 2; ch++) {
+					plm_audio_idct36(self->sample[ch], p, self->V[ch], self->v_pos);
+
+					// Build U, windowing, calculate output
+					memset(self->U, 0, sizeof(self->U));
+
+					int d_index = 512 - (self->v_pos >> 1);
+					int v_index = (self->v_pos % 128) >> 1;
+					while (v_index < 1024) {
+						for (int i = 0; i < 32; ++i) {
+							self->U[i] += self->D[d_index++] * self->V[ch][v_index++];
+						}
+
+						v_index += 128 - 32;
+						d_index += 64 - 32;
+					}
+
+					d_index -= (512 - 32);
+					v_index = (128 - 32 + 1024) - v_index;
+					while (v_index < 1024) {
+						for (int i = 0; i < 32; ++i) {
+							self->U[i] += self->D[d_index++] * self->V[ch][v_index++];
+						}
+
+						v_index += 128 - 32;
+						d_index += 64 - 32;
+					}
+
+					// Output samples
+					#ifdef PLM_AUDIO_SEPARATE_CHANNELS
+						float *out_channel = ch == 0
+							? self->samples.left
+							: self->samples.right;
+						for (int j = 0; j < 32; j++) {
+							out_channel[out_pos + j] = self->U[j] / 2147418112.0f;
+						}
+					#else
+						for (int j = 0; j < 32; j++) {
+							self->samples.interleaved[((out_pos + j) << 1) + ch] = 
+								self->U[j] / 2147418112.0f;
+						}
+					#endif
+				} // End of synthesis channel loop
+				out_pos += 32;
+			} // End of synthesis sub-block loop
+
+		} // Decoding of the granule finished
+	}
+
+	plm_buffer_align(self->buffer);
+}
+
+const plm_quantizer_spec_t *plm_audio_read_allocation(plm_audio_t *self, int sb, int tab3) {
+	int tab4 = PLM_AUDIO_QUANT_LUT_STEP_3[tab3][sb];
+	int qtab = PLM_AUDIO_QUANT_LUT_STEP_4[tab4 & 15][plm_buffer_read(self->buffer, tab4 >> 4)];
+	return qtab ? (&PLM_AUDIO_QUANT_TAB[qtab - 1]) : 0;
+}
+
+void plm_audio_read_samples(plm_audio_t *self, int ch, int sb, int part) {
+	const plm_quantizer_spec_t *q = self->allocation[ch][sb];
+	int sf = self->scale_factor[ch][sb][part];
+	int *sample = self->sample[ch][sb];
+	int val = 0;
+
+	if (!q) {
+		// No bits allocated for this subband
+		sample[0] = sample[1] = sample[2] = 0;
+		return;
+	}
+
+	// Resolve scalefactor
+	if (sf == 63) {
+		sf = 0;
+	}
+	else {
+		int shift = (sf / 3) | 0;
+		sf = (PLM_AUDIO_SCALEFACTOR_BASE[sf % 3] + ((1 << shift) >> 1)) >> shift;
+	}
+
+	// Decode samples
+	int adj = q->levels;
+	if (q->group) {
+		// Decode grouped samples
+		val = plm_buffer_read(self->buffer, q->bits);
+		sample[0] = val % adj;
+		val /= adj;
+		sample[1] = val % adj;
+		sample[2] = val / adj;
+	}
+	else {
+		// Decode direct samples
+		sample[0] = plm_buffer_read(self->buffer, q->bits);
+		sample[1] = plm_buffer_read(self->buffer, q->bits);
+		sample[2] = plm_buffer_read(self->buffer, q->bits);
+	}
+
+	// Postmultiply samples
+	int scale = 65536 / (adj + 1);
+	adj = ((adj + 1) >> 1) - 1;
+
+	val = (adj - sample[0]) * scale;
+	sample[0] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+
+	val = (adj - sample[1]) * scale;
+	sample[1] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+
+	val = (adj - sample[2]) * scale;
+	sample[2] = (val * (sf >> 12) + ((val * (sf & 4095) + 2048) >> 12)) >> 12;
+}
+
+void plm_audio_idct36(int s[32][3], int ss, float *d, int dp) {
+	float t01, t02, t03, t04, t05, t06, t07, t08, t09, t10, t11, t12,
+		t13, t14, t15, t16, t17, t18, t19, t20, t21, t22, t23, t24,
+		t25, t26, t27, t28, t29, t30, t31, t32, t33;
+
+	t01 = (float)(s[0][ss] + s[31][ss]); t02 = (float)(s[0][ss] - s[31][ss]) * 0.500602998235f;
+	t03 = (float)(s[1][ss] + s[30][ss]); t04 = (float)(s[1][ss] - s[30][ss]) * 0.505470959898f;
+	t05 = (float)(s[2][ss] + s[29][ss]); t06 = (float)(s[2][ss] - s[29][ss]) * 0.515447309923f;
+	t07 = (float)(s[3][ss] + s[28][ss]); t08 = (float)(s[3][ss] - s[28][ss]) * 0.53104259109f;
+	t09 = (float)(s[4][ss] + s[27][ss]); t10 = (float)(s[4][ss] - s[27][ss]) * 0.553103896034f;
+	t11 = (float)(s[5][ss] + s[26][ss]); t12 = (float)(s[5][ss] - s[26][ss]) * 0.582934968206f;
+	t13 = (float)(s[6][ss] + s[25][ss]); t14 = (float)(s[6][ss] - s[25][ss]) * 0.622504123036f;
+	t15 = (float)(s[7][ss] + s[24][ss]); t16 = (float)(s[7][ss] - s[24][ss]) * 0.674808341455f;
+	t17 = (float)(s[8][ss] + s[23][ss]); t18 = (float)(s[8][ss] - s[23][ss]) * 0.744536271002f;
+	t19 = (float)(s[9][ss] + s[22][ss]); t20 = (float)(s[9][ss] - s[22][ss]) * 0.839349645416f;
+	t21 = (float)(s[10][ss] + s[21][ss]); t22 = (float)(s[10][ss] - s[21][ss]) * 0.972568237862f;
+	t23 = (float)(s[11][ss] + s[20][ss]); t24 = (float)(s[11][ss] - s[20][ss]) * 1.16943993343f;
+	t25 = (float)(s[12][ss] + s[19][ss]); t26 = (float)(s[12][ss] - s[19][ss]) * 1.48416461631f;
+	t27 = (float)(s[13][ss] + s[18][ss]); t28 = (float)(s[13][ss] - s[18][ss]) * 2.05778100995f;
+	t29 = (float)(s[14][ss] + s[17][ss]); t30 = (float)(s[14][ss] - s[17][ss]) * 3.40760841847f;
+	t31 = (float)(s[15][ss] + s[16][ss]); t32 = (float)(s[15][ss] - s[16][ss]) * 10.1900081235f;
+
+	t33 = t01 + t31; t31 = (t01 - t31) * 0.502419286188f;
+	t01 = t03 + t29; t29 = (t03 - t29) * 0.52249861494f;
+	t03 = t05 + t27; t27 = (t05 - t27) * 0.566944034816f;
+	t05 = t07 + t25; t25 = (t07 - t25) * 0.64682178336f;
+	t07 = t09 + t23; t23 = (t09 - t23) * 0.788154623451f;
+	t09 = t11 + t21; t21 = (t11 - t21) * 1.06067768599f;
+	t11 = t13 + t19; t19 = (t13 - t19) * 1.72244709824f;
+	t13 = t15 + t17; t17 = (t15 - t17) * 5.10114861869f;
+	t15 = t33 + t13; t13 = (t33 - t13) * 0.509795579104f;
+	t33 = t01 + t11; t01 = (t01 - t11) * 0.601344886935f;
+	t11 = t03 + t09; t09 = (t03 - t09) * 0.899976223136f;
+	t03 = t05 + t07; t07 = (t05 - t07) * 2.56291544774f;
+	t05 = t15 + t03; t15 = (t15 - t03) * 0.541196100146f;
+	t03 = t33 + t11; t11 = (t33 - t11) * 1.30656296488f;
+	t33 = t05 + t03; t05 = (t05 - t03) * 0.707106781187f;
+	t03 = t15 + t11; t15 = (t15 - t11) * 0.707106781187f;
+	t03 += t15;
+	t11 = t13 + t07; t13 = (t13 - t07) * 0.541196100146f;
+	t07 = t01 + t09; t09 = (t01 - t09) * 1.30656296488f;
+	t01 = t11 + t07; t07 = (t11 - t07) * 0.707106781187f;
+	t11 = t13 + t09; t13 = (t13 - t09) * 0.707106781187f;
+	t11 += t13; t01 += t11;
+	t11 += t07; t07 += t13;
+	t09 = t31 + t17; t31 = (t31 - t17) * 0.509795579104f;
+	t17 = t29 + t19; t29 = (t29 - t19) * 0.601344886935f;
+	t19 = t27 + t21; t21 = (t27 - t21) * 0.899976223136f;
+	t27 = t25 + t23; t23 = (t25 - t23) * 2.56291544774f;
+	t25 = t09 + t27; t09 = (t09 - t27) * 0.541196100146f;
+	t27 = t17 + t19; t19 = (t17 - t19) * 1.30656296488f;
+	t17 = t25 + t27; t27 = (t25 - t27) * 0.707106781187f;
+	t25 = t09 + t19; t19 = (t09 - t19) * 0.707106781187f;
+	t25 += t19;
+	t09 = t31 + t23; t31 = (t31 - t23) * 0.541196100146f;
+	t23 = t29 + t21; t21 = (t29 - t21) * 1.30656296488f;
+	t29 = t09 + t23; t23 = (t09 - t23) * 0.707106781187f;
+	t09 = t31 + t21; t31 = (t31 - t21) * 0.707106781187f;
+	t09 += t31;	t29 += t09;	t09 += t23;	t23 += t31;
+	t17 += t29;	t29 += t25;	t25 += t09;	t09 += t27;
+	t27 += t23;	t23 += t19; t19 += t31;
+	t21 = t02 + t32; t02 = (t02 - t32) * 0.502419286188f;
+	t32 = t04 + t30; t04 = (t04 - t30) * 0.52249861494f;
+	t30 = t06 + t28; t28 = (t06 - t28) * 0.566944034816f;
+	t06 = t08 + t26; t08 = (t08 - t26) * 0.64682178336f;
+	t26 = t10 + t24; t10 = (t10 - t24) * 0.788154623451f;
+	t24 = t12 + t22; t22 = (t12 - t22) * 1.06067768599f;
+	t12 = t14 + t20; t20 = (t14 - t20) * 1.72244709824f;
+	t14 = t16 + t18; t16 = (t16 - t18) * 5.10114861869f;
+	t18 = t21 + t14; t14 = (t21 - t14) * 0.509795579104f;
+	t21 = t32 + t12; t32 = (t32 - t12) * 0.601344886935f;
+	t12 = t30 + t24; t24 = (t30 - t24) * 0.899976223136f;
+	t30 = t06 + t26; t26 = (t06 - t26) * 2.56291544774f;
+	t06 = t18 + t30; t18 = (t18 - t30) * 0.541196100146f;
+	t30 = t21 + t12; t12 = (t21 - t12) * 1.30656296488f;
+	t21 = t06 + t30; t30 = (t06 - t30) * 0.707106781187f;
+	t06 = t18 + t12; t12 = (t18 - t12) * 0.707106781187f;
+	t06 += t12;
+	t18 = t14 + t26; t26 = (t14 - t26) * 0.541196100146f;
+	t14 = t32 + t24; t24 = (t32 - t24) * 1.30656296488f;
+	t32 = t18 + t14; t14 = (t18 - t14) * 0.707106781187f;
+	t18 = t26 + t24; t24 = (t26 - t24) * 0.707106781187f;
+	t18 += t24; t32 += t18;
+	t18 += t14; t26 = t14 + t24;
+	t14 = t02 + t16; t02 = (t02 - t16) * 0.509795579104f;
+	t16 = t04 + t20; t04 = (t04 - t20) * 0.601344886935f;
+	t20 = t28 + t22; t22 = (t28 - t22) * 0.899976223136f;
+	t28 = t08 + t10; t10 = (t08 - t10) * 2.56291544774f;
+	t08 = t14 + t28; t14 = (t14 - t28) * 0.541196100146f;
+	t28 = t16 + t20; t20 = (t16 - t20) * 1.30656296488f;
+	t16 = t08 + t28; t28 = (t08 - t28) * 0.707106781187f;
+	t08 = t14 + t20; t20 = (t14 - t20) * 0.707106781187f;
+	t08 += t20;
+	t14 = t02 + t10; t02 = (t02 - t10) * 0.541196100146f;
+	t10 = t04 + t22; t22 = (t04 - t22) * 1.30656296488f;
+	t04 = t14 + t10; t10 = (t14 - t10) * 0.707106781187f;
+	t14 = t02 + t22; t02 = (t02 - t22) * 0.707106781187f;
+	t14 += t02;	t04 += t14;	t14 += t10;	t10 += t02;
+	t16 += t04;	t04 += t08;	t08 += t14;	t14 += t28;
+	t28 += t10;	t10 += t20;	t20 += t02;	t21 += t16;
+	t16 += t32;	t32 += t04;	t04 += t06;	t06 += t08;
+	t08 += t18;	t18 += t14;	t14 += t30;	t30 += t28;
+	t28 += t26;	t26 += t10;	t10 += t12;	t12 += t20;
+	t20 += t24;	t24 += t02;
+
+	d[dp + 48] = -t33;
+	d[dp + 49] = d[dp + 47] = -t21;
+	d[dp + 50] = d[dp + 46] = -t17;
+	d[dp + 51] = d[dp + 45] = -t16;
+	d[dp + 52] = d[dp + 44] = -t01;
+	d[dp + 53] = d[dp + 43] = -t32;
+	d[dp + 54] = d[dp + 42] = -t29;
+	d[dp + 55] = d[dp + 41] = -t04;
+	d[dp + 56] = d[dp + 40] = -t03;
+	d[dp + 57] = d[dp + 39] = -t06;
+	d[dp + 58] = d[dp + 38] = -t25;
+	d[dp + 59] = d[dp + 37] = -t08;
+	d[dp + 60] = d[dp + 36] = -t11;
+	d[dp + 61] = d[dp + 35] = -t18;
+	d[dp + 62] = d[dp + 34] = -t09;
+	d[dp + 63] = d[dp + 33] = -t14;
+	d[dp + 32] = -t05;
+	d[dp + 0] = t05; d[dp + 31] = -t30;
+	d[dp + 1] = t30; d[dp + 30] = -t27;
+	d[dp + 2] = t27; d[dp + 29] = -t28;
+	d[dp + 3] = t28; d[dp + 28] = -t07;
+	d[dp + 4] = t07; d[dp + 27] = -t26;
+	d[dp + 5] = t26; d[dp + 26] = -t23;
+	d[dp + 6] = t23; d[dp + 25] = -t10;
+	d[dp + 7] = t10; d[dp + 24] = -t15;
+	d[dp + 8] = t15; d[dp + 23] = -t12;
+	d[dp + 9] = t12; d[dp + 22] = -t19;
+	d[dp + 10] = t19; d[dp + 21] = -t20;
+	d[dp + 11] = t20; d[dp + 20] = -t13;
+	d[dp + 12] = t13; d[dp + 19] = -t24;
+	d[dp + 13] = t24; d[dp + 18] = -t31;
+	d[dp + 14] = t31; d[dp + 17] = -t02;
+	d[dp + 15] = t02; d[dp + 16] = 0.0;
+}
+
+
+#endif // PL_MPEG_IMPLEMENTATION
--- /dev/null
+++ b/src/libs/qoa.h
@@ -1,0 +1,728 @@
+/*
+
+Copyright (c) 2023, Dominic Szablewski - https://phoboslab.org
+SPDX-License-Identifier: MIT
+
+QOA - The "Quite OK Audio" format for fast, lossy audio compression
+
+
+-- Data Format
+
+QOA encodes pulse-code modulated (PCM) audio data with up to 255 channels, 
+sample rates from 1 up to 16777215 hertz and a bit depth of 16 bits.
+
+The compression method employed in QOA is lossy; it discards some information
+from the uncompressed PCM data. For many types of audio signals this compression
+is "transparent", i.e. the difference from the original file is often not
+audible.
+
+QOA encodes 20 samples of 16 bit PCM data into slices of 64 bits. A single
+sample therefore requires 3.2 bits of storage space, resulting in a 5x
+compression (16 / 3.2).
+
+A QOA file consists of an 8 byte file header, followed by a number of frames.
+Each frame contains an 8 byte frame header, the current 16 byte en-/decoder
+state per channel and 256 slices per channel. Each slice is 8 bytes wide and
+encodes 20 samples of audio data.
+
+All values, including the slices, are big endian. The file layout is as follows:
+
+struct {
+	struct {
+		char     magic[4];         // magic bytes "qoaf"
+		uint32_t samples;          // samples per channel in this file
+	} file_header;             
+
+	struct {
+		struct {
+			uint8_t  num_channels; // no. of channels
+			uint24_t samplerate;   // samplerate in hz
+			uint16_t fsamples;     // samples per channel in this frame
+			uint16_t fsize;        // frame size (includes this header)
+		} frame_header;          
+
+		struct {
+			int16_t history[4];    // most recent last
+			int16_t weights[4];    // most recent last
+		} lms_state[num_channels]; 
+
+		qoa_slice_t slices[256][num_channels];
+
+	} frames[ceil(samples / (256 * 20))];
+} qoa_file_t;
+
+Each `qoa_slice_t` contains a quantized scalefactor `sf_quant` and 20 quantized
+residuals `qrNN`:
+
+.- QOA_SLICE -- 64 bits, 20 samples --------------------------/  /------------.
+|        Byte[0]         |        Byte[1]         |  Byte[2]  \  \  Byte[7]   |
+| 7  6  5  4  3  2  1  0 | 7  6  5  4  3  2  1  0 | 7  6  5   /  /    2  1  0 |
+|------------+--------+--------+--------+---------+---------+-\  \--+---------|
+|  sf_quant  |  qr00  |  qr01  |  qr02  |  qr03   |  qr04   | /  /  |  qr19   |
+`-------------------------------------------------------------\  \------------`
+
+Each frame except the last must contain exactly 256 slices per channel. The last
+frame may contain between 1 .. 256 (inclusive) slices per channel. The last
+slice (for each channel) in the last frame may contain less than 20 samples; the
+slice still must be 8 bytes wide, with the unused samples zeroed out.
+
+Channels are interleaved per slice. E.g. for 2 channel stereo: 
+slice[0] = L, slice[1] = R, slice[2] = L, slice[3] = R ...
+
+A valid QOA file or stream must have at least one frame. Each frame must contain
+at least one channel and one sample with a samplerate between 1 .. 16777215
+(inclusive).
+
+If the total number of samples is not known by the encoder, the samples in the
+file header may be set to 0x00000000 to indicate that the encoder is 
+"streaming". In a streaming context, the samplerate and number of channels may
+differ from frame to frame. For static files (those with samples set to a
+non-zero value), each frame must have the same number of channels and same
+samplerate.
+
+Note that this implementation of QOA only handles files with a known total
+number of samples.
+
+A decoder should support at least 8 channels. The channel layout for channel
+counts 1 .. 8 is:
+
+	1. Mono
+	2. L, R
+	3. L, R, C 
+	4. FL, FR, B/SL, B/SR 
+	5. FL, FR, C, B/SL, B/SR 
+	6. FL, FR, C, LFE, B/SL, B/SR
+	7. FL, FR, C, LFE, B, SL, SR 
+	8. FL, FR, C, LFE, BL, BR, SL, SR
+
+QOA predicts each audio sample based on the previously decoded ones using a
+"Sign-Sign Least Mean Squares Filter" (LMS). This prediction plus the 
+dequantized residual forms the final output sample.
+
+*/
+
+
+
+/* -----------------------------------------------------------------------------
+	Header - Public functions */
+
+#ifndef QOA_H
+#define QOA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define QOA_MIN_FILESIZE 16
+#define QOA_MAX_CHANNELS 8
+
+#define QOA_SLICE_LEN 20
+#define QOA_SLICES_PER_FRAME 256
+#define QOA_FRAME_LEN (QOA_SLICES_PER_FRAME * QOA_SLICE_LEN)
+#define QOA_LMS_LEN 4
+#define QOA_MAGIC 0x716f6166 /* 'qoaf' */
+
+#define QOA_FRAME_SIZE(channels, slices) \
+	(8 + QOA_LMS_LEN * 4 * channels + 8 * slices * channels)
+
+typedef struct {
+	int history[QOA_LMS_LEN];
+	int weights[QOA_LMS_LEN];
+} qoa_lms_t;
+
+typedef struct {
+	unsigned int channels;
+	unsigned int samplerate;
+	unsigned int samples;
+	qoa_lms_t lms[QOA_MAX_CHANNELS];
+	#ifdef QOA_RECORD_TOTAL_ERROR
+		double error;
+	#endif
+} qoa_desc;
+
+unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes);
+unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes);
+void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len);
+
+unsigned int qoa_max_frame_size(qoa_desc *qoa);
+unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa);
+unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len);
+short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *file);
+
+#ifndef QOA_NO_STDIO
+
+int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa);
+void *qoa_read(const char *filename, qoa_desc *qoa);
+
+#endif /* QOA_NO_STDIO */
+
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* QOA_H */
+
+
+/* -----------------------------------------------------------------------------
+	Implementation */
+
+#ifdef QOA_IMPLEMENTATION
+#include <stdlib.h>
+
+#ifndef QOA_MALLOC
+	#define QOA_MALLOC(sz) malloc(sz)
+	#define QOA_FREE(p) free(p)
+#endif
+
+typedef unsigned long long qoa_uint64_t;
+
+
+/* The quant_tab provides an index into the dequant_tab for residuals in the
+range of -8 .. 8. It maps this range to just 3bits and becomes less accurate at 
+the higher end. Note that the residual zero is identical to the lowest positive 
+value. This is mostly fine, since the qoa_div() function always rounds away 
+from zero. */
+
+static const int qoa_quant_tab[17] = {
+	7, 7, 7, 5, 5, 3, 3, 1, /* -8..-1 */
+	0,                      /*  0     */
+	0, 2, 2, 4, 4, 6, 6, 6  /*  1.. 8 */
+};
+
+
+/* We have 16 different scalefactors. Like the quantized residuals these become
+less accurate at the higher end. In theory, the highest scalefactor that we
+would need to encode the highest 16bit residual is (2**16)/8 = 8192. However we
+rely on the LMS filter to predict samples accurately enough that a maximum 
+residual of one quarter of the 16 bit range is sufficient. I.e. with the 
+scalefactor 2048 times the quant range of 8 we can encode residuals up to 2**14.
+
+The scalefactor values are computed as:
+scalefactor_tab[s] <- round(pow(s + 1, 2.75)) */
+
+static const int qoa_scalefactor_tab[16] = {
+	1, 7, 21, 45, 84, 138, 211, 304, 421, 562, 731, 928, 1157, 1419, 1715, 2048
+};
+
+
+/* The reciprocal_tab maps each of the 16 scalefactors to their rounded 
+reciprocals 1/scalefactor. This allows us to calculate the scaled residuals in 
+the encoder with just one multiplication instead of an expensive division. We 
+do this in .16 fixed point with integers, instead of floats.
+
+The reciprocal_tab is computed as:
+reciprocal_tab[s] <- ((1<<16) + scalefactor_tab[s] - 1) / scalefactor_tab[s] */
+
+static const int qoa_reciprocal_tab[16] = {
+	65536, 9363, 3121, 1457, 781, 475, 311, 216, 156, 117, 90, 71, 57, 47, 39, 32
+};
+
+
+/* The dequant_tab maps each of the scalefactors and quantized residuals to 
+their unscaled & dequantized version.
+
+Since qoa_div rounds away from the zero, the smallest entries are mapped to 3/4
+instead of 1. The dequant_tab assumes the following dequantized values for each 
+of the quant_tab indices and is computed as:
+float dqt[8] = {0.75, -0.75, 2.5, -2.5, 4.5, -4.5, 7, -7};
+dequant_tab[s][q] <- round_ties_away_from_zero(scalefactor_tab[s] * dqt[q])
+
+The rounding employed here is "to nearest, ties away from zero",  i.e. positive
+and negative values are treated symmetrically.
+*/
+
+static const int qoa_dequant_tab[16][8] = {
+	{   1,    -1,    3,    -3,    5,    -5,     7,     -7},
+	{   5,    -5,   18,   -18,   32,   -32,    49,    -49},
+	{  16,   -16,   53,   -53,   95,   -95,   147,   -147},
+	{  34,   -34,  113,  -113,  203,  -203,   315,   -315},
+	{  63,   -63,  210,  -210,  378,  -378,   588,   -588},
+	{ 104,  -104,  345,  -345,  621,  -621,   966,   -966},
+	{ 158,  -158,  528,  -528,  950,  -950,  1477,  -1477},
+	{ 228,  -228,  760,  -760, 1368, -1368,  2128,  -2128},
+	{ 316,  -316, 1053, -1053, 1895, -1895,  2947,  -2947},
+	{ 422,  -422, 1405, -1405, 2529, -2529,  3934,  -3934},
+	{ 548,  -548, 1828, -1828, 3290, -3290,  5117,  -5117},
+	{ 696,  -696, 2320, -2320, 4176, -4176,  6496,  -6496},
+	{ 868,  -868, 2893, -2893, 5207, -5207,  8099,  -8099},
+	{1064, -1064, 3548, -3548, 6386, -6386,  9933,  -9933},
+	{1286, -1286, 4288, -4288, 7718, -7718, 12005, -12005},
+	{1536, -1536, 5120, -5120, 9216, -9216, 14336, -14336},
+};
+
+
+/* The Least Mean Squares Filter is the heart of QOA. It predicts the next
+sample based on the previous 4 reconstructed samples. It does so by continuously
+adjusting 4 weights based on the residual of the previous prediction.
+
+The next sample is predicted as the sum of (weight[i] * history[i]).
+
+The adjustment of the weights is done with a "Sign-Sign-LMS" that adds or
+subtracts the residual to each weight, based on the corresponding sample from 
+the history. This, surprisingly, is sufficient to get worthwhile predictions.
+
+This is all done with fixed point integers. Hence the right-shifts when updating
+the weights and calculating the prediction. */
+
+static int qoa_lms_predict(qoa_lms_t *lms) {
+	int prediction = 0;
+	for (int i = 0; i < QOA_LMS_LEN; i++) {
+		prediction += lms->weights[i] * lms->history[i];
+	}
+	return prediction >> 13;
+}
+
+static void qoa_lms_update(qoa_lms_t *lms, int sample, int residual) {
+	int delta = residual >> 4;
+	for (int i = 0; i < QOA_LMS_LEN; i++) {
+		lms->weights[i] += lms->history[i] < 0 ? -delta : delta;
+	}
+
+	for (int i = 0; i < QOA_LMS_LEN-1; i++) {
+		lms->history[i] = lms->history[i+1];
+	}
+	lms->history[QOA_LMS_LEN-1] = sample;
+}
+
+
+/* qoa_div() implements a rounding division, but avoids rounding to zero for 
+small numbers. E.g. 0.1 will be rounded to 1. Note that 0 itself still 
+returns as 0, which is handled in the qoa_quant_tab[].
+qoa_div() takes an index into the .16 fixed point qoa_reciprocal_tab as an
+argument, so it can do the division with a cheaper integer multiplication. */
+
+static inline int qoa_div(int v, int scalefactor) {
+	int reciprocal = qoa_reciprocal_tab[scalefactor];
+	int n = (v * reciprocal + (1 << 15)) >> 16;
+	n = n + ((v > 0) - (v < 0)) - ((n > 0) - (n < 0)); /* round away from 0 */
+	return n;
+}
+
+static inline int qoa_clamp(int v, int min, int max) {
+	if (v < min) { return min; }
+	if (v > max) { return max; }
+	return v;
+}
+
+/* This specialized clamp function for the signed 16 bit range improves decode
+performance quite a bit. The extra if() statement works nicely with the CPUs
+branch prediction as this branch is rarely taken. */
+
+static inline int qoa_clamp_s16(int v) {
+	if ((unsigned int)(v + 32768) > 65535) {
+		if (v < -32768) { return -32768; }
+		if (v >  32767) { return  32767; }
+	}
+	return v;
+}
+
+static inline qoa_uint64_t qoa_read_u64(const unsigned char *bytes, unsigned int *p) {
+	bytes += *p;
+	*p += 8;
+	return 
+		((qoa_uint64_t)(bytes[0]) << 56) | ((qoa_uint64_t)(bytes[1]) << 48) |
+		((qoa_uint64_t)(bytes[2]) << 40) | ((qoa_uint64_t)(bytes[3]) << 32) |
+		((qoa_uint64_t)(bytes[4]) << 24) | ((qoa_uint64_t)(bytes[5]) << 16) |
+		((qoa_uint64_t)(bytes[6]) <<  8) | ((qoa_uint64_t)(bytes[7]) <<  0);
+}
+
+static inline void qoa_write_u64(qoa_uint64_t v, unsigned char *bytes, unsigned int *p) {
+	bytes += *p;
+	*p += 8;
+	bytes[0] = (v >> 56) & 0xff;
+	bytes[1] = (v >> 48) & 0xff;
+	bytes[2] = (v >> 40) & 0xff;
+	bytes[3] = (v >> 32) & 0xff;
+	bytes[4] = (v >> 24) & 0xff;
+	bytes[5] = (v >> 16) & 0xff;
+	bytes[6] = (v >>  8) & 0xff;
+	bytes[7] = (v >>  0) & 0xff;
+}
+
+
+/* -----------------------------------------------------------------------------
+	Encoder */
+
+unsigned int qoa_encode_header(qoa_desc *qoa, unsigned char *bytes) {
+	unsigned int p = 0;
+	qoa_write_u64(((qoa_uint64_t)QOA_MAGIC << 32) | qoa->samples, bytes, &p);
+	return p;
+}
+
+unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int frame_len, unsigned char *bytes) {
+	unsigned int channels = qoa->channels;
+
+	unsigned int p = 0;
+	unsigned int slices = (frame_len + QOA_SLICE_LEN - 1) / QOA_SLICE_LEN;
+	unsigned int frame_size = QOA_FRAME_SIZE(channels, slices);
+	int prev_scalefactor[QOA_MAX_CHANNELS] = {0};
+
+	/* Write the frame header */
+	qoa_write_u64((
+		(qoa_uint64_t)qoa->channels   << 56 |
+		(qoa_uint64_t)qoa->samplerate << 32 |
+		(qoa_uint64_t)frame_len       << 16 |
+		(qoa_uint64_t)frame_size
+	), bytes, &p);
+
+	
+	for (int c = 0; c < channels; c++) {
+		/* If the weights have grown too large, reset them to 0. This may happen
+		with certain high-frequency sounds. This is a last resort and will 
+		introduce quite a bit of noise, but should at least prevent pops/clicks */
+		int weights_sum = 
+			qoa->lms[c].weights[0] * qoa->lms[c].weights[0] + 
+			qoa->lms[c].weights[1] * qoa->lms[c].weights[1] + 
+			qoa->lms[c].weights[2] * qoa->lms[c].weights[2] + 
+			qoa->lms[c].weights[3] * qoa->lms[c].weights[3];
+		if (weights_sum > 0x2fffffff) {
+			qoa->lms[c].weights[0] = 0;
+			qoa->lms[c].weights[1] = 0;
+			qoa->lms[c].weights[2] = 0;
+			qoa->lms[c].weights[3] = 0;
+		}
+
+		/* Write the current LMS state */
+		qoa_uint64_t weights = 0;
+		qoa_uint64_t history = 0;
+		for (int i = 0; i < QOA_LMS_LEN; i++) {
+			history = (history << 16) | (qoa->lms[c].history[i] & 0xffff);
+			weights = (weights << 16) | (qoa->lms[c].weights[i] & 0xffff);
+		}
+		qoa_write_u64(history, bytes, &p);
+		qoa_write_u64(weights, bytes, &p);
+	}
+
+	/* We encode all samples with the channels interleaved on a slice level.
+	E.g. for stereo: (ch-0, slice 0), (ch 1, slice 0), (ch 0, slice 1), ...*/
+	for (int sample_index = 0; sample_index < frame_len; sample_index += QOA_SLICE_LEN) {
+
+		for (int c = 0; c < channels; c++) {
+			int slice_len = qoa_clamp(QOA_SLICE_LEN, 0, frame_len - sample_index);
+			int slice_start = sample_index * channels + c;
+			int slice_end = (sample_index + slice_len) * channels + c;			
+
+			/* Brute for search for the best scalefactor. Just go through all
+			16 scalefactors, encode all samples for the current slice and 
+			meassure the total squared error. */
+			qoa_uint64_t best_error = -1;
+			qoa_uint64_t best_slice;
+			qoa_lms_t best_lms;
+			int best_scalefactor;
+
+			for (int sfi = 0; sfi < 16; sfi++) {
+				/* There is a strong correlation between the scalefactors of
+				neighboring slices. As an optimization, start testing
+				the best scalefactor of the previous slice first. */
+				int scalefactor = (sfi + prev_scalefactor[c]) % 16;
+
+				/* We have to reset the LMS state to the last known good one
+				before trying each scalefactor, as each pass updates the LMS
+				state when encoding. */
+				qoa_lms_t lms = qoa->lms[c];
+				qoa_uint64_t slice = scalefactor;
+				qoa_uint64_t current_error = 0;
+
+				for (int si = slice_start; si < slice_end; si += channels) {
+					int sample = sample_data[si];
+					int predicted = qoa_lms_predict(&lms);
+
+					int residual = sample - predicted;
+					int scaled = qoa_div(residual, scalefactor);
+					int clamped = qoa_clamp(scaled, -8, 8);
+					int quantized = qoa_quant_tab[clamped + 8];
+					int dequantized = qoa_dequant_tab[scalefactor][quantized];
+					int reconstructed = qoa_clamp_s16(predicted + dequantized);
+
+					long long error = (sample - reconstructed);
+					current_error += error * error;
+					if (current_error > best_error) {
+						break;
+					}
+
+					qoa_lms_update(&lms, reconstructed, dequantized);
+					slice = (slice << 3) | quantized;
+				}
+
+				if (current_error < best_error) {
+					best_error = current_error;
+					best_slice = slice;
+					best_lms = lms;
+					best_scalefactor = scalefactor;
+				}
+			}
+
+			prev_scalefactor[c] = best_scalefactor;
+
+			qoa->lms[c] = best_lms;
+			#ifdef QOA_RECORD_TOTAL_ERROR
+				qoa->error += best_error;
+			#endif
+
+			/* If this slice was shorter than QOA_SLICE_LEN, we have to left-
+			shift all encoded data, to ensure the rightmost bits are the empty
+			ones. This should only happen in the last frame of a file as all
+			slices are completely filled otherwise. */
+			best_slice <<= (QOA_SLICE_LEN - slice_len) * 3;
+			qoa_write_u64(best_slice, bytes, &p);
+		}
+	}
+	
+	return p;
+}
+
+void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len) {
+	if (
+		qoa->samples == 0 || 
+		qoa->samplerate == 0 || qoa->samplerate > 0xffffff ||
+		qoa->channels == 0 || qoa->channels > QOA_MAX_CHANNELS
+	) {
+		return NULL;
+	}
+
+	/* Calculate the encoded size and allocate */
+	unsigned int num_frames = (qoa->samples + QOA_FRAME_LEN-1) / QOA_FRAME_LEN;
+	unsigned int num_slices = (qoa->samples + QOA_SLICE_LEN-1) / QOA_SLICE_LEN;
+	unsigned int encoded_size = 8 +                    /* 8 byte file header */
+		num_frames * 8 +                               /* 8 byte frame headers */
+		num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
+		num_slices * 8 * qoa->channels;                /* 8 byte slices */
+
+	unsigned char *bytes = QOA_MALLOC(encoded_size);
+
+	for (int c = 0; c < qoa->channels; c++) {
+		/* Set the initial LMS weights to {0, 0, -1, 2}. This helps with the 
+		prediction of the first few ms of a file. */
+		qoa->lms[c].weights[0] = 0;
+		qoa->lms[c].weights[1] = 0;
+		qoa->lms[c].weights[2] = -(1<<13);
+		qoa->lms[c].weights[3] =  (1<<14);
+
+		/* Explicitly set the history samples to 0, as we might have some
+		garbage in there. */
+		for (int i = 0; i < QOA_LMS_LEN; i++) {
+			qoa->lms[c].history[i] = 0;
+		}
+	}
+
+
+	/* Encode the header and go through all frames */
+	unsigned int p = qoa_encode_header(qoa, bytes);
+	#ifdef QOA_RECORD_TOTAL_ERROR
+		qoa->error = 0;
+	#endif
+
+	int frame_len = QOA_FRAME_LEN;
+	for (int sample_index = 0; sample_index < qoa->samples; sample_index += frame_len) {
+		frame_len = qoa_clamp(QOA_FRAME_LEN, 0, qoa->samples - sample_index);		
+		const short *frame_samples = sample_data + sample_index * qoa->channels;
+		unsigned int frame_size = qoa_encode_frame(frame_samples, qoa, frame_len, bytes + p);
+		p += frame_size;
+	}
+
+	*out_len = p;
+	return bytes;
+}
+
+
+
+/* -----------------------------------------------------------------------------
+	Decoder */
+
+unsigned int qoa_max_frame_size(qoa_desc *qoa) {
+	return QOA_FRAME_SIZE(qoa->channels, QOA_SLICES_PER_FRAME);
+}
+
+unsigned int qoa_decode_header(const unsigned char *bytes, int size, qoa_desc *qoa) {
+	unsigned int p = 0;
+	if (size < QOA_MIN_FILESIZE) {
+		return 0;
+	}
+
+
+	/* Read the file header, verify the magic number ('qoaf') and read the 
+	total number of samples. */
+	qoa_uint64_t file_header = qoa_read_u64(bytes, &p);
+
+	if ((file_header >> 32) != QOA_MAGIC) {
+		return 0;
+	}
+
+	qoa->samples = file_header & 0xffffffff;
+	if (!qoa->samples) {
+		return 0;
+	}
+
+	/* Peek into the first frame header to get the number of channels and
+	the samplerate. */
+	qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
+	qoa->channels   = (frame_header >> 56) & 0x0000ff;
+	qoa->samplerate = (frame_header >> 32) & 0xffffff;
+
+	if (qoa->channels == 0 || qoa->samples == 0 || qoa->samplerate == 0) {
+		return 0;
+	}
+
+	return 8;
+}
+
+unsigned int qoa_decode_frame(const unsigned char *bytes, unsigned int size, qoa_desc *qoa, short *sample_data, unsigned int *frame_len) {
+	unsigned int p = 0;
+	*frame_len = 0;
+
+	if (size < 8 + QOA_LMS_LEN * 4 * qoa->channels) {
+		return 0;
+	}
+
+	/* Read and verify the frame header */
+	qoa_uint64_t frame_header = qoa_read_u64(bytes, &p);
+	int channels   = (frame_header >> 56) & 0x0000ff;
+	int samplerate = (frame_header >> 32) & 0xffffff;
+	int samples    = (frame_header >> 16) & 0x00ffff;
+	int frame_size = (frame_header      ) & 0x00ffff;
+
+	int data_size = frame_size - 8 - QOA_LMS_LEN * 4 * channels;
+	int num_slices = data_size / 8;
+	int max_total_samples = num_slices * QOA_SLICE_LEN;
+
+	if (
+		channels != qoa->channels || 
+		samplerate != qoa->samplerate ||
+		frame_size > size ||
+		samples * channels > max_total_samples
+	) {
+		return 0;
+	}
+
+
+	/* Read the LMS state: 4 x 2 bytes history, 4 x 2 bytes weights per channel */
+	for (int c = 0; c < channels; c++) {
+		qoa_uint64_t history = qoa_read_u64(bytes, &p);
+		qoa_uint64_t weights = qoa_read_u64(bytes, &p);
+		for (int i = 0; i < QOA_LMS_LEN; i++) {
+			qoa->lms[c].history[i] = ((signed short)(history >> 48));
+			history <<= 16;
+			qoa->lms[c].weights[i] = ((signed short)(weights >> 48));
+			weights <<= 16;
+		}
+	}
+
+
+	/* Decode all slices for all channels in this frame */
+	for (int sample_index = 0; sample_index < samples; sample_index += QOA_SLICE_LEN) {
+		for (int c = 0; c < channels; c++) {
+			qoa_uint64_t slice = qoa_read_u64(bytes, &p);
+
+			int scalefactor = (slice >> 60) & 0xf;
+			int slice_start = sample_index * channels + c;
+			int slice_end = qoa_clamp(sample_index + QOA_SLICE_LEN, 0, samples) * channels + c;
+
+			for (int si = slice_start; si < slice_end; si += channels) {
+				int predicted = qoa_lms_predict(&qoa->lms[c]);
+				int quantized = (slice >> 57) & 0x7;
+				int dequantized = qoa_dequant_tab[scalefactor][quantized];
+				int reconstructed = qoa_clamp_s16(predicted + dequantized);
+				
+				sample_data[si] = reconstructed;
+				slice <<= 3;
+
+				qoa_lms_update(&qoa->lms[c], reconstructed, dequantized);
+			}
+		}
+	}
+
+	*frame_len = samples;
+	return p;
+}
+
+short *qoa_decode(const unsigned char *bytes, int size, qoa_desc *qoa) {
+	unsigned int p = qoa_decode_header(bytes, size, qoa);
+	if (!p) {
+		return NULL;
+	}
+
+	/* Calculate the required size of the sample buffer and allocate */
+	int total_samples = qoa->samples * qoa->channels;
+	short *sample_data = QOA_MALLOC(total_samples * sizeof(short));
+
+	unsigned int sample_index = 0;
+	unsigned int frame_len;
+	unsigned int frame_size;
+
+	/* Decode all frames */
+	do {
+		short *sample_ptr = sample_data + sample_index * qoa->channels;
+		frame_size = qoa_decode_frame(bytes + p, size - p, qoa, sample_ptr, &frame_len);
+
+		p += frame_size;
+		sample_index += frame_len;
+	} while (frame_size && sample_index < qoa->samples);
+
+	qoa->samples = sample_index;
+	return sample_data;
+}
+
+
+
+/* -----------------------------------------------------------------------------
+	File read/write convenience functions */
+
+#ifndef QOA_NO_STDIO
+#include <stdio.h>
+
+int qoa_write(const char *filename, const short *sample_data, qoa_desc *qoa) {
+	FILE *f = fopen(filename, "wb");
+	unsigned int size;
+	void *encoded;
+
+	if (!f) {
+		return 0;
+	}
+
+	encoded = qoa_encode(sample_data, qoa, &size);
+	if (!encoded) {
+		fclose(f);
+		return 0;
+	}
+
+	fwrite(encoded, 1, size, f);
+	fclose(f);
+
+	QOA_FREE(encoded);
+	return size;
+}
+
+void *qoa_read(const char *filename, qoa_desc *qoa) {
+	FILE *f = fopen(filename, "rb");
+	int size, bytes_read;
+	void *data;
+	short *sample_data;
+
+	if (!f) {
+		return NULL;
+	}
+
+	fseek(f, 0, SEEK_END);
+	size = ftell(f);
+	if (size <= 0) {
+		fclose(f);
+		return NULL;
+	}
+	fseek(f, 0, SEEK_SET);
+
+	data = QOA_MALLOC(size);
+	if (!data) {
+		fclose(f);
+		return NULL;
+	}
+
+	bytes_read = fread(data, 1, size, f);
+	fclose(f);
+
+	sample_data = qoa_decode(data, bytes_read, qoa);
+	QOA_FREE(data);
+	return sample_data;
+}
+
+#endif /* QOA_NO_STDIO */
+#endif /* QOA_IMPLEMENTATION */
--- /dev/null
+++ b/src/libs/sokol_app.h
@@ -1,0 +1,11057 @@
+#if defined(SOKOL_IMPL) && !defined(SOKOL_APP_IMPL)
+#define SOKOL_APP_IMPL
+#endif
+#ifndef SOKOL_APP_INCLUDED
+/*
+    sokol_app.h -- cross-platform application wrapper
+
+    Project URL: https://github.com/floooh/sokol
+
+    Do this:
+        #define SOKOL_IMPL or
+        #define SOKOL_APP_IMPL
+    before you include this file in *one* C or C++ file to create the
+    implementation.
+
+    In the same place define one of the following to select the 3D-API
+    which should be initialized by sokol_app.h (this must also match
+    the backend selected for sokol_gfx.h if both are used in the same
+    project):
+
+        #define SOKOL_GLCORE33
+        #define SOKOL_GLES2
+        #define SOKOL_GLES3
+        #define SOKOL_D3D11
+        #define SOKOL_METAL
+        #define SOKOL_WGPU
+
+    Optionally provide the following defines with your own implementations:
+
+        SOKOL_ASSERT(c)     - your own assert macro (default: assert(c))
+        SOKOL_LOG(msg)      - your own logging function (default: puts(msg))
+        SOKOL_UNREACHABLE() - a guard macro for unreachable code (default: assert(false))
+        SOKOL_ABORT()       - called after an unrecoverable error (default: abort())
+        SOKOL_WIN32_FORCE_MAIN  - define this on Win32 to use a main() entry point instead of WinMain
+        SOKOL_NO_ENTRY      - define this if sokol_app.h shouldn't "hijack" the main() function
+        SOKOL_APP_API_DECL  - public function declaration prefix (default: extern)
+        SOKOL_API_DECL      - same as SOKOL_APP_API_DECL
+        SOKOL_API_IMPL      - public function implementation prefix (default: -)
+        SOKOL_CALLOC        - your own calloc function (default: calloc(n, s))
+        SOKOL_FREE          - your own free function (default: free(p))
+
+    Optionally define the following to force debug checks and validations
+    even in release mode:
+
+        SOKOL_DEBUG         - by default this is defined if _DEBUG is defined
+
+    If sokol_app.h is compiled as a DLL, define the following before
+    including the declaration or implementation:
+
+        SOKOL_DLL
+
+    On Windows, SOKOL_DLL will define SOKOL_APP_API_DECL as __declspec(dllexport)
+    or __declspec(dllimport) as needed.
+
+    For example code, see https://github.com/floooh/sokol-samples/tree/master/sapp
+
+    Portions of the Windows and Linux GL initialization, event-, icon- etc... code
+    have been taken from GLFW (http://www.glfw.org/)
+
+    iOS onscreen keyboard support 'inspired' by libgdx.
+
+    Link with the following system libraries:
+
+    - on macOS with Metal: Cocoa, QuartzCore, Metal, MetalKit
+    - on macOS with GL: Cocoa, QuartzCore, OpenGL
+    - on iOS with Metal: Foundation, UIKit, Metal, MetalKit
+    - on iOS with GL: Foundation, UIKit, OpenGLES, GLKit
+    - on Linux: X11, Xi, Xcursor, GL, dl, pthread, m(?)
+    - on Android: GLESv3, EGL, log, android
+    - on Windows with the MSVC or Clang toolchains: no action needed, libs are defined in-source via pragma-comment-lib
+    - on Windows with MINGW/MSYS2 gcc: compile with '-mwin32' so that _WIN32 is defined
+        - link with the following libs: -lkernel32 -luser32 -lshell32
+        - additionally with the GL backend: -lgdi32
+        - additionally with the D3D11 backend: -ld3d11 -ldxgi
+
+    On Linux, you also need to use the -pthread compiler and linker option, otherwise weird
+    things will happen, see here for details: https://github.com/floooh/sokol/issues/376
+
+    Building for UWP requires a recent Visual Studio toolchain and Windows SDK
+    (at least VS2019 and Windows SDK 10.0.19041.0). When the UWP backend is
+    selected, the sokol_app.h implementation must be compiled as C++17.
+
+    On macOS and iOS, the implementation must be compiled as Objective-C.
+
+    FEATURE OVERVIEW
+    ================
+    sokol_app.h provides a minimalistic cross-platform API which
+    implements the 'application-wrapper' parts of a 3D application:
+
+    - a common application entry function
+    - creates a window and 3D-API context/device with a 'default framebuffer'
+    - makes the rendered frame visible
+    - provides keyboard-, mouse- and low-level touch-events
+    - platforms: MacOS, iOS, HTML5, Win32, Linux, Android (TODO: RaspberryPi)
+    - 3D-APIs: Metal, D3D11, GL3.2, GLES2, GLES3, WebGL, WebGL2
+
+    FEATURE/PLATFORM MATRIX
+    =======================
+                        | Windows | macOS | Linux |  iOS  | Android | UWP  | Raspi | HTML5
+    --------------------+---------+-------+-------+-------+---------+------+-------+-------
+    gl 3.x              | YES     | YES   | YES   | ---   | ---     | ---  | ---   | ---
+    gles2/webgl         | ---     | ---   | ---   | YES   | YES     | ---  | TODO  | YES
+    gles3/webgl2        | ---     | ---   | ---   | YES   | YES     | ---  | ---   | YES
+    metal               | ---     | YES   | ---   | YES   | ---     | ---  | ---   | ---
+    d3d11               | YES     | ---   | ---   | ---   | ---     | YES  | ---   | ---
+    KEY_DOWN            | YES     | YES   | YES   | SOME  | TODO    | YES  | TODO  | YES
+    KEY_UP              | YES     | YES   | YES   | SOME  | TODO    | YES  | TODO  | YES
+    CHAR                | YES     | YES   | YES   | YES   | TODO    | YES  | TODO  | YES
+    MOUSE_DOWN          | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    MOUSE_UP            | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    MOUSE_SCROLL        | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    MOUSE_MOVE          | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    MOUSE_ENTER         | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    MOUSE_LEAVE         | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    TOUCHES_BEGAN       | ---     | ---   | ---   | YES   | YES     | TODO | ---   | YES
+    TOUCHES_MOVED       | ---     | ---   | ---   | YES   | YES     | TODO | ---   | YES
+    TOUCHES_ENDED       | ---     | ---   | ---   | YES   | YES     | TODO | ---   | YES
+    TOUCHES_CANCELLED   | ---     | ---   | ---   | YES   | YES     | TODO | ---   | YES
+    RESIZED             | YES     | YES   | YES   | YES   | YES     | YES  | ---   | YES
+    ICONIFIED           | YES     | YES   | YES   | ---   | ---     | YES  | ---   | ---
+    RESTORED            | YES     | YES   | YES   | ---   | ---     | YES  | ---   | ---
+    SUSPENDED           | ---     | ---   | ---   | YES   | YES     | YES  | ---   | TODO
+    RESUMED             | ---     | ---   | ---   | YES   | YES     | YES  | ---   | TODO
+    QUIT_REQUESTED      | YES     | YES   | YES   | ---   | ---     | ---  | TODO  | YES
+    UPDATE_CURSOR       | YES     | YES   | TODO  | ---   | ---     | TODO | ---   | TODO
+    IME                 | TODO    | TODO? | TODO  | ???   | TODO    | ---  | ???   | ???
+    key repeat flag     | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    windowed            | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | YES
+    fullscreen          | YES     | YES   | YES   | YES   | YES     | YES  | TODO  | ---
+    mouse hide          | YES     | YES   | YES   | ---   | ---     | YES  | TODO  | TODO
+    mouse lock          | YES     | YES   | YES   | ---   | ---     | TODO | TODO  | YES
+    screen keyboard     | ---     | ---   | ---   | YES   | TODO    | TODO | ---   | YES
+    swap interval       | YES     | YES   | YES   | YES   | TODO    | ---  | TODO  | YES
+    high-dpi            | YES     | YES   | TODO  | YES   | YES     | YES  | TODO  | YES
+    clipboard           | YES     | YES   | TODO  | ---   | ---     | TODO | ---   | YES
+    MSAA                | YES     | YES   | YES   | YES   | YES     | TODO | TODO  | YES
+    drag'n'drop         | YES     | YES   | YES   | ---   | ---     | TODO | TODO  | YES
+    window icon         | YES     | YES(1)| YES   | ---   | ---     | TODO | TODO  | YES
+
+    (1) macOS has no regular window icons, instead the dock icon is changed
+
+    STEP BY STEP
+    ============
+    --- Add a sokol_main() function to your code which returns a sapp_desc structure
+        with initialization parameters and callback function pointers. This
+        function is called very early, usually at the start of the
+        platform's entry function (e.g. main or WinMain). You should do as
+        little as possible here, since the rest of your code might be called
+        from another thread (this depends on the platform):
+
+            sapp_desc sokol_main(int argc, char* argv[]) {
+                return (sapp_desc) {
+                    .width = 640,
+                    .height = 480,
+                    .init_cb = my_init_func,
+                    .frame_cb = my_frame_func,
+                    .cleanup_cb = my_cleanup_func,
+                    .event_cb = my_event_func,
+                    ...
+                };
+            }
+
+        There are many more setup parameters, but these are the most important.
+        For a complete list search for the sapp_desc structure declaration
+        below.
+
+        DO NOT call any sokol-app function from inside sokol_main(), since
+        sokol-app will not be initialized at this point.
+
+        The .width and .height parameters are the preferred size of the 3D
+        rendering canvas. The actual size may differ from this depending on
+        platform and other circumstances. Also the canvas size may change at
+        any time (for instance when the user resizes the application window,
+        or rotates the mobile device).
+
+        All provided function callbacks will be called from the same thread,
+        but this may be different from the thread where sokol_main() was called.
+
+        .init_cb (void (*)(void))
+            This function is called once after the application window,
+            3D rendering context and swap chain have been created. The
+            function takes no arguments and has no return value.
+        .frame_cb (void (*)(void))
+            This is the per-frame callback, which is usually called 60
+            times per second. This is where your application would update
+            most of its state and perform all rendering.
+        .cleanup_cb (void (*)(void))
+            The cleanup callback is called once right before the application
+            quits.
+        .event_cb (void (*)(const sapp_event* event))
+            The event callback is mainly for input handling, but is also
+            used to communicate other types of events to the application. Keep the
+            event_cb struct member zero-initialized if your application doesn't require
+            event handling.
+        .fail_cb (void (*)(const char* msg))
+            The fail callback is called when a fatal error is encountered
+            during start which doesn't allow the program to continue.
+            Providing a callback here gives you a chance to show an error message
+            to the user. The default behaviour is SOKOL_LOG(msg)
+
+        As you can see, those 'standard callbacks' don't have a user_data
+        argument, so any data that needs to be preserved between callbacks
+        must live in global variables. If keeping state in global variables
+        is not an option, there's an alternative set of callbacks with
+        an additional user_data pointer argument:
+
+        .user_data (void*)
+            The user-data argument for the callbacks below
+        .init_userdata_cb (void (*)(void* user_data))
+        .frame_userdata_cb (void (*)(void* user_data))
+        .cleanup_userdata_cb (void (*)(void* user_data))
+        .event_cb (void(*)(const sapp_event* event, void* user_data))
+        .fail_cb (void(*)(const char* msg, void* user_data))
+            These are the user-data versions of the callback functions. You
+            can mix those with the standard callbacks that don't have the
+            user_data argument.
+
+        The function sapp_userdata() can be used to query the user_data
+        pointer provided in the sapp_desc struct.
+
+        You can also call sapp_query_desc() to get a copy of the
+        original sapp_desc structure.
+
+        NOTE that there's also an alternative compile mode where sokol_app.h
+        doesn't "hijack" the main() function. Search below for SOKOL_NO_ENTRY.
+
+    --- Implement the initialization callback function (init_cb), this is called
+        once after the rendering surface, 3D API and swap chain have been
+        initialized by sokol_app. All sokol-app functions can be called
+        from inside the initialization callback, the most useful functions
+        at this point are:
+
+        int sapp_width(void)
+        int sapp_height(void)
+            Returns the current width and height of the default framebuffer in pixels,
+            this may change from one frame to the next, and it may be different
+            from the initial size provided in the sapp_desc struct.
+
+        float sapp_widthf(void)
+        float sapp_heightf(void)
+            These are alternatives to sapp_width() and sapp_height() which return
+            the default framebuffer size as float values instead of integer. This
+            may help to prevent casting back and forth between int and float
+            in more strongly typed languages than C and C++.
+
+        int sapp_color_format(void)
+        int sapp_depth_format(void)
+            The color and depth-stencil pixelformats of the default framebuffer,
+            as integer values which are compatible with sokol-gfx's
+            sg_pixel_format enum (so that they can be plugged directly in places
+            where sg_pixel_format is expected). Possible values are:
+
+                23 == SG_PIXELFORMAT_RGBA8
+                27 == SG_PIXELFORMAT_BGRA8
+                41 == SG_PIXELFORMAT_DEPTH
+                42 == SG_PIXELFORMAT_DEPTH_STENCIL
+
+        int sapp_sample_count(void)
+            Return the MSAA sample count of the default framebuffer.
+
+        bool sapp_gles2(void)
+            Returns true if a GLES2 or WebGL context has been created. This
+            is useful when a GLES3/WebGL2 context was requested but is not
+            available so that sokol_app.h had to fallback to GLES2/WebGL.
+
+        const void* sapp_metal_get_device(void)
+        const void* sapp_metal_get_renderpass_descriptor(void)
+        const void* sapp_metal_get_drawable(void)
+            If the Metal backend has been selected, these functions return pointers
+            to various Metal API objects required for rendering, otherwise
+            they return a null pointer. These void pointers are actually
+            Objective-C ids converted with a (ARC) __bridge cast so that
+            the ids can be tunnel through C code. Also note that the returned
+            pointers to the renderpass-descriptor and drawable may change from one
+            frame to the next, only the Metal device object is guaranteed to
+            stay the same.
+
+        const void* sapp_macos_get_window(void)
+            On macOS, get the NSWindow object pointer, otherwise a null pointer.
+            Before being used as Objective-C object, the void* must be converted
+            back with a (ARC) __bridge cast.
+
+        const void* sapp_ios_get_window(void)
+            On iOS, get the UIWindow object pointer, otherwise a null pointer.
+            Before being used as Objective-C object, the void* must be converted
+            back with a (ARC) __bridge cast.
+
+        const void* sapp_win32_get_hwnd(void)
+            On Windows, get the window's HWND, otherwise a null pointer. The
+            HWND has been cast to a void pointer in order to be tunneled
+            through code which doesn't include Windows.h.
+
+        const void* sapp_d3d11_get_device(void)
+        const void* sapp_d3d11_get_device_context(void)
+        const void* sapp_d3d11_get_render_target_view(void)
+        const void* sapp_d3d11_get_depth_stencil_view(void)
+            Similar to the sapp_metal_* functions, the sapp_d3d11_* functions
+            return pointers to D3D11 API objects required for rendering,
+            only if the D3D11 backend has been selected. Otherwise they
+            return a null pointer. Note that the returned pointers to the
+            render-target-view and depth-stencil-view may change from one
+            frame to the next!
+
+        const void* sapp_wgpu_get_device(void)
+        const void* sapp_wgpu_get_render_view(void)
+        const void* sapp_wgpu_get_resolve_view(void)
+        const void* sapp_wgpu_get_depth_stencil_view(void)
+            These are the WebGPU-specific functions to get the WebGPU
+            objects and values required for rendering. If sokol_app.h
+            is not compiled with SOKOL_WGPU, these functions return null.
+
+        const void* sapp_android_get_native_activity(void);
+            On Android, get the native activity ANativeActivity pointer, otherwise
+            a null pointer.
+
+    --- Implement the frame-callback function, this function will be called
+        on the same thread as the init callback, but might be on a different
+        thread than the sokol_main() function. Note that the size of
+        the rendering framebuffer might have changed since the frame callback
+        was called last. Call the functions sapp_width() and sapp_height()
+        each frame to get the current size.
+
+    --- Optionally implement the event-callback to handle input events.
+        sokol-app provides the following type of input events:
+            - a 'virtual key' was pressed down or released
+            - a single text character was entered (provided as UTF-32 code point)
+            - a mouse button was pressed down or released (left, right, middle)
+            - mouse-wheel or 2D scrolling events
+            - the mouse was moved
+            - the mouse has entered or left the application window boundaries
+            - low-level, portable multi-touch events (began, moved, ended, cancelled)
+            - the application window was resized, iconified or restored
+            - the application was suspended or restored (on mobile platforms)
+            - the user or application code has asked to quit the application
+            - a string was pasted to the system clipboard
+            - one or more files have been dropped onto the application window
+
+        To explicitly 'consume' an event and prevent that the event is
+        forwarded for further handling to the operating system, call
+        sapp_consume_event() from inside the event handler (NOTE that
+        this behaviour is currently only implemented for some HTML5
+        events, support for other platforms and event types will
+        be added as needed, please open a github ticket and/or provide
+        a PR if needed).
+
+        NOTE: Do *not* call any 3D API rendering functions in the event
+        callback function, since the 3D API context may not be active when the
+        event callback is called (it may work on some platforms and 3D APIs,
+        but not others, and the exact behaviour may change between
+        sokol-app versions).
+
+    --- Implement the cleanup-callback function, this is called once
+        after the user quits the application (see the section
+        "APPLICATION QUIT" for detailed information on quitting
+        behaviour, and how to intercept a pending quit - for instance to show a
+        "Really Quit?" dialog box). Note that the cleanup-callback isn't
+        guaranteed to be called on the web and mobile platforms.
+
+    MOUSE LOCK (AKA POINTER LOCK, AKA MOUSE CAPTURE)
+    ================================================
+    In normal mouse mode, no mouse movement events are reported when the
+    mouse leaves the windows client area or hits the screen border (whether
+    it's one or the other depends on the platform), and the mouse move events
+    (SAPP_EVENTTYPE_MOUSE_MOVE) contain absolute mouse positions in
+    framebuffer pixels in the sapp_event items mouse_x and mouse_y, and
+    relative movement in framebuffer pixels in the sapp_event items mouse_dx
+    and mouse_dy.
+
+    To get continuous mouse movement (also when the mouse leaves the window
+    client area or hits the screen border), activate mouse-lock mode
+    by calling:
+
+        sapp_lock_mouse(true)
+
+    When mouse lock is activated, the mouse pointer is hidden, the
+    reported absolute mouse position (sapp_event.mouse_x/y) appears
+    frozen, and the relative mouse movement in sapp_event.mouse_dx/dy
+    no longer has a direct relation to framebuffer pixels but instead
+    uses "raw mouse input" (what "raw mouse input" exactly means also
+    differs by platform).
+
+    To deactivate mouse lock and return to normal mouse mode, call
+
+        sapp_lock_mouse(false)
+
+    And finally, to check if mouse lock is currently active, call
+
+        if (sapp_mouse_locked()) { ... }
+
+    On native platforms, the sapp_lock_mouse() and sapp_mouse_locked()
+    functions work as expected (mouse lock is activated or deactivated
+    immediately when sapp_lock_mouse() is called, and sapp_mouse_locked()
+    also immediately returns the new state after sapp_lock_mouse()
+    is called.
+
+    On the web platform, sapp_lock_mouse() and sapp_mouse_locked() behave
+    differently, as dictated by the limitations of the HTML5 Pointer Lock API:
+
+        - sapp_lock_mouse(true) can be called at any time, but it will
+          only take effect in a 'short-lived input event handler of a specific
+          type', meaning when one of the following events happens:
+            - SAPP_EVENTTYPE_MOUSE_DOWN
+            - SAPP_EVENTTYPE_MOUSE_UP
+            - SAPP_EVENTTYPE_MOUSE_SCROLL
+            - SAPP_EVENTYTPE_KEY_UP
+            - SAPP_EVENTTYPE_KEY_DOWN
+        - The mouse lock/unlock action on the web platform is asynchronous,
+          this means that sapp_mouse_locked() won't immediately return
+          the new status after calling sapp_lock_mouse(), instead the
+          reported status will only change when the pointer lock has actually
+          been activated or deactivated in the browser.
+        - On the web, mouse lock can be deactivated by the user at any time
+          by pressing the Esc key. When this happens, sokol_app.h behaves
+          the same as if sapp_lock_mouse(false) is called.
+
+    For things like camera manipulation it's most straightforward to lock
+    and unlock the mouse right from the sokol_app.h event handler, for
+    instance the following code enters and leaves mouse lock when the
+    left mouse button is pressed and released, and then uses the relative
+    movement information to manipulate a camera (taken from the
+    cgltf-sapp.c sample in the sokol-samples repository
+    at https://github.com/floooh/sokol-samples):
+
+        static void input(const sapp_event* ev) {
+            switch (ev->type) {
+                case SAPP_EVENTTYPE_MOUSE_DOWN:
+                    if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) {
+                        sapp_lock_mouse(true);
+                    }
+                    break;
+
+                case SAPP_EVENTTYPE_MOUSE_UP:
+                    if (ev->mouse_button == SAPP_MOUSEBUTTON_LEFT) {
+                        sapp_lock_mouse(false);
+                    }
+                    break;
+
+                case SAPP_EVENTTYPE_MOUSE_MOVE:
+                    if (sapp_mouse_locked()) {
+                        cam_orbit(&state.camera, ev->mouse_dx * 0.25f, ev->mouse_dy * 0.25f);
+                    }
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+    CLIPBOARD SUPPORT
+    =================
+    Applications can send and receive UTF-8 encoded text data from and to the
+    system clipboard. By default, clipboard support is disabled and
+    must be enabled at startup via the following sapp_desc struct
+    members:
+
+        sapp_desc.enable_clipboard  - set to true to enable clipboard support
+        sapp_desc.clipboard_size    - size of the internal clipboard buffer in bytes
+
+    Enabling the clipboard will dynamically allocate a clipboard buffer
+    for UTF-8 encoded text data of the requested size in bytes, the default
+    size is 8 KBytes. Strings that don't fit into the clipboard buffer
+    (including the terminating zero) will be silently clipped, so it's
+    important that you provide a big enough clipboard size for your
+    use case.
+
+    To send data to the clipboard, call sapp_set_clipboard_string() with
+    a pointer to an UTF-8 encoded, null-terminated C-string.
+
+    NOTE that on the HTML5 platform, sapp_set_clipboard_string() must be
+    called from inside a 'short-lived event handler', and there are a few
+    other HTML5-specific caveats to workaround. You'll basically have to
+    tinker until it works in all browsers :/ (maybe the situation will
+    improve when all browsers agree on and implement the new
+    HTML5 navigator.clipboard API).
+
+    To get data from the clipboard, check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED
+    event in your event handler function, and then call sapp_get_clipboard_string()
+    to obtain the pasted UTF-8 encoded text.
+
+    NOTE that behaviour of sapp_get_clipboard_string() is slightly different
+    depending on platform:
+
+        - on the HTML5 platform, the internal clipboard buffer will only be updated
+          right before the SAPP_EVENTTYPE_CLIPBOARD_PASTED event is sent,
+          and sapp_get_clipboard_string() will simply return the current content
+          of the clipboard buffer
+        - on 'native' platforms, the call to sapp_get_clipboard_string() will
+          update the internal clipboard buffer with the most recent data
+          from the system clipboard
+
+    Portable code should check for the SAPP_EVENTTYPE_CLIPBOARD_PASTED event,
+    and then call sapp_get_clipboard_string() right in the event handler.
+
+    The SAPP_EVENTTYPE_CLIPBOARD_PASTED event will be generated by sokol-app
+    as follows:
+
+        - on macOS: when the Cmd+V key is pressed down
+        - on HTML5: when the browser sends a 'paste' event to the global 'window' object
+        - on all other platforms: when the Ctrl+V key is pressed down
+
+    DRAG AND DROP SUPPORT
+    =====================
+    PLEASE NOTE: the drag'n'drop feature works differently on WASM/HTML5
+    and on the native desktop platforms (Win32, Linux and macOS) because
+    of security-related restrictions in the HTML5 drag'n'drop API. The
+    WASM/HTML5 specifics are described at the end of this documentation
+    section:
+
+    Like clipboard support, drag'n'drop support must be explicitly enabled
+    at startup in the sapp_desc struct.
+
+        sapp_desc sokol_main() {
+            return (sapp_desc) {
+                .enable_dragndrop = true,   // default is false
+                ...
+            };
+        }
+
+    You can also adjust the maximum number of files that are accepted
+    in a drop operation, and the maximum path length in bytes if needed:
+
+        sapp_desc sokol_main() {
+            return (sapp_desc) {
+                .enable_dragndrop = true,               // default is false
+                .max_dropped_files = 8,                 // default is 1
+                .max_dropped_file_path_length = 8192,   // in bytes, default is 2048
+                ...
+            };
+        }
+
+    When drag'n'drop is enabled, the event callback will be invoked with an
+    event of type SAPP_EVENTTYPE_FILES_DROPPED whenever the user drops files on
+    the application window.
+
+    After the SAPP_EVENTTYPE_FILES_DROPPED is received, you can query the
+    number of dropped files, and their absolute paths by calling separate
+    functions:
+
+        void on_event(const sapp_event* ev) {
+            if (ev->type == SAPP_EVENTTYPE_FILES_DROPPED) {
+
+                // the mouse position where the drop happened
+                float x = ev->mouse_x;
+                float y = ev->mouse_y;
+
+                // get the number of files and their paths like this:
+                const int num_dropped_files = sapp_get_num_dropped_files();
+                for (int i = 0; i < num_dropped_files; i++) {
+                    const char* path = sapp_get_dropped_file_path(i);
+                    ...
+                }
+            }
+        }
+
+    The returned file paths are UTF-8 encoded strings.
+
+    You can call sapp_get_num_dropped_files() and sapp_get_dropped_file_path()
+    anywhere, also outside the event handler callback, but be aware that the
+    file path strings will be overwritten with the next drop operation.
+
+    In any case, sapp_get_dropped_file_path() will never return a null pointer,
+    instead an empty string "" will be returned if the drag'n'drop feature
+    hasn't been enabled, the last drop-operation failed, or the file path index
+    is out of range.
+
+    Drag'n'drop caveats:
+
+        - if more files are dropped in a single drop-action
+          than sapp_desc.max_dropped_files, the additional
+          files will be silently ignored
+        - if any of the file paths is longer than
+          sapp_desc.max_dropped_file_path_length (in number of bytes, after UTF-8
+          encoding) the entire drop operation will be silently ignored (this
+          needs some sort of error feedback in the future)
+        - no mouse positions are reported while the drag is in
+          process, this may change in the future
+
+    Drag'n'drop on HTML5/WASM:
+
+    The HTML5 drag'n'drop API doesn't return file paths, but instead
+    black-box 'file objects' which must be used to load the content
+    of dropped files. This is the reason why sokol_app.h adds two
+    HTML5-specific functions to the drag'n'drop API:
+
+        uint32_t sapp_html5_get_dropped_file_size(int index)
+            Returns the size in bytes of a dropped file.
+
+        void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request)
+            Asynchronously loads the content of a dropped file into a
+            provided memory buffer (which must be big enough to hold
+            the file content)
+
+    To start loading the first dropped file after an SAPP_EVENTTYPE_FILES_DROPPED
+    event is received:
+
+        sapp_html5_fetch_dropped_file(&(sapp_html5_fetch_request){
+            .dropped_file_index = 0,
+            .callback = fetch_cb
+            .buffer_ptr = buf,
+            .buffer_size = buf_size,
+            .user_data = ...
+        });
+
+    Make sure that the memory pointed to by 'buf' stays valid until the
+    callback function is called!
+
+    As result of the asynchronous loading operation (no matter if succeeded or
+    failed) the 'fetch_cb' function will be called:
+
+        void fetch_cb(const sapp_html5_fetch_response* response) {
+            // IMPORTANT: check if the loading operation actually succeeded:
+            if (response->succeeded) {
+                // the size of the loaded file:
+                const uint32_t num_bytes = response->fetched_size;
+                // and the pointer to the data (same as 'buf' in the fetch-call):
+                const void* ptr = response->buffer_ptr;
+            }
+            else {
+                // on error check the error code:
+                switch (response->error_code) {
+                    case SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL:
+                        ...
+                        break;
+                    case SAPP_HTML5_FETCH_ERROR_OTHER:
+                        ...
+                        break;
+                }
+            }
+        }
+
+    Check the droptest-sapp example for a real-world example which works
+    both on native platforms and the web:
+
+    https://github.com/floooh/sokol-samples/blob/master/sapp/droptest-sapp.c
+
+    HIGH-DPI RENDERING
+    ==================
+    You can set the sapp_desc.high_dpi flag during initialization to request
+    a full-resolution framebuffer on HighDPI displays. The default behaviour
+    is sapp_desc.high_dpi=false, this means that the application will
+    render to a lower-resolution framebuffer on HighDPI displays and the
+    rendered content will be upscaled by the window system composer.
+
+    In a HighDPI scenario, you still request the same window size during
+    sokol_main(), but the framebuffer sizes returned by sapp_width()
+    and sapp_height() will be scaled up according to the DPI scaling
+    ratio. You can also get a DPI scaling factor with the function
+    sapp_dpi_scale().
+
+    Here's an example on a Mac with Retina display:
+
+    sapp_desc sokol_main() {
+        return (sapp_desc) {
+            .width = 640,
+            .height = 480,
+            .high_dpi = true,
+            ...
+        };
+    }
+
+    The functions sapp_width(), sapp_height() and sapp_dpi_scale() will
+    return the following values:
+
+    sapp_width      -> 1280
+    sapp_height     -> 960
+    sapp_dpi_scale  -> 2.0
+
+    If the high_dpi flag is false, or you're not running on a Retina display,
+    the values would be:
+
+    sapp_width      -> 640
+    sapp_height     -> 480
+    sapp_dpi_scale  -> 1.0
+
+    APPLICATION QUIT
+    ================
+    Without special quit handling, a sokol_app.h application will quit
+    'gracefully' when the user clicks the window close-button unless a
+    platform's application model prevents this (e.g. on web or mobile).
+    'Graceful exit' means that the application-provided cleanup callback will
+    be called before the application quits.
+
+    On native desktop platforms sokol_app.h provides more control over the
+    application-quit-process. It's possible to initiate a 'programmatic quit'
+    from the application code, and a quit initiated by the application user can
+    be intercepted (for instance to show a custom dialog box).
+
+    This 'programmatic quit protocol' is implemented through 3 functions
+    and 1 event:
+
+        - sapp_quit(): This function simply quits the application without
+          giving the user a chance to intervene. Usually this might
+          be called when the user clicks the 'Ok' button in a 'Really Quit?'
+          dialog box
+        - sapp_request_quit(): Calling sapp_request_quit() will send the
+          event SAPP_EVENTTYPE_QUIT_REQUESTED to the applications event handler
+          callback, giving the user code a chance to intervene and cancel the
+          pending quit process (for instance to show a 'Really Quit?' dialog
+          box). If the event handler callback does nothing, the application
+          will be quit as usual. To prevent this, call the function
+          sapp_cancel_quit() from inside the event handler.
+        - sapp_cancel_quit(): Cancels a pending quit request, either initiated
+          by the user clicking the window close button, or programmatically
+          by calling sapp_request_quit(). The only place where calling this
+          function makes sense is from inside the event handler callback when
+          the SAPP_EVENTTYPE_QUIT_REQUESTED event has been received.
+        - SAPP_EVENTTYPE_QUIT_REQUESTED: this event is sent when the user
+          clicks the window's close button or application code calls the
+          sapp_request_quit() function. The event handler callback code can handle
+          this event by calling sapp_cancel_quit() to cancel the quit.
+          If the event is ignored, the application will quit as usual.
+
+    On the web platform, the quit behaviour differs from native platforms,
+    because of web-specific restrictions:
+
+    A `programmatic quit` initiated by calling sapp_quit() or
+    sapp_request_quit() will work as described above: the cleanup callback is
+    called, platform-specific cleanup is performed (on the web
+    this means that JS event handlers are unregisters), and then
+    the request-animation-loop will be exited. However that's all. The
+    web page itself will continue to exist (e.g. it's not possible to
+    programmatically close the browser tab).
+
+    On the web it's also not possible to run custom code when the user
+    closes a brower tab, so it's not possible to prevent this with a
+    fancy custom dialog box.
+
+    Instead the standard "Leave Site?" dialog box can be activated (or
+    deactivated) with the following function:
+
+        sapp_html5_ask_leave_site(bool ask);
+
+    The initial state of the associated internal flag can be provided
+    at startup via sapp_desc.html5_ask_leave_site.
+
+    This feature should only be used sparingly in critical situations - for
+    instance when the user would loose data - since popping up modal dialog
+    boxes is considered quite rude in the web world. Note that there's no way
+    to customize the content of this dialog box or run any code as a result
+    of the user's decision. Also note that the user must have interacted with
+    the site before the dialog box will appear. These are all security measures
+    to prevent fishing.
+
+    The Dear ImGui HighDPI sample contains example code of how to
+    implement a 'Really Quit?' dialog box with Dear ImGui (native desktop
+    platforms only), and for showing the hardwired "Leave Site?" dialog box
+    when running on the web platform:
+
+        https://floooh.github.io/sokol-html5/wasm/imgui-highdpi-sapp.html
+
+    FULLSCREEN
+    ==========
+    If the sapp_desc.fullscreen flag is true, sokol-app will try to create
+    a fullscreen window on platforms with a 'proper' window system
+    (mobile devices will always use fullscreen). The implementation details
+    depend on the target platform, in general sokol-app will use a
+    'soft approach' which doesn't interfere too much with the platform's
+    window system (for instance borderless fullscreen window instead of
+    a 'real' fullscreen mode). Such details might change over time
+    as sokol-app is adapted for different needs.
+
+    The most important effect of fullscreen mode to keep in mind is that
+    the requested canvas width and height will be ignored for the initial
+    window size, calling sapp_width() and sapp_height() will instead return
+    the resolution of the fullscreen canvas (however the provided size
+    might still be used for the non-fullscreen window, in case the user can
+    switch back from fullscreen- to windowed-mode).
+
+    To toggle fullscreen mode programmatically, call sapp_toggle_fullscreen().
+
+    To check if the application window is currently in fullscreen mode,
+    call sapp_is_fullscreen().
+
+    WINDOW ICON SUPPORT
+    ===================
+    Some sokol_app.h backends allow to change the window icon programmatically:
+
+        - on Win32: the small icon in the window's title bar, and the
+          bigger icon in the task bar
+        - on Linux: highly dependent on the used window manager, but usually
+          the window's title bar icon and/or the task bar icon
+        - on HTML5: the favicon shown in the page's browser tab
+
+    NOTE that it is not possible to set the actual application icon which is
+    displayed by the operating system on the desktop or 'home screen'. Those
+    icons must be provided 'traditionally' through operating-system-specific
+    resources which are associated with the application (sokol_app.h might
+    later support setting the window icon from platform specific resource data
+    though).
+
+    There are two ways to set the window icon:
+
+        - at application start in the sokol_main() function by initializing
+          the sapp_desc.icon nested struct
+        - or later by calling the function sapp_set_icon()
+
+    As a convenient shortcut, sokol_app.h comes with a builtin default-icon
+    (a rainbow-colored 'S', which at least looks a bit better than the Windows
+    default icon for applications), which can be activated like this:
+
+    At startup in sokol_main():
+
+        sapp_desc sokol_main(...) {
+            return (sapp_desc){
+                ...
+                icon.sokol_default = true
+            };
+        }
+
+    Or later by calling:
+
+        sapp_set_icon(&(sapp_icon_desc){ .sokol_default = true });
+
+    NOTE that a completely zero-initialized sapp_icon_desc struct will not
+    update the window icon in any way. This is an 'escape hatch' so that you
+    can handle the window icon update yourself (or if you do this already,
+    sokol_app.h won't get in your way, in this case just leave the
+    sapp_desc.icon struct zero-initialized).
+
+    Providing your own icon images works exactly like in GLFW (down to the
+    data format):
+
+    You provide one or more 'candidate images' in different sizes, and the
+    sokol_app.h platform backends pick the best match for the specific backend
+    and icon type.
+
+    For each candidate image, you need to provide:
+
+        - the width in pixels
+        - the height in pixels
+        - and the actual pixel data in RGBA8 pixel format (e.g. 0xFFCC8844
+          on a little-endian CPU means: alpha=0xFF, blue=0xCC, green=0x88, red=0x44)
+
+    For instance, if you have 3 candidate images (small, medium, big) of
+    sizes 16x16, 32x32 and 64x64 the corresponding sapp_icon_desc struct is setup
+    like this:
+
+        // the actual pixel data (RGBA8, origin top-left)
+        const uint32_t small[16][16]  = { ... };
+        const uint32_t medium[32][32] = { ... };
+        const uint32_t big[64][64]    = { ... };
+
+        const sapp_icon_desc icon_desc = {
+            .images = {
+                { .width = 16, .height = 16, .pixels = SAPP_RANGE(small) },
+                { .width = 32, .height = 32, .pixels = SAPP_RANGE(medium) },
+                // ...or without the SAPP_RANGE helper macro:
+                { .width = 64, .height = 64, .pixels = { .ptr=big, .size=sizeof(big) } }
+            }
+        };
+
+    An sapp_icon_desc struct initialized like this can then either be applied
+    at application start in sokol_main:
+
+        sapp_desc sokol_main(...) {
+            return (sapp_desc){
+                ...
+                icon = icon_desc
+            };
+        }
+
+    ...or later by calling sapp_set_icon():
+
+        sapp_set_icon(&icon_desc);
+
+    Some window icon caveats:
+
+        - once the window icon has been updated, there's no way to go back to
+          the platform's default icon, this is because some platforms (Linux
+          and HTML5) don't switch the icon visual back to the default even if
+          the custom icon is deleted or removed
+        - on HTML5, if the sokol_app.h icon doesn't show up in the browser
+          tab, check that there's no traditional favicon 'link' element
+          is defined in the page's index.html, sokol_app.h will only
+          append a new favicon link element, but not delete any manually
+          defined favicon in the page
+
+    For an example and test of the window icon feature, check out the the
+    'icon-sapp' sample on the sokol-samples git repository.
+
+    ONSCREEN KEYBOARD
+    =================
+    On some platforms which don't provide a physical keyboard, sokol-app
+    can display the platform's integrated onscreen keyboard for text
+    input. To request that the onscreen keyboard is shown, call
+
+        sapp_show_keyboard(true);
+
+    Likewise, to hide the keyboard call:
+
+        sapp_show_keyboard(false);
+
+    Note that on the web platform, the keyboard can only be shown from
+    inside an input handler. On such platforms, sapp_show_keyboard()
+    will only work as expected when it is called from inside the
+    sokol-app event callback function. When called from other places,
+    an internal flag will be set, and the onscreen keyboard will be
+    called at the next 'legal' opportunity (when the next input event
+    is handled).
+
+    OPTIONAL: DON'T HIJACK main() (#define SOKOL_NO_ENTRY)
+    ======================================================
+    In its default configuration, sokol_app.h "hijacks" the platform's
+    standard main() function. This was done because different platforms
+    have different main functions which are not compatible with
+    C's main() (for instance WinMain on Windows has completely different
+    arguments). However, this "main hijacking" posed a problem for
+    usage scenarios like integrating sokol_app.h with other languages than
+    C or C++, so an alternative SOKOL_NO_ENTRY mode has been added
+    in which the user code provides the platform's main function:
+
+    - define SOKOL_NO_ENTRY before including the sokol_app.h implementation
+    - do *not* provide a sokol_main() function
+    - instead provide the standard main() function of the platform
+    - from the main function, call the function ```sapp_run()``` which
+      takes a pointer to an ```sapp_desc``` structure.
+    - ```sapp_run()``` takes over control and calls the provided init-, frame-,
+      shutdown- and event-callbacks just like in the default model, it
+      will only return when the application quits (or not at all on some
+      platforms, like emscripten)
+
+    NOTE: SOKOL_NO_ENTRY is currently not supported on Android.
+
+    WINDOWS CONSOLE OUTPUT
+    ======================
+    On Windows, regular windowed applications don't show any stdout/stderr text
+    output, which can be a bit of a hassle for printf() debugging or generally
+    logging text to the console. Also, console output by default uses a local
+    codepage setting and thus international UTF-8 encoded text is printed
+    as garbage.
+
+    To help with these issues, sokol_app.h can be configured at startup
+    via the following Windows-specific sapp_desc flags:
+
+        sapp_desc.win32_console_utf8 (default: false)
+            When set to true, the output console codepage will be switched
+            to UTF-8 (and restored to the original codepage on exit)
+
+        sapp_desc.win32_console_attach (default: false)
+            When set to true, stdout and stderr will be attached to the
+            console of the parent process (if the parent process actually
+            has a console). This means that if the application was started
+            in a command line window, stdout and stderr output will be printed
+            to the terminal, just like a regular command line program. But if
+            the application is started via double-click, it will behave like
+            a regular UI application, and stdout/stderr will not be visible.
+
+        sapp_desc.win32_console_create (default: false)
+            When set to true, a new console window will be created and
+            stdout/stderr will be redirected to that console window. It
+            doesn't matter if the application is started from the command
+            line or via double-click.
+
+    TEMP NOTE DUMP
+    ==============
+    - onscreen keyboard support on Android requires Java :(, should we even bother?
+    - sapp_desc needs a bool whether to initialize depth-stencil surface
+    - GL context initialization needs more control (at least what GL version to initialize)
+    - application icon
+    - the UPDATE_CURSOR event currently behaves differently between Win32 and OSX
+      (Win32 sends the event each frame when the mouse moves and is inside the window
+      client area, OSX sends it only once when the mouse enters the client area)
+    - the Android implementation calls cleanup_cb() and destroys the egl context in onDestroy
+      at the latest but should do it earlier, in onStop, as an app is "killable" after onStop
+      on Android Honeycomb and later (it can't be done at the moment as the app may be started
+      again after onStop and the sokol lifecycle does not yet handle context teardown/bringup)
+
+
+    LICENSE
+    =======
+    zlib/libpng license
+
+    Copyright (c) 2018 Andre Weissflog
+
+    This software is provided 'as-is', without any express or implied warranty.
+    In no event will the authors be held liable for any damages arising from the
+    use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+        1. The origin of this software must not be misrepresented; you must not
+        claim that you wrote the original software. If you use this software in a
+        product, an acknowledgment in the product documentation would be
+        appreciated but is not required.
+
+        2. Altered source versions must be plainly marked as such, and must not
+        be misrepresented as being the original software.
+
+        3. This notice may not be removed or altered from any source
+        distribution.
+*/
+#define SOKOL_APP_INCLUDED (1)
+#include <stddef.h> // size_t
+#include <stdint.h>
+#include <stdbool.h>
+
+#if defined(SOKOL_API_DECL) && !defined(SOKOL_APP_API_DECL)
+#define SOKOL_APP_API_DECL SOKOL_API_DECL
+#endif
+#ifndef SOKOL_APP_API_DECL
+#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_APP_IMPL)
+#define SOKOL_APP_API_DECL __declspec(dllexport)
+#elif defined(_WIN32) && defined(SOKOL_DLL)
+#define SOKOL_APP_API_DECL __declspec(dllimport)
+#else
+#define SOKOL_APP_API_DECL extern
+#endif
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* misc constants */
+enum {
+    SAPP_MAX_TOUCHPOINTS = 8,
+    SAPP_MAX_MOUSEBUTTONS = 3,
+    SAPP_MAX_KEYCODES = 512,
+    SAPP_MAX_ICONIMAGES = 8,
+};
+
+/*
+    sapp_event_type
+
+    The type of event that's passed to the event handler callback
+    in the sapp_event.type field. These are not just "traditional"
+    input events, but also notify the application about state changes
+    or other user-invoked actions.
+*/
+typedef enum sapp_event_type {
+    SAPP_EVENTTYPE_INVALID,
+    SAPP_EVENTTYPE_KEY_DOWN,
+    SAPP_EVENTTYPE_KEY_UP,
+    SAPP_EVENTTYPE_CHAR,
+    SAPP_EVENTTYPE_MOUSE_DOWN,
+    SAPP_EVENTTYPE_MOUSE_UP,
+    SAPP_EVENTTYPE_MOUSE_SCROLL,
+    SAPP_EVENTTYPE_MOUSE_MOVE,
+    SAPP_EVENTTYPE_MOUSE_ENTER,
+    SAPP_EVENTTYPE_MOUSE_LEAVE,
+    SAPP_EVENTTYPE_TOUCHES_BEGAN,
+    SAPP_EVENTTYPE_TOUCHES_MOVED,
+    SAPP_EVENTTYPE_TOUCHES_ENDED,
+    SAPP_EVENTTYPE_TOUCHES_CANCELLED,
+    SAPP_EVENTTYPE_RESIZED,
+    SAPP_EVENTTYPE_ICONIFIED,
+    SAPP_EVENTTYPE_RESTORED,
+    SAPP_EVENTTYPE_SUSPENDED,
+    SAPP_EVENTTYPE_RESUMED,
+    SAPP_EVENTTYPE_UPDATE_CURSOR,
+    SAPP_EVENTTYPE_QUIT_REQUESTED,
+    SAPP_EVENTTYPE_CLIPBOARD_PASTED,
+    SAPP_EVENTTYPE_FILES_DROPPED,
+    _SAPP_EVENTTYPE_NUM,
+    _SAPP_EVENTTYPE_FORCE_U32 = 0x7FFFFFFF
+} sapp_event_type;
+
+/*
+    sapp_keycode
+
+    The 'virtual keycode' of a KEY_DOWN or KEY_UP event in the
+    struct field sapp_event.key_code.
+
+    Note that the keycode values are identical with GLFW.
+*/
+typedef enum sapp_keycode {
+    SAPP_KEYCODE_INVALID          = 0,
+    SAPP_KEYCODE_SPACE            = 32,
+    SAPP_KEYCODE_APOSTROPHE       = 39,  /* ' */
+    SAPP_KEYCODE_COMMA            = 44,  /* , */
+    SAPP_KEYCODE_MINUS            = 45,  /* - */
+    SAPP_KEYCODE_PERIOD           = 46,  /* . */
+    SAPP_KEYCODE_SLASH            = 47,  /* / */
+    SAPP_KEYCODE_0                = 48,
+    SAPP_KEYCODE_1                = 49,
+    SAPP_KEYCODE_2                = 50,
+    SAPP_KEYCODE_3                = 51,
+    SAPP_KEYCODE_4                = 52,
+    SAPP_KEYCODE_5                = 53,
+    SAPP_KEYCODE_6                = 54,
+    SAPP_KEYCODE_7                = 55,
+    SAPP_KEYCODE_8                = 56,
+    SAPP_KEYCODE_9                = 57,
+    SAPP_KEYCODE_SEMICOLON        = 59,  /* ; */
+    SAPP_KEYCODE_EQUAL            = 61,  /* = */
+    SAPP_KEYCODE_A                = 65,
+    SAPP_KEYCODE_B                = 66,
+    SAPP_KEYCODE_C                = 67,
+    SAPP_KEYCODE_D                = 68,
+    SAPP_KEYCODE_E                = 69,
+    SAPP_KEYCODE_F                = 70,
+    SAPP_KEYCODE_G                = 71,
+    SAPP_KEYCODE_H                = 72,
+    SAPP_KEYCODE_I                = 73,
+    SAPP_KEYCODE_J                = 74,
+    SAPP_KEYCODE_K                = 75,
+    SAPP_KEYCODE_L                = 76,
+    SAPP_KEYCODE_M                = 77,
+    SAPP_KEYCODE_N                = 78,
+    SAPP_KEYCODE_O                = 79,
+    SAPP_KEYCODE_P                = 80,
+    SAPP_KEYCODE_Q                = 81,
+    SAPP_KEYCODE_R                = 82,
+    SAPP_KEYCODE_S                = 83,
+    SAPP_KEYCODE_T                = 84,
+    SAPP_KEYCODE_U                = 85,
+    SAPP_KEYCODE_V                = 86,
+    SAPP_KEYCODE_W                = 87,
+    SAPP_KEYCODE_X                = 88,
+    SAPP_KEYCODE_Y                = 89,
+    SAPP_KEYCODE_Z                = 90,
+    SAPP_KEYCODE_LEFT_BRACKET     = 91,  /* [ */
+    SAPP_KEYCODE_BACKSLASH        = 92,  /* \ */
+    SAPP_KEYCODE_RIGHT_BRACKET    = 93,  /* ] */
+    SAPP_KEYCODE_GRAVE_ACCENT     = 96,  /* ` */
+    SAPP_KEYCODE_WORLD_1          = 161, /* non-US #1 */
+    SAPP_KEYCODE_WORLD_2          = 162, /* non-US #2 */
+    SAPP_KEYCODE_ESCAPE           = 256,
+    SAPP_KEYCODE_ENTER            = 257,
+    SAPP_KEYCODE_TAB              = 258,
+    SAPP_KEYCODE_BACKSPACE        = 259,
+    SAPP_KEYCODE_INSERT           = 260,
+    SAPP_KEYCODE_DELETE           = 261,
+    SAPP_KEYCODE_RIGHT            = 262,
+    SAPP_KEYCODE_LEFT             = 263,
+    SAPP_KEYCODE_DOWN             = 264,
+    SAPP_KEYCODE_UP               = 265,
+    SAPP_KEYCODE_PAGE_UP          = 266,
+    SAPP_KEYCODE_PAGE_DOWN        = 267,
+    SAPP_KEYCODE_HOME             = 268,
+    SAPP_KEYCODE_END              = 269,
+    SAPP_KEYCODE_CAPS_LOCK        = 280,
+    SAPP_KEYCODE_SCROLL_LOCK      = 281,
+    SAPP_KEYCODE_NUM_LOCK         = 282,
+    SAPP_KEYCODE_PRINT_SCREEN     = 283,
+    SAPP_KEYCODE_PAUSE            = 284,
+    SAPP_KEYCODE_F1               = 290,
+    SAPP_KEYCODE_F2               = 291,
+    SAPP_KEYCODE_F3               = 292,
+    SAPP_KEYCODE_F4               = 293,
+    SAPP_KEYCODE_F5               = 294,
+    SAPP_KEYCODE_F6               = 295,
+    SAPP_KEYCODE_F7               = 296,
+    SAPP_KEYCODE_F8               = 297,
+    SAPP_KEYCODE_F9               = 298,
+    SAPP_KEYCODE_F10              = 299,
+    SAPP_KEYCODE_F11              = 300,
+    SAPP_KEYCODE_F12              = 301,
+    SAPP_KEYCODE_F13              = 302,
+    SAPP_KEYCODE_F14              = 303,
+    SAPP_KEYCODE_F15              = 304,
+    SAPP_KEYCODE_F16              = 305,
+    SAPP_KEYCODE_F17              = 306,
+    SAPP_KEYCODE_F18              = 307,
+    SAPP_KEYCODE_F19              = 308,
+    SAPP_KEYCODE_F20              = 309,
+    SAPP_KEYCODE_F21              = 310,
+    SAPP_KEYCODE_F22              = 311,
+    SAPP_KEYCODE_F23              = 312,
+    SAPP_KEYCODE_F24              = 313,
+    SAPP_KEYCODE_F25              = 314,
+    SAPP_KEYCODE_KP_0             = 320,
+    SAPP_KEYCODE_KP_1             = 321,
+    SAPP_KEYCODE_KP_2             = 322,
+    SAPP_KEYCODE_KP_3             = 323,
+    SAPP_KEYCODE_KP_4             = 324,
+    SAPP_KEYCODE_KP_5             = 325,
+    SAPP_KEYCODE_KP_6             = 326,
+    SAPP_KEYCODE_KP_7             = 327,
+    SAPP_KEYCODE_KP_8             = 328,
+    SAPP_KEYCODE_KP_9             = 329,
+    SAPP_KEYCODE_KP_DECIMAL       = 330,
+    SAPP_KEYCODE_KP_DIVIDE        = 331,
+    SAPP_KEYCODE_KP_MULTIPLY      = 332,
+    SAPP_KEYCODE_KP_SUBTRACT      = 333,
+    SAPP_KEYCODE_KP_ADD           = 334,
+    SAPP_KEYCODE_KP_ENTER         = 335,
+    SAPP_KEYCODE_KP_EQUAL         = 336,
+    SAPP_KEYCODE_LEFT_SHIFT       = 340,
+    SAPP_KEYCODE_LEFT_CONTROL     = 341,
+    SAPP_KEYCODE_LEFT_ALT         = 342,
+    SAPP_KEYCODE_LEFT_SUPER       = 343,
+    SAPP_KEYCODE_RIGHT_SHIFT      = 344,
+    SAPP_KEYCODE_RIGHT_CONTROL    = 345,
+    SAPP_KEYCODE_RIGHT_ALT        = 346,
+    SAPP_KEYCODE_RIGHT_SUPER      = 347,
+    SAPP_KEYCODE_MENU             = 348,
+} sapp_keycode;
+
+/*
+    sapp_touchpoint
+
+    Describes a single touchpoint in a multitouch event (TOUCHES_BEGAN,
+    TOUCHES_MOVED, TOUCHES_ENDED).
+
+    Touch points are stored in the nested array sapp_event.touches[],
+    and the number of touches is stored in sapp_event.num_touches.
+*/
+typedef struct sapp_touchpoint {
+    uintptr_t identifier;
+    float pos_x;
+    float pos_y;
+    bool changed;
+} sapp_touchpoint;
+
+/*
+    sapp_mousebutton
+
+    The currently pressed mouse button in the events MOUSE_DOWN
+    and MOUSE_UP, stored in the struct field sapp_event.mouse_button.
+*/
+typedef enum sapp_mousebutton {
+    SAPP_MOUSEBUTTON_LEFT = 0x0,
+    SAPP_MOUSEBUTTON_RIGHT = 0x1,
+    SAPP_MOUSEBUTTON_MIDDLE = 0x2,
+    SAPP_MOUSEBUTTON_INVALID = 0x100,
+} sapp_mousebutton;
+
+/*
+    These are currently pressed modifier keys (and mouse buttons) which are
+    passed in the event struct field sapp_event.modifiers.
+*/
+enum {
+    SAPP_MODIFIER_SHIFT = 0x1,      // left or right shift key
+    SAPP_MODIFIER_CTRL  = 0x2,      // left or right control key
+    SAPP_MODIFIER_ALT   = 0x4,      // left or right alt key
+    SAPP_MODIFIER_SUPER = 0x8,      // left or right 'super' key
+    SAPP_MODIFIER_LMB   = 0x100,    // left mouse button
+    SAPP_MODIFIER_RMB   = 0x200,    // right mouse button
+    SAPP_MODIFIER_MMB   = 0x400,    // middle mouse button
+};
+
+/*
+    sapp_event
+
+    This is an all-in-one event struct passed to the event handler
+    user callback function. Note that it depends on the event
+    type what struct fields actually contain useful values, so you
+    should first check the event type before reading other struct
+    fields.
+*/
+typedef struct sapp_event {
+    uint64_t frame_count;               // current frame counter, always valid, useful for checking if two events were issued in the same frame
+    sapp_event_type type;               // the event type, always valid
+    sapp_keycode key_code;              // the virtual key code, only valid in KEY_UP, KEY_DOWN
+    uint32_t char_code;                 // the UTF-32 character code, only valid in CHAR events
+    bool key_repeat;                    // true if this is a key-repeat event, valid in KEY_UP, KEY_DOWN and CHAR
+    uint32_t modifiers;                 // current modifier keys, valid in all key-, char- and mouse-events
+    sapp_mousebutton mouse_button;      // mouse button that was pressed or released, valid in MOUSE_DOWN, MOUSE_UP
+    float mouse_x;                      // current horizontal mouse position in pixels, always valid except during mouse lock
+    float mouse_y;                      // current vertical mouse position in pixels, always valid except during mouse lock
+    float mouse_dx;                     // relative horizontal mouse movement since last frame, always valid
+    float mouse_dy;                     // relative vertical mouse movement since last frame, always valid
+    float scroll_x;                     // horizontal mouse wheel scroll distance, valid in MOUSE_SCROLL events
+    float scroll_y;                     // vertical mouse wheel scroll distance, valid in MOUSE_SCROLL events
+    int num_touches;                    // number of valid items in the touches[] array
+    sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS];  // current touch points, valid in TOUCHES_BEGIN, TOUCHES_MOVED, TOUCHES_ENDED
+    int window_width;                   // current window- and framebuffer sizes in pixels, always valid
+    int window_height;
+    int framebuffer_width;              // = window_width * dpi_scale
+    int framebuffer_height;             // = window_height * dpi_scale
+} sapp_event;
+
+/*
+    sg_range
+
+    A general pointer/size-pair struct and constructor macros for passing binary blobs
+    into sokol_app.h.
+*/
+typedef struct sapp_range {
+    const void* ptr;
+    size_t size;
+} sapp_range;
+// disabling this for every includer isn't great, but the warnings are also quite pointless
+#if defined(_MSC_VER)
+#pragma warning(disable:4221)   /* /W4 only: nonstandard extension used: 'x': cannot be initialized using address of automatic variable 'y' */
+#pragma warning(disable:4204)   /* VS2015: nonstandard extension used: non-constant aggregate initializer */
+#endif
+#if defined(__cplusplus)
+#define SAPP_RANGE(x) sapp_range{ &x, sizeof(x) }
+#else
+#define SAPP_RANGE(x) (sapp_range){ &x, sizeof(x) }
+#endif
+
+/*
+    sapp_image_desc
+
+    This is used to describe image data to sokol_app.h (at first, window
+    icons, later maybe cursor images).
+
+    Note that the actual image pixel format depends on the use case:
+
+    - window icon pixels are RGBA8
+    - cursor images are ??? (FIXME)
+*/
+typedef struct sapp_image_desc {
+    int width;
+    int height;
+    sapp_range pixels;
+} sapp_image_desc;
+
+/*
+    sapp_icon_desc
+
+    An icon description structure for use in sapp_desc.icon and
+    sapp_set_icon().
+
+    When setting a custom image, the application can provide a number of
+    candidates differing in size, and sokol_app.h will pick the image(s)
+    closest to the size expected by the platform's window system.
+
+    To set sokol-app's default icon, set .sokol_default to true.
+
+    Otherwise provide candidate images of different sizes in the
+    images[] array.
+
+    If both the sokol_default flag is set to true, any image candidates
+    will be ignored and the sokol_app.h default icon will be set.
+*/
+typedef struct sapp_icon_desc {
+    bool sokol_default;
+    sapp_image_desc images[SAPP_MAX_ICONIMAGES];
+} sapp_icon_desc;
+
+
+typedef struct sapp_desc {
+    void (*init_cb)(void);                  // these are the user-provided callbacks without user data
+    void (*frame_cb)(void);
+    void (*cleanup_cb)(void);
+    void (*event_cb)(const sapp_event*);
+    void (*fail_cb)(const char*);
+
+    void* user_data;                        // these are the user-provided callbacks with user data
+    void (*init_userdata_cb)(void*);
+    void (*frame_userdata_cb)(void*);
+    void (*cleanup_userdata_cb)(void*);
+    void (*event_userdata_cb)(const sapp_event*, void*);
+    void (*fail_userdata_cb)(const char*, void*);
+
+    int width;                          // the preferred width of the window / canvas
+    int height;                         // the preferred height of the window / canvas
+    int sample_count;                   // MSAA sample count
+    int swap_interval;                  // the preferred swap interval (ignored on some platforms)
+    bool high_dpi;                      // whether the rendering canvas is full-resolution on HighDPI displays
+    bool fullscreen;                    // whether the window should be created in fullscreen mode
+    bool alpha;                         // whether the framebuffer should have an alpha channel (ignored on some platforms)
+    const char* window_title;           // the window title as UTF-8 encoded string
+    bool user_cursor;                   // if true, user is expected to manage cursor image in SAPP_EVENTTYPE_UPDATE_CURSOR
+    bool enable_clipboard;              // enable clipboard access, default is false
+    int clipboard_size;                 // max size of clipboard content in bytes
+    bool enable_dragndrop;              // enable file dropping (drag'n'drop), default is false
+    int max_dropped_files;              // max number of dropped files to process (default: 1)
+    int max_dropped_file_path_length;   // max length in bytes of a dropped UTF-8 file path (default: 2048)
+    sapp_icon_desc icon;                // the initial window icon to set
+
+    /* backend-specific options */
+    bool gl_force_gles2;                // if true, setup GLES2/WebGL even if GLES3/WebGL2 is available
+    bool win32_console_utf8;            // if true, set the output console codepage to UTF-8
+    bool win32_console_create;          // if true, attach stdout/stderr to a new console window
+    bool win32_console_attach;          // if true, attach stdout/stderr to parent process
+    const char* html5_canvas_name;      // the name (id) of the HTML5 canvas element, default is "canvas"
+    bool html5_canvas_resize;           // if true, the HTML5 canvas size is set to sapp_desc.width/height, otherwise canvas size is tracked
+    bool html5_preserve_drawing_buffer; // HTML5 only: whether to preserve default framebuffer content between frames
+    bool html5_premultiplied_alpha;     // HTML5 only: whether the rendered pixels use premultiplied alpha convention
+    bool html5_ask_leave_site;          // initial state of the internal html5_ask_leave_site flag (see sapp_html5_ask_leave_site())
+    bool ios_keyboard_resizes_canvas;   // if true, showing the iOS keyboard shrinks the canvas
+} sapp_desc;
+
+/* HTML5 specific: request and response structs for
+   asynchronously loading dropped-file content.
+*/
+typedef enum sapp_html5_fetch_error {
+    SAPP_HTML5_FETCH_ERROR_NO_ERROR,
+    SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL,
+    SAPP_HTML5_FETCH_ERROR_OTHER,
+} sapp_html5_fetch_error;
+
+typedef struct sapp_html5_fetch_response {
+    bool succeeded;         /* true if the loading operation has succeeded */
+    sapp_html5_fetch_error error_code;
+    int file_index;         /* index of the dropped file (0..sapp_get_num_dropped_filed()-1) */
+    uint32_t fetched_size;  /* size in bytes of loaded data */
+    void* buffer_ptr;       /* pointer to user-provided buffer which contains the loaded data */
+    uint32_t buffer_size;   /* size of user-provided buffer (buffer_size >= fetched_size) */
+    void* user_data;        /* user-provided user data pointer */
+} sapp_html5_fetch_response;
+
+typedef struct sapp_html5_fetch_request {
+    int dropped_file_index;                 /* 0..sapp_get_num_dropped_files()-1 */
+    void (*callback)(const sapp_html5_fetch_response*);     /* response callback function pointer (required) */
+    void* buffer_ptr;                       /* pointer to buffer to load data into */
+    uint32_t buffer_size;                   /* size in bytes of buffer */
+    void* user_data;                        /* optional userdata pointer */
+} sapp_html5_fetch_request;
+
+/* user-provided functions */
+extern sapp_desc sokol_main(int argc, char* argv[]);
+
+/* returns true after sokol-app has been initialized */
+SOKOL_APP_API_DECL bool sapp_isvalid(void);
+/* returns the current framebuffer width in pixels */
+SOKOL_APP_API_DECL int sapp_width(void);
+/* same as sapp_width(), but returns float */
+SOKOL_APP_API_DECL float sapp_widthf(void);
+/* returns the current framebuffer height in pixels */
+SOKOL_APP_API_DECL int sapp_height(void);
+/* same as sapp_height(), but returns float */
+SOKOL_APP_API_DECL float sapp_heightf(void);
+/* get default framebuffer color pixel format */
+SOKOL_APP_API_DECL int sapp_color_format(void);
+/* get default framebuffer depth pixel format */
+SOKOL_APP_API_DECL int sapp_depth_format(void);
+/* get default framebuffer sample count */
+SOKOL_APP_API_DECL int sapp_sample_count(void);
+/* returns true when high_dpi was requested and actually running in a high-dpi scenario */
+SOKOL_APP_API_DECL bool sapp_high_dpi(void);
+/* returns the dpi scaling factor (window pixels to framebuffer pixels) */
+SOKOL_APP_API_DECL float sapp_dpi_scale(void);
+/* show or hide the mobile device onscreen keyboard */
+SOKOL_APP_API_DECL void sapp_show_keyboard(bool show);
+/* return true if the mobile device onscreen keyboard is currently shown */
+SOKOL_APP_API_DECL bool sapp_keyboard_shown(void);
+/* query fullscreen mode */
+SOKOL_APP_API_DECL bool sapp_is_fullscreen(void);
+/* toggle fullscreen mode */
+SOKOL_APP_API_DECL void sapp_toggle_fullscreen(void);
+/* show or hide the mouse cursor */
+SOKOL_APP_API_DECL void sapp_show_mouse(bool show);
+/* show or hide the mouse cursor */
+SOKOL_APP_API_DECL bool sapp_mouse_shown();
+/* enable/disable mouse-pointer-lock mode */
+SOKOL_APP_API_DECL void sapp_lock_mouse(bool lock);
+/* return true if in mouse-pointer-lock mode (this may toggle a few frames later) */
+SOKOL_APP_API_DECL bool sapp_mouse_locked(void);
+/* return the userdata pointer optionally provided in sapp_desc */
+SOKOL_APP_API_DECL void* sapp_userdata(void);
+/* return a copy of the sapp_desc structure */
+SOKOL_APP_API_DECL sapp_desc sapp_query_desc(void);
+/* initiate a "soft quit" (sends SAPP_EVENTTYPE_QUIT_REQUESTED) */
+SOKOL_APP_API_DECL void sapp_request_quit(void);
+/* cancel a pending quit (when SAPP_EVENTTYPE_QUIT_REQUESTED has been received) */
+SOKOL_APP_API_DECL void sapp_cancel_quit(void);
+/* initiate a "hard quit" (quit application without sending SAPP_EVENTTYPE_QUIT_REQUSTED) */
+SOKOL_APP_API_DECL void sapp_quit(void);
+/* call from inside event callback to consume the current event (don't forward to platform) */
+SOKOL_APP_API_DECL void sapp_consume_event(void);
+/* get the current frame counter (for comparison with sapp_event.frame_count) */
+SOKOL_APP_API_DECL uint64_t sapp_frame_count(void);
+/* write string into clipboard */
+SOKOL_APP_API_DECL void sapp_set_clipboard_string(const char* str);
+/* read string from clipboard (usually during SAPP_EVENTTYPE_CLIPBOARD_PASTED) */
+SOKOL_APP_API_DECL const char* sapp_get_clipboard_string(void);
+/* set the window title (only on desktop platforms) */
+SOKOL_APP_API_DECL void sapp_set_window_title(const char* str);
+/* set the window icon (only on Windows and Linux) */
+SOKOL_APP_API_DECL void sapp_set_icon(const sapp_icon_desc* icon_desc);
+/* gets the total number of dropped files (after an SAPP_EVENTTYPE_FILES_DROPPED event) */
+SOKOL_APP_API_DECL int sapp_get_num_dropped_files(void);
+/* gets the dropped file paths */
+SOKOL_APP_API_DECL const char* sapp_get_dropped_file_path(int index);
+
+/* special run-function for SOKOL_NO_ENTRY (in standard mode this is an empty stub) */
+SOKOL_APP_API_DECL void sapp_run(const sapp_desc* desc);
+
+/* GL: return true when GLES2 fallback is active (to detect fallback from GLES3) */
+SOKOL_APP_API_DECL bool sapp_gles2(void);
+
+/* HTML5: enable or disable the hardwired "Leave Site?" dialog box */
+SOKOL_APP_API_DECL void sapp_html5_ask_leave_site(bool ask);
+/* HTML5: get byte size of a dropped file */
+SOKOL_APP_API_DECL uint32_t sapp_html5_get_dropped_file_size(int index);
+/* HTML5: asynchronously load the content of a dropped file */
+SOKOL_APP_API_DECL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request);
+
+/* Metal: get bridged pointer to Metal device object */
+SOKOL_APP_API_DECL const void* sapp_metal_get_device(void);
+/* Metal: get bridged pointer to this frame's renderpass descriptor */
+SOKOL_APP_API_DECL const void* sapp_metal_get_renderpass_descriptor(void);
+/* Metal: get bridged pointer to current drawable */
+SOKOL_APP_API_DECL const void* sapp_metal_get_drawable(void);
+/* macOS: get bridged pointer to macOS NSWindow */
+SOKOL_APP_API_DECL const void* sapp_macos_get_window(void);
+/* iOS: get bridged pointer to iOS UIWindow */
+SOKOL_APP_API_DECL const void* sapp_ios_get_window(void);
+
+/* D3D11: get pointer to ID3D11Device object */
+SOKOL_APP_API_DECL const void* sapp_d3d11_get_device(void);
+/* D3D11: get pointer to ID3D11DeviceContext object */
+SOKOL_APP_API_DECL const void* sapp_d3d11_get_device_context(void);
+/* D3D11: get pointer to IDXGISwapChain object */
+SOKOL_APP_API_DECL const void* sapp_d3d11_get_swap_chain(void);
+/* D3D11: get pointer to ID3D11RenderTargetView object */
+SOKOL_APP_API_DECL const void* sapp_d3d11_get_render_target_view(void);
+/* D3D11: get pointer to ID3D11DepthStencilView */
+SOKOL_APP_API_DECL const void* sapp_d3d11_get_depth_stencil_view(void);
+/* Win32: get the HWND window handle */
+SOKOL_APP_API_DECL const void* sapp_win32_get_hwnd(void);
+
+/* WebGPU: get WGPUDevice handle */
+SOKOL_APP_API_DECL const void* sapp_wgpu_get_device(void);
+/* WebGPU: get swapchain's WGPUTextureView handle for rendering */
+SOKOL_APP_API_DECL const void* sapp_wgpu_get_render_view(void);
+/* WebGPU: get swapchain's MSAA-resolve WGPUTextureView (may return null) */
+SOKOL_APP_API_DECL const void* sapp_wgpu_get_resolve_view(void);
+/* WebGPU: get swapchain's WGPUTextureView for the depth-stencil surface */
+SOKOL_APP_API_DECL const void* sapp_wgpu_get_depth_stencil_view(void);
+
+/* Android: get native activity handle */
+SOKOL_APP_API_DECL const void* sapp_android_get_native_activity(void);
+
+#ifdef __cplusplus
+} /* extern "C" */
+
+/* reference-based equivalents for C++ */
+inline void sapp_run(const sapp_desc& desc) { return sapp_run(&desc); }
+
+#endif
+
+// this WinRT specific hack is required when wWinMain is in a static library
+#if defined(_MSC_VER) && defined(UNICODE)
+#include <winapifamily.h>
+#if defined(WINAPI_FAMILY_PARTITION) && !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
+#pragma comment(linker, "/include:wWinMain")
+#endif
+#endif
+
+#endif // SOKOL_APP_INCLUDED
+
+/*-- IMPLEMENTATION ----------------------------------------------------------*/
+#ifdef SOKOL_APP_IMPL
+#define SOKOL_APP_IMPL_INCLUDED (1)
+
+#include <string.h> // memset
+#include <stddef.h> // size_t
+
+/* check if the config defines are alright */
+#if defined(__APPLE__)
+    // see https://clang.llvm.org/docs/LanguageExtensions.html#automatic-reference-counting
+    #if !defined(__cplusplus)
+        #if __has_feature(objc_arc) && !__has_feature(objc_arc_fields)
+            #error "sokol_app.h requires __has_feature(objc_arc_field) if ARC is enabled (use a more recent compiler version)"
+        #endif
+    #endif
+    #define _SAPP_APPLE (1)
+    #include <TargetConditionals.h>
+    #if defined(TARGET_OS_IPHONE) && !TARGET_OS_IPHONE
+        /* MacOS */
+        #define _SAPP_MACOS (1)
+        #if !defined(SOKOL_METAL) && !defined(SOKOL_GLCORE33)
+        #error("sokol_app.h: unknown 3D API selected for MacOS, must be SOKOL_METAL or SOKOL_GLCORE33")
+        #endif
+    #else
+        /* iOS or iOS Simulator */
+        #define _SAPP_IOS (1)
+        #if !defined(SOKOL_METAL) && !defined(SOKOL_GLES3)
+        #error("sokol_app.h: unknown 3D API selected for iOS, must be SOKOL_METAL or SOKOL_GLES3")
+        #endif
+    #endif
+#elif defined(__EMSCRIPTEN__)
+    /* emscripten (asm.js or wasm) */
+    #define _SAPP_EMSCRIPTEN (1)
+    #if !defined(SOKOL_GLES3) && !defined(SOKOL_GLES2) && !defined(SOKOL_WGPU)
+    #error("sokol_app.h: unknown 3D API selected for emscripten, must be SOKOL_GLES3, SOKOL_GLES2 or SOKOL_WGPU")
+    #endif
+#elif defined(_WIN32)
+    /* Windows (D3D11 or GL) */
+    #include <winapifamily.h>
+    #if (defined(WINAPI_FAMILY_PARTITION) && !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP))
+        #define _SAPP_UWP (1)
+        #if !defined(SOKOL_D3D11)
+        #error("sokol_app.h: unknown 3D API selected for UWP, must be SOKOL_D3D11")
+        #endif
+        #if !defined(__cplusplus)
+        #error("sokol_app.h: UWP bindings require C++/17")
+        #endif
+    #else
+        #define _SAPP_WIN32 (1)
+        #if !defined(SOKOL_D3D11) && !defined(SOKOL_GLCORE33)
+        #error("sokol_app.h: unknown 3D API selected for Win32, must be SOKOL_D3D11 or SOKOL_GLCORE33")
+        #endif
+    #endif
+#elif defined(__ANDROID__)
+    /* Android */
+    #define _SAPP_ANDROID (1)
+    #if !defined(SOKOL_GLES3) && !defined(SOKOL_GLES2)
+    #error("sokol_app.h: unknown 3D API selected for Android, must be SOKOL_GLES3 or SOKOL_GLES2")
+    #endif
+    #if defined(SOKOL_NO_ENTRY)
+    #error("sokol_app.h: SOKOL_NO_ENTRY is not supported on Android")
+    #endif
+#elif defined(__linux__) || defined(__unix__)
+    /* Linux */
+    #define _SAPP_LINUX (1)
+    #if !defined(SOKOL_GLCORE33)
+    #error("sokol_app.h: unknown 3D API selected for Linux, must be SOKOL_GLCORE33")
+    #endif
+#else
+#error "sokol_app.h: Unknown platform"
+#endif
+
+#ifndef SOKOL_API_IMPL
+    #define SOKOL_API_IMPL
+#endif
+#ifndef SOKOL_DEBUG
+    #ifndef NDEBUG
+        #define SOKOL_DEBUG (1)
+    #endif
+#endif
+#ifndef SOKOL_ASSERT
+    #include <assert.h>
+    #define SOKOL_ASSERT(c) assert(c)
+#endif
+#ifndef SOKOL_UNREACHABLE
+    #define SOKOL_UNREACHABLE SOKOL_ASSERT(false)
+#endif
+#if !defined(SOKOL_CALLOC) || !defined(SOKOL_FREE)
+    #include <stdlib.h>
+#endif
+#if !defined(SOKOL_CALLOC)
+    #define SOKOL_CALLOC(n,s) calloc(n,s)
+#endif
+#if !defined(SOKOL_FREE)
+    #define SOKOL_FREE(p) free(p)
+#endif
+#ifndef SOKOL_LOG
+    #ifdef SOKOL_DEBUG
+        #if defined(__ANDROID__)
+            #include <android/log.h>
+            #define SOKOL_LOG(s) { SOKOL_ASSERT(s); __android_log_write(ANDROID_LOG_INFO, "SOKOL_APP", s); }
+        #else
+            #include <stdio.h>
+            #define SOKOL_LOG(s) { SOKOL_ASSERT(s); puts(s); }
+        #endif
+    #else
+        #define SOKOL_LOG(s)
+    #endif
+#endif
+#ifndef SOKOL_ABORT
+    #include <stdlib.h>
+    #define SOKOL_ABORT() abort()
+#endif
+#ifndef _SOKOL_PRIVATE
+    #if defined(__GNUC__) || defined(__clang__)
+        #define _SOKOL_PRIVATE __attribute__((unused)) static
+    #else
+        #define _SOKOL_PRIVATE static
+    #endif
+#endif
+#ifndef _SOKOL_UNUSED
+    #define _SOKOL_UNUSED(x) (void)(x)
+#endif
+
+/*== PLATFORM SPECIFIC INCLUDES AND DEFINES ==================================*/
+#if defined(_SAPP_APPLE)
+    #if defined(SOKOL_METAL)
+        #import <Metal/Metal.h>
+        #import <MetalKit/MetalKit.h>
+    #endif
+    #if defined(_SAPP_MACOS)
+        #if !defined(SOKOL_METAL)
+            #ifndef GL_SILENCE_DEPRECATION
+            #define GL_SILENCE_DEPRECATION
+            #endif
+            #include <Cocoa/Cocoa.h>
+        #endif
+    #elif defined(_SAPP_IOS)
+        #import <UIKit/UIKit.h>
+        #if !defined(SOKOL_METAL)
+            #import <GLKit/GLKit.h>
+        #endif
+    #endif
+#elif defined(_SAPP_EMSCRIPTEN)
+    #if defined(SOKOL_WGPU)
+        #include <webgpu/webgpu.h>
+    #endif
+    #include <emscripten/emscripten.h>
+    #include <emscripten/html5.h>
+#elif defined(_SAPP_WIN32)
+    #ifdef _MSC_VER
+        #pragma warning(push)
+        #pragma warning(disable:4201)   /* nonstandard extension used: nameless struct/union */
+        #pragma warning(disable:4204)   /* nonstandard extension used: non-constant aggregate initializer */
+        #pragma warning(disable:4054)   /* 'type cast': from function pointer */
+        #pragma warning(disable:4055)   /* 'type cast': from data pointer */
+        #pragma warning(disable:4505)   /* unreferenced local function has been removed */
+        #pragma warning(disable:4115)   /* /W4: 'ID3D11ModuleInstance': named type definition in parentheses (in d3d11.h) */
+    #endif
+    #ifndef WIN32_LEAN_AND_MEAN
+        #define WIN32_LEAN_AND_MEAN
+    #endif
+    #ifndef NOMINMAX
+        #define NOMINMAX
+    #endif
+    #include <windows.h>
+    #include <windowsx.h>
+    #include <shellapi.h>
+    #if !defined(SOKOL_NO_ENTRY)    // if SOKOL_NO_ENTRY is defined, it's the applications' responsibility to use the right subsystem
+        #if defined(SOKOL_WIN32_FORCE_MAIN)
+            #pragma comment (linker, "/subsystem:console")
+        #else
+            #pragma comment (linker, "/subsystem:windows")
+        #endif
+    #endif
+    #include <stdio.h>  /* freopens() */
+    #include <wchar.h>  /* wcslen() */
+
+    #pragma comment (lib, "kernel32")
+    #pragma comment (lib, "user32")
+    #pragma comment (lib, "shell32")    /* CommandLineToArgvW, DragQueryFileW, DragFinished */
+    #pragma comment (lib, "gdi32")
+    #if defined(SOKOL_D3D11)
+        #pragma comment (lib, "dxgi")
+        #pragma comment (lib, "d3d11")
+    #endif
+
+    #if defined(SOKOL_D3D11)
+        #ifndef D3D11_NO_HELPERS
+            #define D3D11_NO_HELPERS
+        #endif
+        #include <d3d11.h>
+        #include <dxgi.h>
+        // DXGI_SWAP_EFFECT_FLIP_DISCARD is only defined in newer Windows SDKs, so don't depend on it
+        #define _SAPP_DXGI_SWAP_EFFECT_FLIP_DISCARD (4)
+    #endif
+    #ifndef WM_MOUSEHWHEEL /* see https://github.com/floooh/sokol/issues/138 */
+        #define WM_MOUSEHWHEEL (0x020E)
+    #endif
+#elif defined(_SAPP_UWP)
+    #ifndef NOMINMAX
+        #define NOMINMAX
+    #endif
+    #ifdef _MSC_VER
+        #pragma warning(push)
+        #pragma warning(disable:4201)   /* nonstandard extension used: nameless struct/union */
+        #pragma warning(disable:4054)   /* 'type cast': from function pointer */
+        #pragma warning(disable:4055)   /* 'type cast': from data pointer */
+        #pragma warning(disable:4505)   /* unreferenced local function has been removed */
+        #pragma warning(disable:4115)   /* /W4: 'ID3D11ModuleInstance': named type definition in parentheses (in d3d11.h) */
+    #endif
+    #include <windows.h>
+    #include <winrt/Windows.ApplicationModel.Core.h>
+    #include <winrt/Windows.Foundation.h>
+    #include <winrt/Windows.Foundation.Collections.h>
+    #include <winrt/Windows.Graphics.Display.h>
+    #include <winrt/Windows.UI.Core.h>
+    #include <winrt/Windows.UI.Composition.h>
+    #include <winrt/Windows.UI.Input.h>
+    #include <winrt/Windows.UI.ViewManagement.h>
+    #include <winrt/Windows.System.h>
+    #include <ppltasks.h>
+
+    #include <dxgi1_4.h>
+    #include <d3d11_3.h>
+    #include <DirectXMath.h>
+
+    #pragma comment (lib, "WindowsApp")
+    #pragma comment (lib, "dxguid")
+#elif defined(_SAPP_ANDROID)
+    #include <pthread.h>
+    #include <unistd.h>
+    #include <android/native_activity.h>
+    #include <android/looper.h>
+    #include <EGL/egl.h>
+#elif defined(_SAPP_LINUX)
+    #define GL_GLEXT_PROTOTYPES
+    #include <X11/Xlib.h>
+    #include <X11/Xutil.h>
+    #include <X11/XKBlib.h>
+    #include <X11/keysym.h>
+    #include <X11/Xresource.h>
+    #include <X11/Xatom.h>
+    #include <X11/extensions/XInput2.h>
+    #include <X11/Xcursor/Xcursor.h>
+    #include <X11/Xmd.h> /* CARD32 */
+    #include <dlfcn.h> /* dlopen, dlsym, dlclose */
+    #include <limits.h> /* LONG_MAX */
+    #include <pthread.h>    /* only used a linker-guard, search for _sapp_linux_run() and see first comment */
+#endif
+
+/*== MACOS DECLARATIONS ======================================================*/
+#if defined(_SAPP_MACOS)
+@interface _sapp_macos_app_delegate : NSObject<NSApplicationDelegate>
+@end
+@interface _sapp_macos_window : NSWindow
+@end
+@interface _sapp_macos_window_delegate : NSObject<NSWindowDelegate>
+@end
+#if defined(SOKOL_METAL)
+    @interface _sapp_macos_view : MTKView
+    @end
+#elif defined(SOKOL_GLCORE33)
+    @interface _sapp_macos_view : NSOpenGLView
+    - (void)timerFired:(id)sender;
+    @end
+#endif // SOKOL_GLCORE33
+
+typedef struct {
+    uint32_t flags_changed_store;
+    uint8_t mouse_buttons;
+    NSWindow* window;
+    NSTrackingArea* tracking_area;
+    _sapp_macos_app_delegate* app_dlg;
+    _sapp_macos_window_delegate* win_dlg;
+    _sapp_macos_view* view;
+    #if defined(SOKOL_METAL)
+        id<MTLDevice> mtl_device;
+    #endif
+} _sapp_macos_t;
+
+#endif // _SAPP_MACOS
+
+/*== IOS DECLARATIONS ========================================================*/
+#if defined(_SAPP_IOS)
+
+@interface _sapp_app_delegate : NSObject<UIApplicationDelegate>
+@end
+@interface _sapp_textfield_dlg : NSObject<UITextFieldDelegate>
+- (void)keyboardWasShown:(NSNotification*)notif;
+- (void)keyboardWillBeHidden:(NSNotification*)notif;
+- (void)keyboardDidChangeFrame:(NSNotification*)notif;
+@end
+#if defined(SOKOL_METAL)
+    @interface _sapp_ios_view : MTKView;
+    @end
+#else
+    @interface _sapp_ios_view : GLKView
+    @end
+#endif
+
+typedef struct {
+    UIWindow* window;
+    _sapp_ios_view* view;
+    UITextField* textfield;
+    _sapp_textfield_dlg* textfield_dlg;
+    #if defined(SOKOL_METAL)
+        UIViewController* view_ctrl;
+        id<MTLDevice> mtl_device;
+    #else
+        GLKViewController* view_ctrl;
+        EAGLContext* eagl_ctx;
+    #endif
+    bool suspended;
+} _sapp_ios_t;
+
+#endif // _SAPP_IOS
+
+/*== EMSCRIPTEN DECLARATIONS =================================================*/
+#if defined(_SAPP_EMSCRIPTEN)
+
+#if defined(SOKOL_WGPU)
+typedef struct {
+    int state;
+    WGPUDevice device;
+    WGPUSwapChain swapchain;
+    WGPUTextureFormat render_format;
+    WGPUTexture msaa_tex;
+    WGPUTexture depth_stencil_tex;
+    WGPUTextureView swapchain_view;
+    WGPUTextureView msaa_view;
+    WGPUTextureView depth_stencil_view;
+} _sapp_wgpu_t;
+#endif
+
+typedef struct {
+    bool textfield_created;
+    bool wants_show_keyboard;
+    bool wants_hide_keyboard;
+    bool mouse_lock_requested;
+    uint16_t mouse_buttons;
+    #if defined(SOKOL_WGPU)
+    _sapp_wgpu_t wgpu;
+    #endif
+} _sapp_emsc_t;
+#endif // _SAPP_EMSCRIPTEN
+
+/*== WIN32 DECLARATIONS ======================================================*/
+#if defined(SOKOL_D3D11) && (defined(_SAPP_WIN32) || defined(_SAPP_UWP))
+typedef struct {
+    ID3D11Device* device;
+    ID3D11DeviceContext* device_context;
+    ID3D11Texture2D* rt;
+    ID3D11RenderTargetView* rtv;
+    ID3D11Texture2D* msaa_rt;
+    ID3D11RenderTargetView* msaa_rtv;
+    ID3D11Texture2D* ds;
+    ID3D11DepthStencilView* dsv;
+    DXGI_SWAP_CHAIN_DESC swap_chain_desc;
+    IDXGISwapChain* swap_chain;
+} _sapp_d3d11_t;
+#endif
+
+/*== WIN32 DECLARATIONS ======================================================*/
+#if defined(_SAPP_WIN32)
+
+#ifndef DPI_ENUMS_DECLARED
+typedef enum PROCESS_DPI_AWARENESS
+{
+    PROCESS_DPI_UNAWARE = 0,
+    PROCESS_SYSTEM_DPI_AWARE = 1,
+    PROCESS_PER_MONITOR_DPI_AWARE = 2
+} PROCESS_DPI_AWARENESS;
+typedef enum MONITOR_DPI_TYPE {
+    MDT_EFFECTIVE_DPI = 0,
+    MDT_ANGULAR_DPI = 1,
+    MDT_RAW_DPI = 2,
+    MDT_DEFAULT = MDT_EFFECTIVE_DPI
+} MONITOR_DPI_TYPE;
+#endif /*DPI_ENUMS_DECLARED*/
+
+typedef struct {
+    bool aware;
+    float content_scale;
+    float window_scale;
+    float mouse_scale;
+} _sapp_win32_dpi_t;
+
+typedef struct {
+    HWND hwnd;
+    HDC dc;
+    HICON big_icon;
+    HICON small_icon;
+    UINT orig_codepage;
+    LONG mouse_locked_x, mouse_locked_y;
+    bool is_win10_or_greater;
+    bool in_create_window;
+    bool iconified;
+    bool mouse_tracked;
+    uint8_t mouse_capture_mask;
+    _sapp_win32_dpi_t dpi;
+    bool raw_input_mousepos_valid;
+    LONG raw_input_mousepos_x;
+    LONG raw_input_mousepos_y;
+    uint8_t raw_input_data[256];
+} _sapp_win32_t;
+
+#if defined(SOKOL_GLCORE33)
+#define WGL_NUMBER_PIXEL_FORMATS_ARB 0x2000
+#define WGL_SUPPORT_OPENGL_ARB 0x2010
+#define WGL_DRAW_TO_WINDOW_ARB 0x2001
+#define WGL_PIXEL_TYPE_ARB 0x2013
+#define WGL_TYPE_RGBA_ARB 0x202b
+#define WGL_ACCELERATION_ARB 0x2003
+#define WGL_NO_ACCELERATION_ARB 0x2025
+#define WGL_RED_BITS_ARB 0x2015
+#define WGL_GREEN_BITS_ARB 0x2017
+#define WGL_BLUE_BITS_ARB 0x2019
+#define WGL_ALPHA_BITS_ARB 0x201b
+#define WGL_DEPTH_BITS_ARB 0x2022
+#define WGL_STENCIL_BITS_ARB 0x2023
+#define WGL_DOUBLE_BUFFER_ARB 0x2011
+#define WGL_SAMPLES_ARB 0x2042
+#define WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002
+#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
+#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define WGL_CONTEXT_FLAGS_ARB 0x2094
+#define ERROR_INVALID_VERSION_ARB 0x2095
+#define ERROR_INVALID_PROFILE_ARB 0x2096
+#define ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB 0x2054
+typedef BOOL (WINAPI * PFNWGLSWAPINTERVALEXTPROC)(int);
+typedef BOOL (WINAPI * PFNWGLGETPIXELFORMATATTRIBIVARBPROC)(HDC,int,int,UINT,const int*,int*);
+typedef const char* (WINAPI * PFNWGLGETEXTENSIONSSTRINGEXTPROC)(void);
+typedef const char* (WINAPI * PFNWGLGETEXTENSIONSSTRINGARBPROC)(HDC);
+typedef HGLRC (WINAPI * PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC,HGLRC,const int*);
+typedef HGLRC (WINAPI * PFN_wglCreateContext)(HDC);
+typedef BOOL (WINAPI * PFN_wglDeleteContext)(HGLRC);
+typedef PROC (WINAPI * PFN_wglGetProcAddress)(LPCSTR);
+typedef HDC (WINAPI * PFN_wglGetCurrentDC)(void);
+typedef BOOL (WINAPI * PFN_wglMakeCurrent)(HDC,HGLRC);
+
+typedef struct {
+    HINSTANCE opengl32;
+    HGLRC gl_ctx;
+    PFN_wglCreateContext CreateContext;
+    PFN_wglDeleteContext DeleteContext;
+    PFN_wglGetProcAddress GetProcAddress;
+    PFN_wglGetCurrentDC GetCurrentDC;
+    PFN_wglMakeCurrent MakeCurrent;
+    PFNWGLSWAPINTERVALEXTPROC SwapIntervalEXT;
+    PFNWGLGETPIXELFORMATATTRIBIVARBPROC GetPixelFormatAttribivARB;
+    PFNWGLGETEXTENSIONSSTRINGEXTPROC GetExtensionsStringEXT;
+    PFNWGLGETEXTENSIONSSTRINGARBPROC GetExtensionsStringARB;
+    PFNWGLCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB;
+    bool ext_swap_control;
+    bool arb_multisample;
+    bool arb_pixel_format;
+    bool arb_create_context;
+    bool arb_create_context_profile;
+    HWND msg_hwnd;
+    HDC msg_dc;
+} _sapp_wgl_t;
+#endif // SOKOL_GLCORE33
+
+#endif // _SAPP_WIN32
+
+/*== UWP DECLARATIONS ======================================================*/
+#if defined(_SAPP_UWP)
+
+typedef struct {
+    float content_scale;
+    float window_scale;
+    float mouse_scale;
+} _sapp_uwp_dpi_t;
+
+typedef struct {
+    bool mouse_tracked;
+    uint8_t mouse_buttons;
+    _sapp_uwp_dpi_t dpi;
+} _sapp_uwp_t;
+
+#endif // _SAPP_UWP
+
+/*== ANDROID DECLARATIONS ====================================================*/
+
+#if defined(_SAPP_ANDROID)
+typedef enum {
+    _SOKOL_ANDROID_MSG_CREATE,
+    _SOKOL_ANDROID_MSG_RESUME,
+    _SOKOL_ANDROID_MSG_PAUSE,
+    _SOKOL_ANDROID_MSG_FOCUS,
+    _SOKOL_ANDROID_MSG_NO_FOCUS,
+    _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW,
+    _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE,
+    _SOKOL_ANDROID_MSG_DESTROY,
+} _sapp_android_msg_t;
+
+typedef struct {
+    pthread_t thread;
+    pthread_mutex_t mutex;
+    pthread_cond_t cond;
+    int read_from_main_fd;
+    int write_from_main_fd;
+} _sapp_android_pt_t;
+
+typedef struct {
+    ANativeWindow* window;
+    AInputQueue* input;
+} _sapp_android_resources_t;
+
+typedef struct {
+    ANativeActivity* activity;
+    _sapp_android_pt_t pt;
+    _sapp_android_resources_t pending;
+    _sapp_android_resources_t current;
+    ALooper* looper;
+    bool is_thread_started;
+    bool is_thread_stopping;
+    bool is_thread_stopped;
+    bool has_created;
+    bool has_resumed;
+    bool has_focus;
+    EGLConfig config;
+    EGLDisplay display;
+    EGLContext context;
+    EGLSurface surface;
+} _sapp_android_t;
+
+#endif // _SAPP_ANDROID
+
+/*== LINUX DECLARATIONS ======================================================*/
+#if defined(_SAPP_LINUX)
+
+#define _SAPP_X11_XDND_VERSION (5)
+
+#define GLX_VENDOR 1
+#define GLX_RGBA_BIT 0x00000001
+#define GLX_WINDOW_BIT 0x00000001
+#define GLX_DRAWABLE_TYPE 0x8010
+#define GLX_RENDER_TYPE	0x8011
+#define GLX_DOUBLEBUFFER 5
+#define GLX_RED_SIZE 8
+#define GLX_GREEN_SIZE 9
+#define GLX_BLUE_SIZE 10
+#define GLX_ALPHA_SIZE 11
+#define GLX_DEPTH_SIZE 12
+#define GLX_STENCIL_SIZE 13
+#define GLX_SAMPLES 0x186a1
+#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
+#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126
+#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002
+#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
+#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
+#define GLX_CONTEXT_FLAGS_ARB 0x2094
+
+typedef XID GLXWindow;
+typedef XID GLXDrawable;
+typedef struct __GLXFBConfig* GLXFBConfig;
+typedef struct __GLXcontext* GLXContext;
+typedef void (*__GLXextproc)(void);
+
+typedef int (*PFNGLXGETFBCONFIGATTRIBPROC)(Display*,GLXFBConfig,int,int*);
+typedef const char* (*PFNGLXGETCLIENTSTRINGPROC)(Display*,int);
+typedef Bool (*PFNGLXQUERYEXTENSIONPROC)(Display*,int*,int*);
+typedef Bool (*PFNGLXQUERYVERSIONPROC)(Display*,int*,int*);
+typedef void (*PFNGLXDESTROYCONTEXTPROC)(Display*,GLXContext);
+typedef Bool (*PFNGLXMAKECURRENTPROC)(Display*,GLXDrawable,GLXContext);
+typedef void (*PFNGLXSWAPBUFFERSPROC)(Display*,GLXDrawable);
+typedef const char* (*PFNGLXQUERYEXTENSIONSSTRINGPROC)(Display*,int);
+typedef GLXFBConfig* (*PFNGLXGETFBCONFIGSPROC)(Display*,int,int*);
+typedef __GLXextproc (* PFNGLXGETPROCADDRESSPROC)(const char *procName);
+typedef void (*PFNGLXSWAPINTERVALEXTPROC)(Display*,GLXDrawable,int);
+typedef XVisualInfo* (*PFNGLXGETVISUALFROMFBCONFIGPROC)(Display*,GLXFBConfig);
+typedef GLXWindow (*PFNGLXCREATEWINDOWPROC)(Display*,GLXFBConfig,Window,const int*);
+typedef void (*PFNGLXDESTROYWINDOWPROC)(Display*,GLXWindow);
+
+typedef int (*PFNGLXSWAPINTERVALMESAPROC)(int);
+typedef GLXContext (*PFNGLXCREATECONTEXTATTRIBSARBPROC)(Display*,GLXFBConfig,GLXContext,Bool,const int*);
+
+typedef struct {
+    bool available;
+    int major_opcode;
+    int event_base;
+    int error_base;
+    int major;
+    int minor;
+} _sapp_xi_t;
+
+typedef struct {
+    int version;
+    Window source;
+    Atom format;
+    Atom XdndAware;
+    Atom XdndEnter;
+    Atom XdndPosition;
+    Atom XdndStatus;
+    Atom XdndActionCopy;
+    Atom XdndDrop;
+    Atom XdndFinished;
+    Atom XdndSelection;
+    Atom XdndTypeList;
+    Atom text_uri_list;
+} _sapp_xdnd_t;
+
+typedef struct {
+    uint8_t mouse_buttons;
+    Display* display;
+    int screen;
+    Window root;
+    Colormap colormap;
+    Window window;
+    Cursor hidden_cursor;
+    int window_state;
+    float dpi;
+    unsigned char error_code;
+    Atom UTF8_STRING;
+    Atom WM_PROTOCOLS;
+    Atom WM_DELETE_WINDOW;
+    Atom WM_STATE;
+    Atom NET_WM_NAME;
+    Atom NET_WM_ICON_NAME;
+    Atom NET_WM_ICON;
+    Atom NET_WM_STATE;
+    Atom NET_WM_STATE_FULLSCREEN;
+    _sapp_xi_t xi;
+    _sapp_xdnd_t xdnd;
+} _sapp_x11_t;
+
+typedef struct {
+    void* libgl;
+    int major;
+    int minor;
+    int event_base;
+    int error_base;
+    GLXContext ctx;
+    GLXWindow window;
+
+    // GLX 1.3 functions
+    PFNGLXGETFBCONFIGSPROC GetFBConfigs;
+    PFNGLXGETFBCONFIGATTRIBPROC GetFBConfigAttrib;
+    PFNGLXGETCLIENTSTRINGPROC GetClientString;
+    PFNGLXQUERYEXTENSIONPROC QueryExtension;
+    PFNGLXQUERYVERSIONPROC QueryVersion;
+    PFNGLXDESTROYCONTEXTPROC DestroyContext;
+    PFNGLXMAKECURRENTPROC MakeCurrent;
+    PFNGLXSWAPBUFFERSPROC SwapBuffers;
+    PFNGLXQUERYEXTENSIONSSTRINGPROC QueryExtensionsString;
+    PFNGLXGETVISUALFROMFBCONFIGPROC GetVisualFromFBConfig;
+    PFNGLXCREATEWINDOWPROC CreateWindow;
+    PFNGLXDESTROYWINDOWPROC DestroyWindow;
+
+    // GLX 1.4 and extension functions
+    PFNGLXGETPROCADDRESSPROC GetProcAddress;
+    PFNGLXGETPROCADDRESSPROC GetProcAddressARB;
+    PFNGLXSWAPINTERVALEXTPROC SwapIntervalEXT;
+    PFNGLXSWAPINTERVALMESAPROC SwapIntervalMESA;
+    PFNGLXCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB;
+
+    // extension availability
+    bool EXT_swap_control;
+    bool MESA_swap_control;
+    bool ARB_multisample;
+    bool ARB_create_context;
+    bool ARB_create_context_profile;
+} _sapp_glx_t;
+
+#endif // _SAPP_LINUX
+
+/*== COMMON DECLARATIONS =====================================================*/
+
+/* helper macros */
+#define _sapp_def(val, def) (((val) == 0) ? (def) : (val))
+#define _sapp_absf(a) (((a)<0.0f)?-(a):(a))
+
+#define _SAPP_MAX_TITLE_LENGTH (128)
+/* NOTE: the pixel format values *must* be compatible with sg_pixel_format */
+#define _SAPP_PIXELFORMAT_RGBA8 (23)
+#define _SAPP_PIXELFORMAT_BGRA8 (27)
+#define _SAPP_PIXELFORMAT_DEPTH (41)
+#define _SAPP_PIXELFORMAT_DEPTH_STENCIL (42)
+
+#if defined(_SAPP_MACOS) || defined(_SAPP_IOS)
+    // this is ARC compatible
+    #if defined(__cplusplus)
+        #define _SAPP_CLEAR(type, item) { item = (type) { }; }
+    #else
+        #define _SAPP_CLEAR(type, item) { item = (type) { 0 }; }
+    #endif
+#else
+    #define _SAPP_CLEAR(type, item) { memset(&item, 0, sizeof(item)); }
+#endif
+
+typedef struct {
+    bool enabled;
+    int buf_size;
+    char* buffer;
+} _sapp_clipboard_t;
+
+typedef struct {
+    bool enabled;
+    int max_files;
+    int max_path_length;
+    int num_files;
+    int buf_size;
+    char* buffer;
+} _sapp_drop_t;
+
+typedef struct {
+    float x, y;
+    float dx, dy;
+    bool shown;
+    bool locked;
+    bool pos_valid;
+} _sapp_mouse_t;
+
+typedef struct {
+    sapp_desc desc;
+    bool valid;
+    bool fullscreen;
+    bool gles2_fallback;
+    bool first_frame;
+    bool init_called;
+    bool cleanup_called;
+    bool quit_requested;
+    bool quit_ordered;
+    bool event_consumed;
+    bool html5_ask_leave_site;
+    bool onscreen_keyboard_shown;
+    int window_width;
+    int window_height;
+    int framebuffer_width;
+    int framebuffer_height;
+    int sample_count;
+    int swap_interval;
+    float dpi_scale;
+    uint64_t frame_count;
+    sapp_event event;
+    _sapp_mouse_t mouse;
+    _sapp_clipboard_t clipboard;
+    _sapp_drop_t drop;
+    sapp_icon_desc default_icon_desc;
+    uint32_t* default_icon_pixels;
+    #if defined(_SAPP_MACOS)
+        _sapp_macos_t macos;
+    #elif defined(_SAPP_IOS)
+        _sapp_ios_t ios;
+    #elif defined(_SAPP_EMSCRIPTEN)
+        _sapp_emsc_t emsc;
+    #elif defined(_SAPP_WIN32)
+        _sapp_win32_t win32;
+        #if defined(SOKOL_D3D11)
+            _sapp_d3d11_t d3d11;
+        #elif defined(SOKOL_GLCORE33)
+            _sapp_wgl_t wgl;
+        #endif
+    #elif defined(_SAPP_UWP)
+            _sapp_uwp_t uwp;
+        #if defined(SOKOL_D3D11)
+            _sapp_d3d11_t d3d11;
+        #endif
+    #elif defined(_SAPP_ANDROID)
+        _sapp_android_t android;
+    #elif defined(_SAPP_LINUX)
+        _sapp_x11_t x11;
+        _sapp_glx_t glx;
+    #endif
+    char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH];
+    char window_title[_SAPP_MAX_TITLE_LENGTH];      /* UTF-8 */
+    wchar_t window_title_wide[_SAPP_MAX_TITLE_LENGTH];   /* UTF-32 or UCS-2 */
+    sapp_keycode keycodes[SAPP_MAX_KEYCODES];
+} _sapp_t;
+static _sapp_t _sapp;
+
+/*=== PRIVATE HELPER FUNCTIONS ===============================================*/
+_SOKOL_PRIVATE void _sapp_fail(const char* msg) {
+    if (_sapp.desc.fail_cb) {
+        _sapp.desc.fail_cb(msg);
+    }
+    else if (_sapp.desc.fail_userdata_cb) {
+        _sapp.desc.fail_userdata_cb(msg, _sapp.desc.user_data);
+    }
+    else {
+        SOKOL_LOG(msg);
+    }
+    SOKOL_ABORT();
+}
+
+_SOKOL_PRIVATE void _sapp_call_init(void) {
+    if (_sapp.desc.init_cb) {
+        _sapp.desc.init_cb();
+    }
+    else if (_sapp.desc.init_userdata_cb) {
+        _sapp.desc.init_userdata_cb(_sapp.desc.user_data);
+    }
+    _sapp.init_called = true;
+}
+
+_SOKOL_PRIVATE void _sapp_call_frame(void) {
+    if (_sapp.init_called && !_sapp.cleanup_called) {
+        if (_sapp.desc.frame_cb) {
+            _sapp.desc.frame_cb();
+        }
+        else if (_sapp.desc.frame_userdata_cb) {
+            _sapp.desc.frame_userdata_cb(_sapp.desc.user_data);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_call_cleanup(void) {
+    if (!_sapp.cleanup_called) {
+        if (_sapp.desc.cleanup_cb) {
+            _sapp.desc.cleanup_cb();
+        }
+        else if (_sapp.desc.cleanup_userdata_cb) {
+            _sapp.desc.cleanup_userdata_cb(_sapp.desc.user_data);
+        }
+        _sapp.cleanup_called = true;
+    }
+}
+
+_SOKOL_PRIVATE bool _sapp_call_event(const sapp_event* e) {
+    if (!_sapp.cleanup_called) {
+        if (_sapp.desc.event_cb) {
+            _sapp.desc.event_cb(e);
+        }
+        else if (_sapp.desc.event_userdata_cb) {
+            _sapp.desc.event_userdata_cb(e, _sapp.desc.user_data);
+        }
+    }
+    if (_sapp.event_consumed) {
+        _sapp.event_consumed = false;
+        return true;
+    }
+    else {
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE char* _sapp_dropped_file_path_ptr(int index) {
+    SOKOL_ASSERT(_sapp.drop.buffer);
+    SOKOL_ASSERT((index >= 0) && (index <= _sapp.drop.max_files));
+    int offset = index * _sapp.drop.max_path_length;
+    SOKOL_ASSERT(offset < _sapp.drop.buf_size);
+    return &_sapp.drop.buffer[offset];
+}
+
+/* Copy a string into a fixed size buffer with guaranteed zero-
+   termination.
+
+   Return false if the string didn't fit into the buffer and had to be clamped.
+
+   FIXME: Currently UTF-8 strings might become invalid if the string
+   is clamped, because the last zero-byte might be written into
+   the middle of a multi-byte sequence.
+*/
+_SOKOL_PRIVATE bool _sapp_strcpy(const char* src, char* dst, int max_len) {
+    SOKOL_ASSERT(src && dst && (max_len > 0));
+    char* const end = &(dst[max_len-1]);
+    char c = 0;
+    for (int i = 0; i < max_len; i++) {
+        c = *src;
+        if (c != 0) {
+            src++;
+        }
+        *dst++ = c;
+    }
+    /* truncated? */
+    if (c != 0) {
+        *end = 0;
+        return false;
+    }
+    else {
+        return true;
+    }
+}
+
+_SOKOL_PRIVATE sapp_desc _sapp_desc_defaults(const sapp_desc* in_desc) {
+    sapp_desc desc = *in_desc;
+    desc.width = _sapp_def(desc.width, 640);
+    desc.height = _sapp_def(desc.height, 480);
+    desc.sample_count = _sapp_def(desc.sample_count, 1);
+    desc.swap_interval = _sapp_def(desc.swap_interval, 1);
+    desc.html5_canvas_name = _sapp_def(desc.html5_canvas_name, "canvas");
+    desc.clipboard_size = _sapp_def(desc.clipboard_size, 8192);
+    desc.max_dropped_files = _sapp_def(desc.max_dropped_files, 1);
+    desc.max_dropped_file_path_length = _sapp_def(desc.max_dropped_file_path_length, 2048);
+    desc.window_title = _sapp_def(desc.window_title, "sokol_app");
+    return desc;
+}
+
+_SOKOL_PRIVATE void _sapp_init_state(const sapp_desc* desc) {
+    _SAPP_CLEAR(_sapp_t, _sapp);
+    _sapp.desc = _sapp_desc_defaults(desc);
+    _sapp.first_frame = true;
+    _sapp.window_width = _sapp.desc.width;
+    _sapp.window_height = _sapp.desc.height;
+    _sapp.framebuffer_width = _sapp.window_width;
+    _sapp.framebuffer_height = _sapp.window_height;
+    _sapp.sample_count = _sapp.desc.sample_count;
+    _sapp.swap_interval = _sapp.desc.swap_interval;
+    _sapp.html5_canvas_selector[0] = '#';
+    _sapp_strcpy(_sapp.desc.html5_canvas_name, &_sapp.html5_canvas_selector[1], sizeof(_sapp.html5_canvas_selector) - 1);
+    _sapp.desc.html5_canvas_name = &_sapp.html5_canvas_selector[1];
+    _sapp.html5_ask_leave_site = _sapp.desc.html5_ask_leave_site;
+    _sapp.clipboard.enabled = _sapp.desc.enable_clipboard;
+    if (_sapp.clipboard.enabled) {
+        _sapp.clipboard.buf_size = _sapp.desc.clipboard_size;
+        _sapp.clipboard.buffer = (char*) SOKOL_CALLOC(1, (size_t)_sapp.clipboard.buf_size);
+    }
+    _sapp.drop.enabled = _sapp.desc.enable_dragndrop;
+    if (_sapp.drop.enabled) {
+        _sapp.drop.max_files = _sapp.desc.max_dropped_files;
+        _sapp.drop.max_path_length = _sapp.desc.max_dropped_file_path_length;
+        _sapp.drop.buf_size = _sapp.drop.max_files * _sapp.drop.max_path_length;
+        _sapp.drop.buffer = (char*) SOKOL_CALLOC(1, (size_t)_sapp.drop.buf_size);
+    }
+    _sapp_strcpy(_sapp.desc.window_title, _sapp.window_title, sizeof(_sapp.window_title));
+    _sapp.desc.window_title = _sapp.window_title;
+    _sapp.dpi_scale = 1.0f;
+    _sapp.fullscreen = _sapp.desc.fullscreen;
+    _sapp.mouse.shown = true;
+}
+
+_SOKOL_PRIVATE void _sapp_discard_state(void) {
+    if (_sapp.clipboard.enabled) {
+        SOKOL_ASSERT(_sapp.clipboard.buffer);
+        SOKOL_FREE((void*)_sapp.clipboard.buffer);
+    }
+    if (_sapp.drop.enabled) {
+        SOKOL_ASSERT(_sapp.drop.buffer);
+        SOKOL_FREE((void*)_sapp.drop.buffer);
+    }
+    if (_sapp.default_icon_pixels) {
+        SOKOL_FREE((void*)_sapp.default_icon_pixels);
+    }
+    _SAPP_CLEAR(_sapp_t, _sapp);
+}
+
+_SOKOL_PRIVATE void _sapp_init_event(sapp_event_type type) {
+    memset(&_sapp.event, 0, sizeof(_sapp.event));
+    _sapp.event.type = type;
+    _sapp.event.frame_count = _sapp.frame_count;
+    _sapp.event.mouse_button = SAPP_MOUSEBUTTON_INVALID;
+    _sapp.event.window_width = _sapp.window_width;
+    _sapp.event.window_height = _sapp.window_height;
+    _sapp.event.framebuffer_width = _sapp.framebuffer_width;
+    _sapp.event.framebuffer_height = _sapp.framebuffer_height;
+    _sapp.event.mouse_x = _sapp.mouse.x;
+    _sapp.event.mouse_y = _sapp.mouse.y;
+    _sapp.event.mouse_dx = _sapp.mouse.dx;
+    _sapp.event.mouse_dy = _sapp.mouse.dy;
+}
+
+_SOKOL_PRIVATE bool _sapp_events_enabled(void) {
+    /* only send events when an event callback is set, and the init function was called */
+    return (_sapp.desc.event_cb || _sapp.desc.event_userdata_cb) && _sapp.init_called;
+}
+
+_SOKOL_PRIVATE sapp_keycode _sapp_translate_key(int scan_code) {
+    if ((scan_code >= 0) && (scan_code < SAPP_MAX_KEYCODES)) {
+        return _sapp.keycodes[scan_code];
+    }
+    else {
+        return SAPP_KEYCODE_INVALID;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_clear_drop_buffer(void) {
+    if (_sapp.drop.enabled) {
+        SOKOL_ASSERT(_sapp.drop.buffer);
+        memset(_sapp.drop.buffer, 0, (size_t)_sapp.drop.buf_size);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_frame(void) {
+    if (_sapp.first_frame) {
+        _sapp.first_frame = false;
+        _sapp_call_init();
+    }
+    _sapp_call_frame();
+    _sapp.frame_count++;
+}
+
+_SOKOL_PRIVATE bool _sapp_image_validate(const sapp_image_desc* desc) {
+    SOKOL_ASSERT(desc->width > 0);
+    SOKOL_ASSERT(desc->height > 0);
+    SOKOL_ASSERT(desc->pixels.ptr != 0);
+    SOKOL_ASSERT(desc->pixels.size > 0);
+    const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t);
+    if (wh_size != desc->pixels.size) {
+        SOKOL_LOG("Image data size mismatch (must be width*height*4 bytes)\n");
+        return false;
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE int _sapp_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) {
+    int least_diff = 0x7FFFFFFF;
+    int least_index = 0;
+    for (int i = 0; i < num_images; i++) {
+        int diff = (image_descs[i].width * image_descs[i].height) - (width * height);
+        if (diff < 0) {
+            diff = -diff;
+        }
+        if (diff < least_diff) {
+            least_diff = diff;
+            least_index = i;
+        }
+    }
+    return least_index;
+}
+
+_SOKOL_PRIVATE int _sapp_icon_num_images(const sapp_icon_desc* desc) {
+    int index = 0;
+    for (; index < SAPP_MAX_ICONIMAGES; index++) {
+        if (0 == desc->images[index].pixels.ptr) {
+            break;
+        }
+    }
+    return index;
+}
+
+_SOKOL_PRIVATE bool _sapp_validate_icon_desc(const sapp_icon_desc* desc, int num_images) {
+    SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES);
+    for (int i = 0; i < num_images; i++) {
+        const sapp_image_desc* img_desc = &desc->images[i];
+        if (!_sapp_image_validate(img_desc)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE void _sapp_setup_default_icon(void) {
+    SOKOL_ASSERT(0 == _sapp.default_icon_pixels);
+
+    const int num_icons = 3;
+    const int icon_sizes[3] = { 16, 32, 64 };   // must be multiple of 8!
+
+    // allocate a pixel buffer for all icon pixels
+    int all_num_pixels = 0;
+    for (int i = 0; i < num_icons; i++) {
+        all_num_pixels += icon_sizes[i] * icon_sizes[i];
+    }
+    _sapp.default_icon_pixels = (uint32_t*) SOKOL_CALLOC((size_t)all_num_pixels, sizeof(uint32_t));
+
+    // initialize default_icon_desc struct
+    uint32_t* dst = _sapp.default_icon_pixels;
+    const uint32_t* dst_end = dst + all_num_pixels;
+    (void)dst_end; // silence unused warning in release mode
+    for (int i = 0; i < num_icons; i++) {
+        const int dim = (int) icon_sizes[i];
+        const int num_pixels = dim * dim;
+        sapp_image_desc* img_desc = &_sapp.default_icon_desc.images[i];
+        img_desc->width = dim;
+        img_desc->height = dim;
+        img_desc->pixels.ptr = dst;
+        img_desc->pixels.size = (size_t)num_pixels * sizeof(uint32_t);
+        dst += num_pixels;
+    }
+    SOKOL_ASSERT(dst == dst_end);
+
+    // Amstrad CPC font 'S'
+    const uint8_t tile[8] = {
+        0x3C,
+        0x66,
+        0x60,
+        0x3C,
+        0x06,
+        0x66,
+        0x3C,
+        0x00,
+    };
+    // rainbow colors
+    const uint32_t colors[8] = {
+        0xFF4370FF,
+        0xFF26A7FF,
+        0xFF58EEFF,
+        0xFF57E1D4,
+        0xFF65CC9C,
+        0xFF6ABB66,
+        0xFFF5A542,
+        0xFFC2577E,
+    };
+    dst = _sapp.default_icon_pixels;
+    const uint32_t blank = 0x00FFFFFF;
+    const uint32_t shadow = 0xFF000000;
+    for (int i = 0; i < num_icons; i++) {
+        const int dim = icon_sizes[i];
+        SOKOL_ASSERT((dim % 8) == 0);
+        const int scale = dim / 8;
+        for (int ty = 0, y = 0; ty < 8; ty++) {
+            const uint32_t color = colors[ty];
+            for (int sy = 0; sy < scale; sy++, y++) {
+                uint8_t bits = tile[ty];
+                for (int tx = 0, x = 0; tx < 8; tx++, bits<<=1) {
+                    uint32_t pixel = (0 == (bits & 0x80)) ? blank : color;
+                    for (int sx = 0; sx < scale; sx++, x++) {
+                        SOKOL_ASSERT(dst < dst_end);
+                        *dst++ = pixel;
+                    }
+                }
+            }
+        }
+    }
+    SOKOL_ASSERT(dst == dst_end);
+
+    // right shadow
+    dst = _sapp.default_icon_pixels;
+    for (int i = 0; i < num_icons; i++) {
+        const int dim = icon_sizes[i];
+        for (int y = 0; y < dim; y++) {
+            uint32_t prev_color = blank;
+            for (int x = 0; x < dim; x++) {
+                const int dst_index = y * dim + x;
+                const uint32_t cur_color = dst[dst_index];
+                if ((cur_color == blank) && (prev_color != blank)) {
+                    dst[dst_index] = shadow;
+                }
+                prev_color = cur_color;
+            }
+        }
+        dst += dim * dim;
+    }
+    SOKOL_ASSERT(dst == dst_end);
+
+    // bottom shadow
+    dst = _sapp.default_icon_pixels;
+    for (int i = 0; i < num_icons; i++) {
+        const int dim = icon_sizes[i];
+        for (int x = 0; x < dim; x++) {
+            uint32_t prev_color = blank;
+            for (int y = 0; y < dim; y++) {
+                const int dst_index = y * dim + x;
+                const uint32_t cur_color = dst[dst_index];
+                if ((cur_color == blank) && (prev_color != blank)) {
+                    dst[dst_index] = shadow;
+                }
+                prev_color = cur_color;
+            }
+        }
+        dst += dim * dim;
+    }
+    SOKOL_ASSERT(dst == dst_end);
+}
+
+/*== MacOS/iOS ===============================================================*/
+#if defined(_SAPP_APPLE)
+
+#if __has_feature(objc_arc)
+#define _SAPP_OBJC_RELEASE(obj) { obj = nil; }
+#else
+#define _SAPP_OBJC_RELEASE(obj) { [obj release]; obj = nil; }
+#endif
+
+/*== MacOS ===================================================================*/
+#if defined(_SAPP_MACOS)
+
+_SOKOL_PRIVATE void _sapp_macos_init_keytable(void) {
+    _sapp.keycodes[0x1D] = SAPP_KEYCODE_0;
+    _sapp.keycodes[0x12] = SAPP_KEYCODE_1;
+    _sapp.keycodes[0x13] = SAPP_KEYCODE_2;
+    _sapp.keycodes[0x14] = SAPP_KEYCODE_3;
+    _sapp.keycodes[0x15] = SAPP_KEYCODE_4;
+    _sapp.keycodes[0x17] = SAPP_KEYCODE_5;
+    _sapp.keycodes[0x16] = SAPP_KEYCODE_6;
+    _sapp.keycodes[0x1A] = SAPP_KEYCODE_7;
+    _sapp.keycodes[0x1C] = SAPP_KEYCODE_8;
+    _sapp.keycodes[0x19] = SAPP_KEYCODE_9;
+    _sapp.keycodes[0x00] = SAPP_KEYCODE_A;
+    _sapp.keycodes[0x0B] = SAPP_KEYCODE_B;
+    _sapp.keycodes[0x08] = SAPP_KEYCODE_C;
+    _sapp.keycodes[0x02] = SAPP_KEYCODE_D;
+    _sapp.keycodes[0x0E] = SAPP_KEYCODE_E;
+    _sapp.keycodes[0x03] = SAPP_KEYCODE_F;
+    _sapp.keycodes[0x05] = SAPP_KEYCODE_G;
+    _sapp.keycodes[0x04] = SAPP_KEYCODE_H;
+    _sapp.keycodes[0x22] = SAPP_KEYCODE_I;
+    _sapp.keycodes[0x26] = SAPP_KEYCODE_J;
+    _sapp.keycodes[0x28] = SAPP_KEYCODE_K;
+    _sapp.keycodes[0x25] = SAPP_KEYCODE_L;
+    _sapp.keycodes[0x2E] = SAPP_KEYCODE_M;
+    _sapp.keycodes[0x2D] = SAPP_KEYCODE_N;
+    _sapp.keycodes[0x1F] = SAPP_KEYCODE_O;
+    _sapp.keycodes[0x23] = SAPP_KEYCODE_P;
+    _sapp.keycodes[0x0C] = SAPP_KEYCODE_Q;
+    _sapp.keycodes[0x0F] = SAPP_KEYCODE_R;
+    _sapp.keycodes[0x01] = SAPP_KEYCODE_S;
+    _sapp.keycodes[0x11] = SAPP_KEYCODE_T;
+    _sapp.keycodes[0x20] = SAPP_KEYCODE_U;
+    _sapp.keycodes[0x09] = SAPP_KEYCODE_V;
+    _sapp.keycodes[0x0D] = SAPP_KEYCODE_W;
+    _sapp.keycodes[0x07] = SAPP_KEYCODE_X;
+    _sapp.keycodes[0x10] = SAPP_KEYCODE_Y;
+    _sapp.keycodes[0x06] = SAPP_KEYCODE_Z;
+    _sapp.keycodes[0x27] = SAPP_KEYCODE_APOSTROPHE;
+    _sapp.keycodes[0x2A] = SAPP_KEYCODE_BACKSLASH;
+    _sapp.keycodes[0x2B] = SAPP_KEYCODE_COMMA;
+    _sapp.keycodes[0x18] = SAPP_KEYCODE_EQUAL;
+    _sapp.keycodes[0x32] = SAPP_KEYCODE_GRAVE_ACCENT;
+    _sapp.keycodes[0x21] = SAPP_KEYCODE_LEFT_BRACKET;
+    _sapp.keycodes[0x1B] = SAPP_KEYCODE_MINUS;
+    _sapp.keycodes[0x2F] = SAPP_KEYCODE_PERIOD;
+    _sapp.keycodes[0x1E] = SAPP_KEYCODE_RIGHT_BRACKET;
+    _sapp.keycodes[0x29] = SAPP_KEYCODE_SEMICOLON;
+    _sapp.keycodes[0x2C] = SAPP_KEYCODE_SLASH;
+    _sapp.keycodes[0x0A] = SAPP_KEYCODE_WORLD_1;
+    _sapp.keycodes[0x33] = SAPP_KEYCODE_BACKSPACE;
+    _sapp.keycodes[0x39] = SAPP_KEYCODE_CAPS_LOCK;
+    _sapp.keycodes[0x75] = SAPP_KEYCODE_DELETE;
+    _sapp.keycodes[0x7D] = SAPP_KEYCODE_DOWN;
+    _sapp.keycodes[0x77] = SAPP_KEYCODE_END;
+    _sapp.keycodes[0x24] = SAPP_KEYCODE_ENTER;
+    _sapp.keycodes[0x35] = SAPP_KEYCODE_ESCAPE;
+    _sapp.keycodes[0x7A] = SAPP_KEYCODE_F1;
+    _sapp.keycodes[0x78] = SAPP_KEYCODE_F2;
+    _sapp.keycodes[0x63] = SAPP_KEYCODE_F3;
+    _sapp.keycodes[0x76] = SAPP_KEYCODE_F4;
+    _sapp.keycodes[0x60] = SAPP_KEYCODE_F5;
+    _sapp.keycodes[0x61] = SAPP_KEYCODE_F6;
+    _sapp.keycodes[0x62] = SAPP_KEYCODE_F7;
+    _sapp.keycodes[0x64] = SAPP_KEYCODE_F8;
+    _sapp.keycodes[0x65] = SAPP_KEYCODE_F9;
+    _sapp.keycodes[0x6D] = SAPP_KEYCODE_F10;
+    _sapp.keycodes[0x67] = SAPP_KEYCODE_F11;
+    _sapp.keycodes[0x6F] = SAPP_KEYCODE_F12;
+    _sapp.keycodes[0x69] = SAPP_KEYCODE_F13;
+    _sapp.keycodes[0x6B] = SAPP_KEYCODE_F14;
+    _sapp.keycodes[0x71] = SAPP_KEYCODE_F15;
+    _sapp.keycodes[0x6A] = SAPP_KEYCODE_F16;
+    _sapp.keycodes[0x40] = SAPP_KEYCODE_F17;
+    _sapp.keycodes[0x4F] = SAPP_KEYCODE_F18;
+    _sapp.keycodes[0x50] = SAPP_KEYCODE_F19;
+    _sapp.keycodes[0x5A] = SAPP_KEYCODE_F20;
+    _sapp.keycodes[0x73] = SAPP_KEYCODE_HOME;
+    _sapp.keycodes[0x72] = SAPP_KEYCODE_INSERT;
+    _sapp.keycodes[0x7B] = SAPP_KEYCODE_LEFT;
+    _sapp.keycodes[0x3A] = SAPP_KEYCODE_LEFT_ALT;
+    _sapp.keycodes[0x3B] = SAPP_KEYCODE_LEFT_CONTROL;
+    _sapp.keycodes[0x38] = SAPP_KEYCODE_LEFT_SHIFT;
+    _sapp.keycodes[0x37] = SAPP_KEYCODE_LEFT_SUPER;
+    _sapp.keycodes[0x6E] = SAPP_KEYCODE_MENU;
+    _sapp.keycodes[0x47] = SAPP_KEYCODE_NUM_LOCK;
+    _sapp.keycodes[0x79] = SAPP_KEYCODE_PAGE_DOWN;
+    _sapp.keycodes[0x74] = SAPP_KEYCODE_PAGE_UP;
+    _sapp.keycodes[0x7C] = SAPP_KEYCODE_RIGHT;
+    _sapp.keycodes[0x3D] = SAPP_KEYCODE_RIGHT_ALT;
+    _sapp.keycodes[0x3E] = SAPP_KEYCODE_RIGHT_CONTROL;
+    _sapp.keycodes[0x3C] = SAPP_KEYCODE_RIGHT_SHIFT;
+    _sapp.keycodes[0x36] = SAPP_KEYCODE_RIGHT_SUPER;
+    _sapp.keycodes[0x31] = SAPP_KEYCODE_SPACE;
+    _sapp.keycodes[0x30] = SAPP_KEYCODE_TAB;
+    _sapp.keycodes[0x7E] = SAPP_KEYCODE_UP;
+    _sapp.keycodes[0x52] = SAPP_KEYCODE_KP_0;
+    _sapp.keycodes[0x53] = SAPP_KEYCODE_KP_1;
+    _sapp.keycodes[0x54] = SAPP_KEYCODE_KP_2;
+    _sapp.keycodes[0x55] = SAPP_KEYCODE_KP_3;
+    _sapp.keycodes[0x56] = SAPP_KEYCODE_KP_4;
+    _sapp.keycodes[0x57] = SAPP_KEYCODE_KP_5;
+    _sapp.keycodes[0x58] = SAPP_KEYCODE_KP_6;
+    _sapp.keycodes[0x59] = SAPP_KEYCODE_KP_7;
+    _sapp.keycodes[0x5B] = SAPP_KEYCODE_KP_8;
+    _sapp.keycodes[0x5C] = SAPP_KEYCODE_KP_9;
+    _sapp.keycodes[0x45] = SAPP_KEYCODE_KP_ADD;
+    _sapp.keycodes[0x41] = SAPP_KEYCODE_KP_DECIMAL;
+    _sapp.keycodes[0x4B] = SAPP_KEYCODE_KP_DIVIDE;
+    _sapp.keycodes[0x4C] = SAPP_KEYCODE_KP_ENTER;
+    _sapp.keycodes[0x51] = SAPP_KEYCODE_KP_EQUAL;
+    _sapp.keycodes[0x43] = SAPP_KEYCODE_KP_MULTIPLY;
+    _sapp.keycodes[0x4E] = SAPP_KEYCODE_KP_SUBTRACT;
+}
+
+_SOKOL_PRIVATE void _sapp_macos_discard_state(void) {
+    // NOTE: it's safe to call [release] on a nil object
+    _SAPP_OBJC_RELEASE(_sapp.macos.tracking_area);
+    _SAPP_OBJC_RELEASE(_sapp.macos.app_dlg);
+    _SAPP_OBJC_RELEASE(_sapp.macos.win_dlg);
+    _SAPP_OBJC_RELEASE(_sapp.macos.view);
+    #if defined(SOKOL_METAL)
+        _SAPP_OBJC_RELEASE(_sapp.macos.mtl_device);
+    #endif
+    _SAPP_OBJC_RELEASE(_sapp.macos.window);
+}
+
+_SOKOL_PRIVATE void _sapp_macos_run(const sapp_desc* desc) {
+    _sapp_init_state(desc);
+    _sapp_macos_init_keytable();
+    [NSApplication sharedApplication];
+    // set the application dock icon as early as possible, otherwise
+    // the dummy icon will be visible for a short time
+    sapp_set_icon(&_sapp.desc.icon);
+    _sapp.macos.app_dlg = [[_sapp_macos_app_delegate alloc] init];
+    NSApp.delegate = _sapp.macos.app_dlg;
+    [NSApp run];
+    // NOTE: [NSApp run] never returns, instead cleanup code
+    // must be put into applicationWillTerminate
+}
+
+/* MacOS entry function */
+#if !defined(SOKOL_NO_ENTRY)
+int main(int argc, char* argv[]) {
+    sapp_desc desc = sokol_main(argc, argv);
+    _sapp_macos_run(&desc);
+    return 0;
+}
+#endif /* SOKOL_NO_ENTRY */
+
+_SOKOL_PRIVATE uint32_t _sapp_macos_mods(NSEvent* ev) {
+    const NSEventModifierFlags f = ev.modifierFlags;
+    const NSUInteger b = NSEvent.pressedMouseButtons;
+    uint32_t m = 0;
+    if (f & NSEventModifierFlagShift) {
+        m |= SAPP_MODIFIER_SHIFT;
+    }
+    if (f & NSEventModifierFlagControl) {
+        m |= SAPP_MODIFIER_CTRL;
+    }
+    if (f & NSEventModifierFlagOption) {
+        m |= SAPP_MODIFIER_ALT;
+    }
+    if (f & NSEventModifierFlagCommand) {
+        m |= SAPP_MODIFIER_SUPER;
+    }
+    if (0 != (b & (1<<0))) {
+        m |= SAPP_MODIFIER_LMB;
+    }
+    if (0 != (b & (1<<1))) {
+        m |= SAPP_MODIFIER_RMB;
+    }
+    if (0 != (b & (1<<2))) {
+        m |= SAPP_MODIFIER_MMB;
+    }
+    return m;
+}
+
+_SOKOL_PRIVATE void _sapp_macos_mouse_event(sapp_event_type type, sapp_mousebutton btn, uint32_t mod) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.mouse_button = btn;
+        _sapp.event.modifiers = mod;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_macos_key_event(sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mod) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.key_code = key;
+        _sapp.event.key_repeat = repeat;
+        _sapp.event.modifiers = mod;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_macos_app_event(sapp_event_type type) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+/* NOTE: unlike the iOS version of this function, the macOS version
+    can dynamically update the DPI scaling factor when a window is moved
+    between HighDPI / LowDPI screens.
+*/
+_SOKOL_PRIVATE void _sapp_macos_update_dimensions(void) {
+    #if defined(SOKOL_METAL)
+        const NSRect fb_rect = [_sapp.macos.view bounds];
+        _sapp.framebuffer_width = fb_rect.size.width * _sapp.dpi_scale;
+        _sapp.framebuffer_height = fb_rect.size.height * _sapp.dpi_scale;
+    #elif defined(SOKOL_GLCORE33)
+        const NSRect fb_rect = [_sapp.macos.view convertRectToBacking:[_sapp.macos.view frame]];
+        _sapp.framebuffer_width = fb_rect.size.width;
+        _sapp.framebuffer_height = fb_rect.size.height;
+    #endif
+    const NSRect bounds = [_sapp.macos.view bounds];
+    _sapp.window_width = bounds.size.width;
+    _sapp.window_height = bounds.size.height;
+    if (_sapp.framebuffer_width == 0) {
+        _sapp.framebuffer_width = 1;
+    }
+    if (_sapp.framebuffer_height == 0) {
+        _sapp.framebuffer_height = 1;
+    }
+    if (_sapp.window_width == 0) {
+        _sapp.window_width = 1;
+    }
+    if (_sapp.window_height == 0) {
+        _sapp.window_height = 1;
+    }
+    _sapp.dpi_scale = (float)_sapp.framebuffer_width / (float)_sapp.window_width;
+
+    /* NOTE: _sapp_macos_update_dimensions() isn't called each frame, but only
+        when the window size actually changes, so resizing the MTKView's
+        in each call is fine even when MTKView doesn't ignore setting an
+        identical drawableSize.
+    */
+    #if defined(SOKOL_METAL)
+    CGSize drawable_size = { (CGFloat) _sapp.framebuffer_width, (CGFloat) _sapp.framebuffer_height };
+    _sapp.macos.view.drawableSize = drawable_size;
+    #endif
+}
+
+_SOKOL_PRIVATE void _sapp_macos_toggle_fullscreen(void) {
+    /* NOTE: the _sapp.fullscreen flag is also notified by the
+       windowDidEnterFullscreen / windowDidExitFullscreen
+       event handlers
+    */
+    _sapp.fullscreen = !_sapp.fullscreen;
+    [_sapp.macos.window toggleFullScreen:nil];
+}
+
+_SOKOL_PRIVATE void _sapp_macos_set_clipboard_string(const char* str) {
+    @autoreleasepool {
+        NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
+        [pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil];
+        [pasteboard setString:@(str) forType:NSPasteboardTypeString];
+    }
+}
+
+_SOKOL_PRIVATE const char* _sapp_macos_get_clipboard_string(void) {
+    SOKOL_ASSERT(_sapp.clipboard.buffer);
+    @autoreleasepool {
+        _sapp.clipboard.buffer[0] = 0;
+        NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
+        if (![[pasteboard types] containsObject:NSPasteboardTypeString]) {
+            return _sapp.clipboard.buffer;
+        }
+        NSString* str = [pasteboard stringForType:NSPasteboardTypeString];
+        if (!str) {
+            return _sapp.clipboard.buffer;
+        }
+        _sapp_strcpy([str UTF8String], _sapp.clipboard.buffer, _sapp.clipboard.buf_size);
+    }
+    return _sapp.clipboard.buffer;
+}
+
+_SOKOL_PRIVATE void _sapp_macos_update_window_title(void) {
+    [_sapp.macos.window setTitle: [NSString stringWithUTF8String:_sapp.window_title]];
+}
+
+_SOKOL_PRIVATE void _sapp_macos_update_mouse(NSEvent* event) {
+    if (!_sapp.mouse.locked) {
+        const NSPoint mouse_pos = event.locationInWindow;
+        float new_x = mouse_pos.x * _sapp.dpi_scale;
+        float new_y = _sapp.framebuffer_height - (mouse_pos.y * _sapp.dpi_scale) - 1;
+        /* don't update dx/dy in the very first update */
+        if (_sapp.mouse.pos_valid) {
+            _sapp.mouse.dx = new_x - _sapp.mouse.x;
+            _sapp.mouse.dy = new_y - _sapp.mouse.y;
+        }
+        _sapp.mouse.x = new_x;
+        _sapp.mouse.y = new_y;
+        _sapp.mouse.pos_valid = true;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_macos_show_mouse(bool visible) {
+    /* NOTE: this function is only called when the mouse visibility actually changes */
+    if (visible) {
+        CGDisplayShowCursor(kCGDirectMainDisplay);
+    }
+    else {
+        CGDisplayHideCursor(kCGDirectMainDisplay);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_macos_lock_mouse(bool lock) {
+    if (lock == _sapp.mouse.locked) {
+        return;
+    }
+    _sapp.mouse.dx = 0.0f;
+    _sapp.mouse.dy = 0.0f;
+    _sapp.mouse.locked = lock;
+    /*
+        NOTE that this code doesn't warp the mouse cursor to the window
+        center as everybody else does it. This lead to a spike in the
+        *second* mouse-moved event after the warp happened. The
+        mouse centering doesn't seem to be required (mouse-moved events
+        are reported correctly even when the cursor is at an edge of the screen).
+
+        NOTE also that the hide/show of the mouse cursor should properly
+        stack with calls to sapp_show_mouse()
+    */
+    if (_sapp.mouse.locked) {
+        CGAssociateMouseAndMouseCursorPosition(NO);
+        CGDisplayHideCursor(kCGDirectMainDisplay);
+    }
+    else {
+        CGDisplayShowCursor(kCGDirectMainDisplay);
+        CGAssociateMouseAndMouseCursorPosition(YES);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_macos_set_icon(const sapp_icon_desc* icon_desc, int num_images) {
+    NSDockTile* dock_tile = NSApp.dockTile;
+    const int wanted_width = (int) dock_tile.size.width;
+    const int wanted_height = (int) dock_tile.size.height;
+    const int img_index = _sapp_image_bestmatch(icon_desc->images, num_images, wanted_width, wanted_height);
+    const sapp_image_desc* img_desc = &icon_desc->images[img_index];
+
+    CGColorSpaceRef cg_color_space = CGColorSpaceCreateDeviceRGB();
+    CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)img_desc->pixels.ptr, (CFIndex)img_desc->pixels.size);
+    CGDataProviderRef cg_data_provider = CGDataProviderCreateWithCFData(cf_data);
+    CGImageRef cg_img = CGImageCreate(
+        (size_t)img_desc->width,    // width
+        (size_t)img_desc->height,   // height
+        8,                          // bitsPerComponent
+        32,                         // bitsPerPixel
+        (size_t)img_desc->width * 4,// bytesPerRow
+        cg_color_space,             // space
+        kCGImageAlphaLast | kCGImageByteOrderDefault,  // bitmapInfo
+        cg_data_provider,           // provider
+        NULL,                       // decode
+        false,                      // shouldInterpolate
+        kCGRenderingIntentDefault);
+    CFRelease(cf_data);
+    CGDataProviderRelease(cg_data_provider);
+    CGColorSpaceRelease(cg_color_space);
+
+    NSImage* ns_image = [[NSImage alloc] initWithCGImage:cg_img size:dock_tile.size];
+    dock_tile.contentView = [NSImageView imageViewWithImage:ns_image];
+    [dock_tile display];
+    _SAPP_OBJC_RELEASE(ns_image);
+    CGImageRelease(cg_img);
+}
+
+_SOKOL_PRIVATE void _sapp_macos_frame(void) {
+    _sapp_frame();
+    if (_sapp.quit_requested || _sapp.quit_ordered) {
+        [_sapp.macos.window performClose:nil];
+    }
+}
+
+@implementation _sapp_macos_app_delegate
+- (void)applicationDidFinishLaunching:(NSNotification*)aNotification {
+    _SOKOL_UNUSED(aNotification);
+    if (_sapp.fullscreen) {
+        NSRect screen_rect = NSScreen.mainScreen.frame;
+        _sapp.window_width = screen_rect.size.width;
+        _sapp.window_height = screen_rect.size.height;
+    }
+    if (_sapp.desc.high_dpi) {
+        _sapp.framebuffer_width = 2 * _sapp.window_width;
+        _sapp.framebuffer_height = 2 * _sapp.window_height;
+    }
+    else {
+        _sapp.framebuffer_width = _sapp.window_width;
+        _sapp.framebuffer_height = _sapp.window_height;
+    }
+    _sapp.dpi_scale = (float)_sapp.framebuffer_width / (float) _sapp.window_width;
+    const NSUInteger style =
+        NSWindowStyleMaskTitled |
+        NSWindowStyleMaskClosable |
+        NSWindowStyleMaskMiniaturizable |
+        NSWindowStyleMaskResizable;
+    NSRect window_rect = NSMakeRect(0, 0, _sapp.window_width, _sapp.window_height);
+    _sapp.macos.window = [[_sapp_macos_window alloc]
+        initWithContentRect:window_rect
+        styleMask:style
+        backing:NSBackingStoreBuffered
+        defer:NO];
+    _sapp.macos.window.releasedWhenClosed = NO; // this is necessary for proper cleanup in applicationWillTerminate
+    _sapp.macos.window.title = [NSString stringWithUTF8String:_sapp.window_title];
+    _sapp.macos.window.acceptsMouseMovedEvents = YES;
+    _sapp.macos.window.restorable = YES;
+
+    _sapp.macos.win_dlg = [[_sapp_macos_window_delegate alloc] init];
+    _sapp.macos.window.delegate = _sapp.macos.win_dlg;
+    #if defined(SOKOL_METAL)
+        _sapp.macos.mtl_device = MTLCreateSystemDefaultDevice();
+        _sapp.macos.view = [[_sapp_macos_view alloc] init];
+        [_sapp.macos.view updateTrackingAreas];
+        _sapp.macos.view.preferredFramesPerSecond = 60 / _sapp.swap_interval;
+        _sapp.macos.view.device = _sapp.macos.mtl_device;
+        _sapp.macos.view.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
+        _sapp.macos.view.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
+        _sapp.macos.view.sampleCount = (NSUInteger) _sapp.sample_count;
+        _sapp.macos.view.autoResizeDrawable = false;
+        _sapp.macos.window.contentView = _sapp.macos.view;
+        [_sapp.macos.window makeFirstResponder:_sapp.macos.view];
+        _sapp.macos.view.layer.magnificationFilter = kCAFilterNearest;
+    #elif defined(SOKOL_GLCORE33)
+        NSOpenGLPixelFormatAttribute attrs[32];
+        int i = 0;
+        attrs[i++] = NSOpenGLPFAAccelerated;
+        attrs[i++] = NSOpenGLPFADoubleBuffer;
+        attrs[i++] = NSOpenGLPFAOpenGLProfile; attrs[i++] = NSOpenGLProfileVersion3_2Core;
+        attrs[i++] = NSOpenGLPFAColorSize; attrs[i++] = 24;
+        attrs[i++] = NSOpenGLPFAAlphaSize; attrs[i++] = 8;
+        attrs[i++] = NSOpenGLPFADepthSize; attrs[i++] = 24;
+        attrs[i++] = NSOpenGLPFAStencilSize; attrs[i++] = 8;
+        if (_sapp.sample_count > 1) {
+            attrs[i++] = NSOpenGLPFAMultisample;
+            attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 1;
+            attrs[i++] = NSOpenGLPFASamples; attrs[i++] = (NSOpenGLPixelFormatAttribute)_sapp.sample_count;
+        }
+        else {
+            attrs[i++] = NSOpenGLPFASampleBuffers; attrs[i++] = 0;
+        }
+        attrs[i++] = 0;
+        NSOpenGLPixelFormat* glpixelformat_obj = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
+        SOKOL_ASSERT(glpixelformat_obj != nil);
+
+        _sapp.macos.view = [[_sapp_macos_view alloc]
+            initWithFrame:window_rect
+            pixelFormat:glpixelformat_obj];
+        _SAPP_OBJC_RELEASE(glpixelformat_obj);
+        [_sapp.macos.view updateTrackingAreas];
+        if (_sapp.desc.high_dpi) {
+            [_sapp.macos.view setWantsBestResolutionOpenGLSurface:YES];
+        }
+        else {
+            [_sapp.macos.view setWantsBestResolutionOpenGLSurface:NO];
+        }
+
+        _sapp.macos.window.contentView = _sapp.macos.view;
+        [_sapp.macos.window makeFirstResponder:_sapp.macos.view];
+
+        NSTimer* timer_obj = [NSTimer timerWithTimeInterval:0.001
+            target:_sapp.macos.view
+            selector:@selector(timerFired:)
+            userInfo:nil
+            repeats:YES];
+        [[NSRunLoop currentRunLoop] addTimer:timer_obj forMode:NSDefaultRunLoopMode];
+        timer_obj = nil;
+    #endif
+    _sapp.valid = true;
+    if (_sapp.fullscreen) {
+        /* on GL, this already toggles a rendered frame, so set the valid flag before */
+        [_sapp.macos.window toggleFullScreen:self];
+    }
+    else {
+        [_sapp.macos.window center];
+    }
+    NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
+    [NSApp activateIgnoringOtherApps:YES];
+    [_sapp.macos.window makeKeyAndOrderFront:nil];
+    _sapp_macos_update_dimensions();
+    [NSEvent setMouseCoalescingEnabled:NO];
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender {
+    _SOKOL_UNUSED(sender);
+    return YES;
+}
+
+- (void)applicationWillTerminate:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp_call_cleanup();
+    _sapp_macos_discard_state();
+    _sapp_discard_state();
+}
+@end
+
+@implementation _sapp_macos_window_delegate
+- (BOOL)windowShouldClose:(id)sender {
+    _SOKOL_UNUSED(sender);
+    /* only give user-code a chance to intervene when sapp_quit() wasn't already called */
+    if (!_sapp.quit_ordered) {
+        /* if window should be closed and event handling is enabled, give user code
+           a chance to intervene via sapp_cancel_quit()
+        */
+        _sapp.quit_requested = true;
+        _sapp_macos_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED);
+        /* user code hasn't intervened, quit the app */
+        if (_sapp.quit_requested) {
+            _sapp.quit_ordered = true;
+        }
+    }
+    if (_sapp.quit_ordered) {
+        return YES;
+    }
+    else {
+        return NO;
+    }
+}
+
+- (void)windowDidResize:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp_macos_update_dimensions();
+    if (!_sapp.first_frame) {
+        _sapp_macos_app_event(SAPP_EVENTTYPE_RESIZED);
+    }
+}
+
+- (void)windowDidMiniaturize:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp_macos_app_event(SAPP_EVENTTYPE_ICONIFIED);
+}
+
+- (void)windowDidDeminiaturize:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp_macos_app_event(SAPP_EVENTTYPE_RESTORED);
+}
+
+- (void)windowDidEnterFullScreen:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp.fullscreen = true;
+}
+
+- (void)windowDidExitFullScreen:(NSNotification*)notification {
+    _SOKOL_UNUSED(notification);
+    _sapp.fullscreen = false;
+}
+@end
+
+@implementation _sapp_macos_window
+- (instancetype)initWithContentRect:(NSRect)contentRect
+                          styleMask:(NSWindowStyleMask)style
+                            backing:(NSBackingStoreType)backingStoreType
+                              defer:(BOOL)flag {
+    if (self = [super initWithContentRect:contentRect styleMask:style backing:backingStoreType defer:flag]) {
+        #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
+            [self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeFileURL]];
+        #endif
+    }
+    return self;
+}
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
+    return NSDragOperationCopy;
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender {
+    return NSDragOperationCopy;
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
+    #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
+    NSPasteboard *pboard = [sender draggingPasteboard];
+    if ([pboard.types containsObject:NSPasteboardTypeFileURL]) {
+        _sapp_clear_drop_buffer();
+        _sapp.drop.num_files = ((int)pboard.pasteboardItems.count > _sapp.drop.max_files) ? _sapp.drop.max_files : pboard.pasteboardItems.count;
+        bool drop_failed = false;
+        for (int i = 0; i < _sapp.drop.num_files; i++) {
+            NSURL *fileUrl = [NSURL fileURLWithPath:[pboard.pasteboardItems[(NSUInteger)i] stringForType:NSPasteboardTypeFileURL]];
+            if (!_sapp_strcpy(fileUrl.standardizedURL.path.UTF8String, _sapp_dropped_file_path_ptr(i), _sapp.drop.max_path_length)) {
+                SOKOL_LOG("sokol_app.h: dropped file path too long (sapp_desc.max_dropped_file_path_length)\n");
+                drop_failed = true;
+                break;
+            }
+        }
+        if (!drop_failed) {
+            if (_sapp_events_enabled()) {
+                _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED);
+                _sapp_call_event(&_sapp.event);
+            }
+        }
+        else {
+            _sapp_clear_drop_buffer();
+            _sapp.drop.num_files = 0;
+        }
+        return YES;
+    }
+    #endif
+    return NO;
+}
+@end
+
+@implementation _sapp_macos_view
+#if defined(SOKOL_GLCORE33)
+/* NOTE: this is a hack/fix when the initial window size has been clipped by
+    macOS because it didn't fit on the screen, in that case the
+    frame size of the window is reported wrong if low-dpi rendering
+    was requested (instead the high-dpi dimensions are returned)
+    until the window is resized for the first time.
+
+    Hooking into reshape and getting the frame dimensions seems to report
+    the correct dimensions.
+*/
+- (void)reshape {
+    _sapp_macos_update_dimensions();
+    [super reshape];
+}
+- (void)timerFired:(id)sender {
+    _SOKOL_UNUSED(sender);
+    [self setNeedsDisplay:YES];
+}
+- (void)prepareOpenGL {
+    [super prepareOpenGL];
+    GLint swapInt = 1;
+    NSOpenGLContext* ctx = [_sapp.macos.view openGLContext];
+    [ctx setValues:&swapInt forParameter:NSOpenGLContextParameterSwapInterval];
+    [ctx makeCurrentContext];
+}
+#endif
+
+_SOKOL_PRIVATE void _sapp_macos_poll_input_events() {
+    /*
+
+    NOTE: late event polling temporarily out-commented to check if this
+    causes infrequent and almost impossible to reproduce probelms with the
+    window close events, see:
+    https://github.com/floooh/sokol/pull/483#issuecomment-805148815
+
+
+    const NSEventMask mask = NSEventMaskLeftMouseDown |
+                             NSEventMaskLeftMouseUp|
+                             NSEventMaskRightMouseDown |
+                             NSEventMaskRightMouseUp |
+                             NSEventMaskMouseMoved |
+                             NSEventMaskLeftMouseDragged |
+                             NSEventMaskRightMouseDragged |
+                             NSEventMaskMouseEntered |
+                             NSEventMaskMouseExited |
+                             NSEventMaskKeyDown |
+                             NSEventMaskKeyUp |
+                             NSEventMaskCursorUpdate |
+                             NSEventMaskScrollWheel |
+                             NSEventMaskTabletPoint |
+                             NSEventMaskTabletProximity |
+                             NSEventMaskOtherMouseDown |
+                             NSEventMaskOtherMouseUp |
+                             NSEventMaskOtherMouseDragged |
+                             NSEventMaskPressure |
+                             NSEventMaskDirectTouch;
+    @autoreleasepool {
+        for (;;) {
+            // NOTE: using NSDefaultRunLoopMode here causes stuttering in the GL backend,
+            // see: https://github.com/floooh/sokol/issues/486
+            NSEvent* event = [NSApp nextEventMatchingMask:mask untilDate:nil inMode:NSEventTrackingRunLoopMode dequeue:YES];
+            if (event == nil) {
+                break;
+            }
+            [NSApp sendEvent:event];
+        }
+    }
+    */
+}
+
+- (void)drawRect:(NSRect)rect {
+    _SOKOL_UNUSED(rect);
+    /* Catch any last-moment input events */
+    _sapp_macos_poll_input_events();
+    @autoreleasepool {
+        _sapp_macos_frame();
+    }
+    #if !defined(SOKOL_METAL)
+    [[_sapp.macos.view openGLContext] flushBuffer];
+    #endif
+}
+
+- (BOOL)isOpaque {
+    return YES;
+}
+- (BOOL)canBecomeKeyView {
+    return YES;
+}
+- (BOOL)acceptsFirstResponder {
+    return YES;
+}
+- (void)updateTrackingAreas {
+    if (_sapp.macos.tracking_area != nil) {
+        [self removeTrackingArea:_sapp.macos.tracking_area];
+        _SAPP_OBJC_RELEASE(_sapp.macos.tracking_area);
+    }
+    const NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited |
+                                          NSTrackingActiveInKeyWindow |
+                                          NSTrackingEnabledDuringMouseDrag |
+                                          NSTrackingCursorUpdate |
+                                          NSTrackingInVisibleRect |
+                                          NSTrackingAssumeInside;
+    _sapp.macos.tracking_area = [[NSTrackingArea alloc] initWithRect:[self bounds] options:options owner:self userInfo:nil];
+    [self addTrackingArea:_sapp.macos.tracking_area];
+    [super updateTrackingAreas];
+}
+- (void)mouseEntered:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    /* don't send mouse enter/leave while dragging (so that it behaves the same as
+       on Windows while SetCapture is active
+    */
+    if (0 == _sapp.macos.mouse_buttons) {
+        _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event));
+    }
+}
+- (void)mouseExited:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (0 == _sapp.macos.mouse_buttons) {
+        _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event));
+    }
+}
+- (void)mouseDown:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT, _sapp_macos_mods(event));
+    _sapp.macos.mouse_buttons |= (1<<SAPP_MOUSEBUTTON_LEFT);
+}
+- (void)mouseUp:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_LEFT, _sapp_macos_mods(event));
+    _sapp.macos.mouse_buttons &= ~(1<<SAPP_MOUSEBUTTON_LEFT);
+}
+- (void)rightMouseDown:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_RIGHT, _sapp_macos_mods(event));
+    _sapp.macos.mouse_buttons |= (1<<SAPP_MOUSEBUTTON_RIGHT);
+}
+- (void)rightMouseUp:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_RIGHT, _sapp_macos_mods(event));
+    _sapp.macos.mouse_buttons &= ~(1<<SAPP_MOUSEBUTTON_RIGHT);
+}
+- (void)otherMouseDown:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (2 == event.buttonNumber) {
+        _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_MIDDLE, _sapp_macos_mods(event));
+        _sapp.macos.mouse_buttons |= (1<<SAPP_MOUSEBUTTON_MIDDLE);
+    }
+}
+- (void)otherMouseUp:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (2 == event.buttonNumber) {
+        _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_MIDDLE, _sapp_macos_mods(event));
+        _sapp.macos.mouse_buttons &= (1<<SAPP_MOUSEBUTTON_MIDDLE);
+    }
+}
+- (void)otherMouseDragged:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (2 == event.buttonNumber) {
+        if (_sapp.mouse.locked) {
+            _sapp.mouse.dx = [event deltaX];
+            _sapp.mouse.dy = [event deltaY];
+        }
+        _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event));
+    }
+}
+- (void)mouseMoved:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (_sapp.mouse.locked) {
+        _sapp.mouse.dx = [event deltaX];
+        _sapp.mouse.dy = [event deltaY];
+    }
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID , _sapp_macos_mods(event));
+}
+- (void)mouseDragged:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (_sapp.mouse.locked) {
+        _sapp.mouse.dx = [event deltaX];
+        _sapp.mouse.dy = [event deltaY];
+    }
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID , _sapp_macos_mods(event));
+}
+- (void)rightMouseDragged:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (_sapp.mouse.locked) {
+        _sapp.mouse.dx = [event deltaX];
+        _sapp.mouse.dy = [event deltaY];
+    }
+    _sapp_macos_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_macos_mods(event));
+}
+- (void)scrollWheel:(NSEvent*)event {
+    _sapp_macos_update_mouse(event);
+    if (_sapp_events_enabled()) {
+        float dx = (float) event.scrollingDeltaX;
+        float dy = (float) event.scrollingDeltaY;
+        if (event.hasPreciseScrollingDeltas) {
+            dx *= 0.1;
+            dy *= 0.1;
+        }
+        if ((_sapp_absf(dx) > 0.0f) || (_sapp_absf(dy) > 0.0f)) {
+            _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL);
+            _sapp.event.modifiers = _sapp_macos_mods(event);
+            _sapp.event.scroll_x = dx;
+            _sapp.event.scroll_y = dy;
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+- (void)keyDown:(NSEvent*)event {
+    if (_sapp_events_enabled()) {
+        const uint32_t mods = _sapp_macos_mods(event);
+        /* NOTE: macOS doesn't send keyUp events while the Cmd key is pressed,
+            as a workaround, to prevent key presses from sticking we'll send
+            a keyup event following right after the keydown if SUPER is also pressed
+        */
+        const sapp_keycode key_code = _sapp_translate_key(event.keyCode);
+        _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_DOWN, key_code, event.isARepeat, mods);
+        if (0 != (mods & SAPP_MODIFIER_SUPER)) {
+            _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_UP, key_code, event.isARepeat, mods);
+        }
+        const NSString* chars = event.characters;
+        const NSUInteger len = chars.length;
+        if (len > 0) {
+            _sapp_init_event(SAPP_EVENTTYPE_CHAR);
+            _sapp.event.modifiers = mods;
+            for (NSUInteger i = 0; i < len; i++) {
+                const unichar codepoint = [chars characterAtIndex:i];
+                if ((codepoint & 0xFF00) == 0xF700) {
+                    continue;
+                }
+                _sapp.event.char_code = codepoint;
+                _sapp.event.key_repeat = event.isARepeat;
+                _sapp_call_event(&_sapp.event);
+            }
+        }
+        /* if this is a Cmd+V (paste), also send a CLIPBOARD_PASTE event */
+        if (_sapp.clipboard.enabled && (mods == SAPP_MODIFIER_SUPER) && (key_code == SAPP_KEYCODE_V)) {
+            _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+- (void)keyUp:(NSEvent*)event {
+    _sapp_macos_key_event(SAPP_EVENTTYPE_KEY_UP,
+        _sapp_translate_key(event.keyCode),
+        event.isARepeat,
+        _sapp_macos_mods(event));
+}
+- (void)flagsChanged:(NSEvent*)event {
+    const uint32_t old_f = _sapp.macos.flags_changed_store;
+    const uint32_t new_f = event.modifierFlags;
+    _sapp.macos.flags_changed_store = new_f;
+    sapp_keycode key_code = SAPP_KEYCODE_INVALID;
+    bool down = false;
+    if ((new_f ^ old_f) & NSEventModifierFlagShift) {
+        key_code = SAPP_KEYCODE_LEFT_SHIFT;
+        down = 0 != (new_f & NSEventModifierFlagShift);
+    }
+    if ((new_f ^ old_f) & NSEventModifierFlagControl) {
+        key_code = SAPP_KEYCODE_LEFT_CONTROL;
+        down = 0 != (new_f & NSEventModifierFlagControl);
+    }
+    if ((new_f ^ old_f) & NSEventModifierFlagOption) {
+        key_code = SAPP_KEYCODE_LEFT_ALT;
+        down = 0 != (new_f & NSEventModifierFlagOption);
+    }
+    if ((new_f ^ old_f) & NSEventModifierFlagCommand) {
+        key_code = SAPP_KEYCODE_LEFT_SUPER;
+        down = 0 != (new_f & NSEventModifierFlagCommand);
+    }
+    if (key_code != SAPP_KEYCODE_INVALID) {
+        _sapp_macos_key_event(down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP,
+            key_code,
+            false,
+            _sapp_macos_mods(event));
+    }
+}
+- (void)cursorUpdate:(NSEvent*)event {
+    _SOKOL_UNUSED(event);
+    if (_sapp.desc.user_cursor) {
+        _sapp_macos_app_event(SAPP_EVENTTYPE_UPDATE_CURSOR);
+    }
+}
+@end
+
+#endif /* MacOS */
+
+/*== iOS =====================================================================*/
+#if defined(_SAPP_IOS)
+
+_SOKOL_PRIVATE void _sapp_ios_discard_state(void) {
+    // NOTE: it's safe to call [release] on a nil object
+    _SAPP_OBJC_RELEASE(_sapp.ios.textfield_dlg);
+    _SAPP_OBJC_RELEASE(_sapp.ios.textfield);
+    #if defined(SOKOL_METAL)
+        _SAPP_OBJC_RELEASE(_sapp.ios.view_ctrl);
+        _SAPP_OBJC_RELEASE(_sapp.ios.mtl_device);
+    #else
+        _SAPP_OBJC_RELEASE(_sapp.ios.view_ctrl);
+        _SAPP_OBJC_RELEASE(_sapp.ios.eagl_ctx);
+    #endif
+    _SAPP_OBJC_RELEASE(_sapp.ios.view);
+    _SAPP_OBJC_RELEASE(_sapp.ios.window);
+}
+
+_SOKOL_PRIVATE void _sapp_ios_run(const sapp_desc* desc) {
+    _sapp_init_state(desc);
+    static int argc = 1;
+    static char* argv[] = { (char*)"sokol_app" };
+    UIApplicationMain(argc, argv, nil, NSStringFromClass([_sapp_app_delegate class]));
+}
+
+/* iOS entry function */
+#if !defined(SOKOL_NO_ENTRY)
+int main(int argc, char* argv[]) {
+    sapp_desc desc = sokol_main(argc, argv);
+    _sapp_ios_run(&desc);
+    return 0;
+}
+#endif /* SOKOL_NO_ENTRY */
+
+_SOKOL_PRIVATE void _sapp_ios_app_event(sapp_event_type type) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_ios_touch_event(sapp_event_type type, NSSet<UITouch *>* touches, UIEvent* event) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        NSEnumerator* enumerator = event.allTouches.objectEnumerator;
+        UITouch* ios_touch;
+        while ((ios_touch = [enumerator nextObject])) {
+            if ((_sapp.event.num_touches + 1) < SAPP_MAX_TOUCHPOINTS) {
+                CGPoint ios_pos = [ios_touch locationInView:_sapp.ios.view];
+                sapp_touchpoint* cur_point = &_sapp.event.touches[_sapp.event.num_touches++];
+                cur_point->identifier = (uintptr_t) ios_touch;
+                cur_point->pos_x = ios_pos.x * _sapp.dpi_scale;
+                cur_point->pos_y = ios_pos.y * _sapp.dpi_scale;
+                cur_point->changed = [touches containsObject:ios_touch];
+            }
+        }
+        if (_sapp.event.num_touches > 0) {
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_ios_update_dimensions(void) {
+    CGRect screen_rect = UIScreen.mainScreen.bounds;
+    _sapp.framebuffer_width = (int)(screen_rect.size.width * _sapp.dpi_scale);
+    _sapp.framebuffer_height = (int)(screen_rect.size.height * _sapp.dpi_scale);
+    _sapp.window_width = (int)screen_rect.size.width;
+    _sapp.window_height = (int)screen_rect.size.height;
+    int cur_fb_width, cur_fb_height;
+    #if defined(SOKOL_METAL)
+        const CGSize fb_size = _sapp.ios.view.drawableSize;
+        cur_fb_width = (int) fb_size.width;
+        cur_fb_height = (int) fb_size.height;
+    #else
+        cur_fb_width = (int) _sapp.ios.view.drawableWidth;
+        cur_fb_height = (int) _sapp.ios.view.drawableHeight;
+    #endif
+    const bool dim_changed = (_sapp.framebuffer_width != cur_fb_width) ||
+                             (_sapp.framebuffer_height != cur_fb_height);
+    if (dim_changed) {
+        #if defined(SOKOL_METAL)
+            const CGSize drawable_size = { (CGFloat) _sapp.framebuffer_width, (CGFloat) _sapp.framebuffer_height };
+            _sapp.ios.view.drawableSize = drawable_size;
+        #else
+            // nothing to do here, GLKView correctly respects the view's contentScaleFactor
+        #endif
+        if (!_sapp.first_frame) {
+            _sapp_ios_app_event(SAPP_EVENTTYPE_RESIZED);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_ios_frame(void) {
+    _sapp_ios_update_dimensions();
+    _sapp_frame();
+}
+
+_SOKOL_PRIVATE void _sapp_ios_show_keyboard(bool shown) {
+    /* if not happened yet, create an invisible text field */
+    if (nil == _sapp.ios.textfield) {
+        _sapp.ios.textfield_dlg = [[_sapp_textfield_dlg alloc] init];
+        _sapp.ios.textfield = [[UITextField alloc] initWithFrame:CGRectMake(10, 10, 100, 50)];
+        _sapp.ios.textfield.keyboardType = UIKeyboardTypeDefault;
+        _sapp.ios.textfield.returnKeyType = UIReturnKeyDefault;
+        _sapp.ios.textfield.autocapitalizationType = UITextAutocapitalizationTypeNone;
+        _sapp.ios.textfield.autocorrectionType = UITextAutocorrectionTypeNo;
+        _sapp.ios.textfield.spellCheckingType = UITextSpellCheckingTypeNo;
+        _sapp.ios.textfield.hidden = YES;
+        _sapp.ios.textfield.text = @"x";
+        _sapp.ios.textfield.delegate = _sapp.ios.textfield_dlg;
+        [_sapp.ios.view_ctrl.view addSubview:_sapp.ios.textfield];
+
+        [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg
+            selector:@selector(keyboardWasShown:)
+            name:UIKeyboardDidShowNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg
+            selector:@selector(keyboardWillBeHidden:)
+            name:UIKeyboardWillHideNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:_sapp.ios.textfield_dlg
+            selector:@selector(keyboardDidChangeFrame:)
+            name:UIKeyboardDidChangeFrameNotification object:nil];
+    }
+    if (shown) {
+        /* setting the text field as first responder brings up the onscreen keyboard */
+        [_sapp.ios.textfield becomeFirstResponder];
+    }
+    else {
+        [_sapp.ios.textfield resignFirstResponder];
+    }
+}
+
+@implementation _sapp_app_delegate
+- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+    CGRect screen_rect = UIScreen.mainScreen.bounds;
+    _sapp.ios.window = [[UIWindow alloc] initWithFrame:screen_rect];
+    _sapp.window_width = screen_rect.size.width;
+    _sapp.window_height = screen_rect.size.height;
+    if (_sapp.desc.high_dpi) {
+        _sapp.dpi_scale = (float) UIScreen.mainScreen.nativeScale;
+    }
+    else {
+        _sapp.dpi_scale = 1.0f;
+    }
+    _sapp.framebuffer_width = _sapp.window_width * _sapp.dpi_scale;
+    _sapp.framebuffer_height = _sapp.window_height * _sapp.dpi_scale;
+    #if defined(SOKOL_METAL)
+        _sapp.ios.mtl_device = MTLCreateSystemDefaultDevice();
+        _sapp.ios.view = [[_sapp_ios_view alloc] init];
+        _sapp.ios.view.preferredFramesPerSecond = 60 / _sapp.swap_interval;
+        _sapp.ios.view.device = _sapp.ios.mtl_device;
+        _sapp.ios.view.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
+        _sapp.ios.view.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
+        _sapp.ios.view.sampleCount = (NSUInteger)_sapp.sample_count;
+        /* NOTE: iOS MTKView seems to ignore thew view's contentScaleFactor
+            and automatically renders at Retina resolution. We'll disable
+            autoResize and instead do the resizing in _sapp_ios_update_dimensions()
+        */
+        _sapp.ios.view.autoResizeDrawable = false;
+        _sapp.ios.view.userInteractionEnabled = YES;
+        _sapp.ios.view.multipleTouchEnabled = YES;
+        _sapp.ios.view_ctrl = [[UIViewController alloc] init];
+        _sapp.ios.view_ctrl.modalPresentationStyle = UIModalPresentationFullScreen;
+        _sapp.ios.view_ctrl.view = _sapp.ios.view;
+        _sapp.ios.window.rootViewController = _sapp.ios.view_ctrl;
+    #else
+        if (_sapp.desc.gl_force_gles2) {
+            _sapp.ios.eagl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+            _sapp.gles2_fallback = true;
+        }
+        else {
+            _sapp.ios.eagl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+            if (_sapp.ios.eagl_ctx == nil) {
+                _sapp.ios.eagl_ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+                _sapp.gles2_fallback = true;
+            }
+        }
+        _sapp.ios.view = [[_sapp_ios_view alloc] initWithFrame:screen_rect];
+        _sapp.ios.view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
+        _sapp.ios.view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
+        _sapp.ios.view.drawableStencilFormat = GLKViewDrawableStencilFormatNone;
+        GLKViewDrawableMultisample msaa = _sapp.sample_count > 1 ? GLKViewDrawableMultisample4X : GLKViewDrawableMultisampleNone;
+        _sapp.ios.view.drawableMultisample = msaa;
+        _sapp.ios.view.context = _sapp.ios.eagl_ctx;
+        _sapp.ios.view.enableSetNeedsDisplay = NO;
+        _sapp.ios.view.userInteractionEnabled = YES;
+        _sapp.ios.view.multipleTouchEnabled = YES;
+        // on GLKView, contentScaleFactor appears to work just fine!
+        if (_sapp.desc.high_dpi) {
+            _sapp.ios.view.contentScaleFactor = 2.0;
+        }
+        else {
+            _sapp.ios.view.contentScaleFactor = 1.0;
+        }
+        _sapp.ios.view_ctrl = [[GLKViewController alloc] init];
+        _sapp.ios.view_ctrl.view = _sapp.ios.view;
+        _sapp.ios.view_ctrl.preferredFramesPerSecond = 60 / _sapp.swap_interval;
+        _sapp.ios.window.rootViewController = _sapp.ios.view_ctrl;
+    #endif
+    [_sapp.ios.window makeKeyAndVisible];
+
+    _sapp.valid = true;
+    return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+    if (!_sapp.ios.suspended) {
+        _sapp.ios.suspended = true;
+        _sapp_ios_app_event(SAPP_EVENTTYPE_SUSPENDED);
+    }
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+    if (_sapp.ios.suspended) {
+        _sapp.ios.suspended = false;
+        _sapp_ios_app_event(SAPP_EVENTTYPE_RESUMED);
+    }
+}
+
+/* NOTE: this method will rarely ever be called, iOS application
+    which are terminated by the user are usually killed via signal 9
+    by the operating system.
+*/
+- (void)applicationWillTerminate:(UIApplication *)application {
+    _SOKOL_UNUSED(application);
+    _sapp_call_cleanup();
+    _sapp_ios_discard_state();
+    _sapp_discard_state();
+}
+@end
+
+@implementation _sapp_textfield_dlg
+- (void)keyboardWasShown:(NSNotification*)notif {
+    _sapp.onscreen_keyboard_shown = true;
+    /* query the keyboard's size, and modify the content view's size */
+    if (_sapp.desc.ios_keyboard_resizes_canvas) {
+        NSDictionary* info = notif.userInfo;
+        CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
+        CGRect view_frame = UIScreen.mainScreen.bounds;
+        view_frame.size.height -= kbd_h;
+        _sapp.ios.view.frame = view_frame;
+    }
+}
+- (void)keyboardWillBeHidden:(NSNotification*)notif {
+    _sapp.onscreen_keyboard_shown = false;
+    if (_sapp.desc.ios_keyboard_resizes_canvas) {
+        _sapp.ios.view.frame = UIScreen.mainScreen.bounds;
+    }
+}
+- (void)keyboardDidChangeFrame:(NSNotification*)notif {
+    /* this is for the case when the screen rotation changes while the keyboard is open */
+    if (_sapp.onscreen_keyboard_shown && _sapp.desc.ios_keyboard_resizes_canvas) {
+        NSDictionary* info = notif.userInfo;
+        CGFloat kbd_h = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
+        CGRect view_frame = UIScreen.mainScreen.bounds;
+        view_frame.size.height -= kbd_h;
+        _sapp.ios.view.frame = view_frame;
+    }
+}
+- (BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string {
+    if (_sapp_events_enabled()) {
+        const NSUInteger len = string.length;
+        if (len > 0) {
+            for (NSUInteger i = 0; i < len; i++) {
+                unichar c = [string characterAtIndex:i];
+                if (c >= 32) {
+                    /* ignore surrogates for now */
+                    if ((c < 0xD800) || (c > 0xDFFF)) {
+                        _sapp_init_event(SAPP_EVENTTYPE_CHAR);
+                        _sapp.event.char_code = c;
+                        _sapp_call_event(&_sapp.event);
+                    }
+                }
+                if (c <= 32) {
+                    sapp_keycode k = SAPP_KEYCODE_INVALID;
+                    switch (c) {
+                        case 10: k = SAPP_KEYCODE_ENTER; break;
+                        case 32: k = SAPP_KEYCODE_SPACE; break;
+                        default: break;
+                    }
+                    if (k != SAPP_KEYCODE_INVALID) {
+                        _sapp_init_event(SAPP_EVENTTYPE_KEY_DOWN);
+                        _sapp.event.key_code = k;
+                        _sapp_call_event(&_sapp.event);
+                        _sapp_init_event(SAPP_EVENTTYPE_KEY_UP);
+                        _sapp.event.key_code = k;
+                        _sapp_call_event(&_sapp.event);
+                    }
+                }
+            }
+        }
+        else {
+            /* this was a backspace */
+            _sapp_init_event(SAPP_EVENTTYPE_KEY_DOWN);
+            _sapp.event.key_code = SAPP_KEYCODE_BACKSPACE;
+            _sapp_call_event(&_sapp.event);
+            _sapp_init_event(SAPP_EVENTTYPE_KEY_UP);
+            _sapp.event.key_code = SAPP_KEYCODE_BACKSPACE;
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+    return NO;
+}
+@end
+
+@implementation _sapp_ios_view
+- (void)drawRect:(CGRect)rect {
+    _SOKOL_UNUSED(rect);
+    @autoreleasepool {
+        _sapp_ios_frame();
+    }
+}
+- (BOOL)isOpaque {
+    return YES;
+}
+- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent*)event {
+    _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_BEGAN, touches, event);
+}
+- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent*)event {
+    _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_MOVED, touches, event);
+}
+- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent*)event {
+    _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_ENDED, touches, event);
+}
+- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent*)event {
+    _sapp_ios_touch_event(SAPP_EVENTTYPE_TOUCHES_CANCELLED, touches, event);
+}
+@end
+#endif /* TARGET_OS_IPHONE */
+
+#endif /* _SAPP_APPLE */
+
+/*== EMSCRIPTEN ==============================================================*/
+#if defined(_SAPP_EMSCRIPTEN)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*_sapp_html5_fetch_callback) (const sapp_html5_fetch_response*);
+
+/* this function is called from a JS event handler when the user hides
+    the onscreen keyboard pressing the 'dismiss keyboard key'
+*/
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_notify_keyboard_hidden(void) {
+    _sapp.onscreen_keyboard_shown = false;
+}
+
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_onpaste(const char* str) {
+    if (_sapp.clipboard.enabled) {
+        _sapp_strcpy(str, _sapp.clipboard.buffer, _sapp.clipboard.buf_size);
+        if (_sapp_events_enabled()) {
+            _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+
+/*  https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload */
+EMSCRIPTEN_KEEPALIVE int _sapp_html5_get_ask_leave_site(void) {
+    return _sapp.html5_ask_leave_site ? 1 : 0;
+}
+
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_begin_drop(int num) {
+    if (!_sapp.drop.enabled) {
+        return;
+    }
+    if (num < 0) {
+        num = 0;
+    }
+    if (num > _sapp.drop.max_files) {
+        num = _sapp.drop.max_files;
+    }
+    _sapp.drop.num_files = num;
+    _sapp_clear_drop_buffer();
+}
+
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_drop(int i, const char* name) {
+    /* NOTE: name is only the filename part, not a path */
+    if (!_sapp.drop.enabled) {
+        return;
+    }
+    if (0 == name) {
+        return;
+    }
+    SOKOL_ASSERT(_sapp.drop.num_files <= _sapp.drop.max_files);
+    if ((i < 0) || (i >= _sapp.drop.num_files)) {
+        return;
+    }
+    if (!_sapp_strcpy(name, _sapp_dropped_file_path_ptr(i), _sapp.drop.max_path_length)) {
+        SOKOL_LOG("sokol_app.h: dropped file path too long!\n");
+        _sapp.drop.num_files = 0;
+    }
+}
+
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_end_drop(int x, int y) {
+    if (!_sapp.drop.enabled) {
+        return;
+    }
+    if (0 == _sapp.drop.num_files) {
+        /* there was an error copying the filenames */
+        _sapp_clear_drop_buffer();
+        return;
+
+    }
+    if (_sapp_events_enabled()) {
+        _sapp.mouse.x = (float)x * _sapp.dpi_scale;
+        _sapp.mouse.y = (float)y * _sapp.dpi_scale;
+        _sapp.mouse.dx = 0.0f;
+        _sapp.mouse.dy = 0.0f;
+        _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_invoke_fetch_cb(int index, int success, int error_code, _sapp_html5_fetch_callback callback, uint32_t fetched_size, void* buf_ptr, uint32_t buf_size, void* user_data) {
+    sapp_html5_fetch_response response;
+    memset(&response, 0, sizeof(response));
+    response.succeeded = (0 != success);
+    response.error_code = (sapp_html5_fetch_error) error_code;
+    response.file_index = index;
+    response.fetched_size = fetched_size;
+    response.buffer_ptr = buf_ptr;
+    response.buffer_size = buf_size;
+    response.user_data = user_data;
+    callback(&response);
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+/* Javascript helper functions for mobile virtual keyboard input */
+EM_JS(void, sapp_js_create_textfield, (void), {
+    var _sapp_inp = document.createElement("input");
+    _sapp_inp.type = "text";
+    _sapp_inp.id = "_sokol_app_input_element";
+    _sapp_inp.autocapitalize = "none";
+    _sapp_inp.addEventListener("focusout", function(_sapp_event) {
+        __sapp_emsc_notify_keyboard_hidden()
+
+    });
+    document.body.append(_sapp_inp);
+});
+
+EM_JS(void, sapp_js_focus_textfield, (void), {
+    document.getElementById("_sokol_app_input_element").focus();
+});
+
+EM_JS(void, sapp_js_unfocus_textfield, (void), {
+    document.getElementById("_sokol_app_input_element").blur();
+});
+
+EM_JS(void, sapp_js_add_beforeunload_listener, (void), {
+    Module.sokol_beforeunload = function(event) {
+        if (__sapp_html5_get_ask_leave_site() != 0) {
+            event.preventDefault();
+            event.returnValue = ' ';
+        }
+    };
+    window.addEventListener('beforeunload', Module.sokol_beforeunload);
+});
+
+EM_JS(void, sapp_js_remove_beforeunload_listener, (void), {
+    window.removeEventListener('beforeunload', Module.sokol_beforeunload);
+});
+
+EM_JS(void, sapp_js_add_clipboard_listener, (void), {
+    Module.sokol_paste = function(event) {
+        var pasted_str = event.clipboardData.getData('text');
+        ccall('_sapp_emsc_onpaste', 'void', ['string'], [pasted_str]);
+    };
+    window.addEventListener('paste', Module.sokol_paste);
+});
+
+EM_JS(void, sapp_js_remove_clipboard_listener, (void), {
+    window.removeEventListener('paste', Module.sokol_paste);
+});
+
+EM_JS(void, sapp_js_write_clipboard, (const char* c_str), {
+    var str = UTF8ToString(c_str);
+    var ta = document.createElement('textarea');
+    ta.setAttribute('autocomplete', 'off');
+    ta.setAttribute('autocorrect', 'off');
+    ta.setAttribute('autocapitalize', 'off');
+    ta.setAttribute('spellcheck', 'false');
+    ta.style.left = -100 + 'px';
+    ta.style.top = -100 + 'px';
+    ta.style.height = 1;
+    ta.style.width = 1;
+    ta.value = str;
+    document.body.appendChild(ta);
+    ta.select();
+    document.execCommand('copy');
+    document.body.removeChild(ta);
+});
+
+_SOKOL_PRIVATE void _sapp_emsc_set_clipboard_string(const char* str) {
+    sapp_js_write_clipboard(str);
+}
+
+EM_JS(void, sapp_js_add_dragndrop_listeners, (const char* canvas_name_cstr), {
+    Module.sokol_drop_files = [];
+    var canvas_name = UTF8ToString(canvas_name_cstr);
+    var canvas = document.getElementById(canvas_name);
+    Module.sokol_dragenter = function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+    };
+    Module.sokol_dragleave = function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+    };
+    Module.sokol_dragover = function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+    };
+    Module.sokol_drop = function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+        var files = event.dataTransfer.files;
+        Module.sokol_dropped_files = files;
+        __sapp_emsc_begin_drop(files.length);
+        var i;
+        for (i = 0; i < files.length; i++) {
+            ccall('_sapp_emsc_drop', 'void', ['number', 'string'], [i, files[i].name]);
+        }
+        // FIXME? see computation of targetX/targetY in emscripten via getClientBoundingRect
+        __sapp_emsc_end_drop(event.clientX, event.clientY);
+    };
+    canvas.addEventListener('dragenter', Module.sokol_dragenter, false);
+    canvas.addEventListener('dragleave', Module.sokol_dragleave, false);
+    canvas.addEventListener('dragover',  Module.sokol_dragover, false);
+    canvas.addEventListener('drop',      Module.sokol_drop, false);
+});
+
+EM_JS(uint32_t, sapp_js_dropped_file_size, (int index), {
+    if ((index < 0) || (index >= Module.sokol_dropped_files.length)) {
+        return 0;
+    }
+    else {
+        return Module.sokol_dropped_files[index].size;
+    }
+});
+
+EM_JS(void, sapp_js_fetch_dropped_file, (int index, _sapp_html5_fetch_callback callback, void* buf_ptr, uint32_t buf_size, void* user_data), {
+    var reader = new FileReader();
+    reader.onload = function(loadEvent) {
+        var content = loadEvent.target.result;
+        if (content.byteLength > buf_size) {
+            // SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL
+            __sapp_emsc_invoke_fetch_cb(index, 0, 1, callback, 0, buf_ptr, buf_size, user_data);
+        }
+        else {
+            HEAPU8.set(new Uint8Array(content), buf_ptr);
+            __sapp_emsc_invoke_fetch_cb(index, 1, 0, callback, content.byteLength, buf_ptr, buf_size, user_data);
+        }
+    };
+    reader.onerror = function() {
+        // SAPP_HTML5_FETCH_ERROR_OTHER
+        __sapp_emsc_invoke_fetch_cb(index, 0, 2, callback, 0, buf_ptr, buf_size, user_data);
+    };
+    reader.readAsArrayBuffer(Module.sokol_dropped_files[index]);
+});
+
+EM_JS(void, sapp_js_remove_dragndrop_listeners, (const char* canvas_name_cstr), {
+    var canvas_name = UTF8ToString(canvas_name_cstr);
+    var canvas = document.getElementById(canvas_name);
+    canvas.removeEventListener('dragenter', Module.sokol_dragenter);
+    canvas.removeEventListener('dragleave', Module.sokol_dragleave);
+    canvas.removeEventListener('dragover',  Module.sokol_dragover);
+    canvas.removeEventListener('drop',      Module.sokol_drop);
+});
+
+/* called from the emscripten event handler to update the keyboard visibility
+    state, this must happen from an JS input event handler, otherwise
+    the request will be ignored by the browser
+*/
+_SOKOL_PRIVATE void _sapp_emsc_update_keyboard_state(void) {
+    if (_sapp.emsc.wants_show_keyboard) {
+        /* create input text field on demand */
+        if (!_sapp.emsc.textfield_created) {
+            _sapp.emsc.textfield_created = true;
+            sapp_js_create_textfield();
+        }
+        /* focus the text input field, this will bring up the keyboard */
+        _sapp.onscreen_keyboard_shown = true;
+        _sapp.emsc.wants_show_keyboard = false;
+        sapp_js_focus_textfield();
+    }
+    if (_sapp.emsc.wants_hide_keyboard) {
+        /* unfocus the text input field */
+        if (_sapp.emsc.textfield_created) {
+            _sapp.onscreen_keyboard_shown = false;
+            _sapp.emsc.wants_hide_keyboard = false;
+            sapp_js_unfocus_textfield();
+        }
+    }
+}
+
+/* actually showing the onscreen keyboard must be initiated from a JS
+    input event handler, so we'll just keep track of the desired
+    state, and the actual state change will happen with the next input event
+*/
+_SOKOL_PRIVATE void _sapp_emsc_show_keyboard(bool show) {
+    if (show) {
+        _sapp.emsc.wants_show_keyboard = true;
+    }
+    else {
+        _sapp.emsc.wants_hide_keyboard = true;
+    }
+}
+
+EM_JS(void, sapp_js_pointer_init, (const char* c_str_target), {
+    // lookup and store canvas object by name
+    var target_str = UTF8ToString(c_str_target);
+    Module.sapp_emsc_target = document.getElementById(target_str);
+    if (!Module.sapp_emsc_target) {
+        console.log("sokol_app.h: invalid target:" + target_str);
+    }
+    if (!Module.sapp_emsc_target.requestPointerLock) {
+        console.log("sokol_app.h: target doesn't support requestPointerLock:" + target_str);
+    }
+});
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_pointerlockchange_cb(int emsc_type, const EmscriptenPointerlockChangeEvent* emsc_event, void* user_data) {
+    _SOKOL_UNUSED(emsc_type);
+    _SOKOL_UNUSED(user_data);
+    _sapp.mouse.locked = emsc_event->isActive;
+    return EM_TRUE;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_pointerlockerror_cb(int emsc_type, const void* reserved, void* user_data) {
+    _SOKOL_UNUSED(emsc_type);
+    _SOKOL_UNUSED(reserved);
+    _SOKOL_UNUSED(user_data);
+    _sapp.mouse.locked = false;
+    _sapp.emsc.mouse_lock_requested = false;
+    return true;
+}
+
+EM_JS(void, sapp_js_request_pointerlock, (void), {
+    if (Module.sapp_emsc_target) {
+        if (Module.sapp_emsc_target.requestPointerLock) {
+            Module.sapp_emsc_target.requestPointerLock();
+        }
+    }
+});
+
+EM_JS(void, sapp_js_exit_pointerlock, (void), {
+    if (document.exitPointerLock) {
+        document.exitPointerLock();
+    }
+});
+
+_SOKOL_PRIVATE void _sapp_emsc_lock_mouse(bool lock) {
+    if (lock) {
+        /* request mouse-lock during event handler invocation (see _sapp_emsc_update_mouse_lock_state) */
+        _sapp.emsc.mouse_lock_requested = true;
+    }
+    else {
+        /* NOTE: the _sapp.mouse_locked state will be set in the pointerlockchange callback */
+        _sapp.emsc.mouse_lock_requested = false;
+        sapp_js_exit_pointerlock();
+    }
+}
+
+/* called from inside event handlers to check if mouse lock had been requested,
+   and if yes, actually enter mouse lock.
+*/
+_SOKOL_PRIVATE void _sapp_emsc_update_mouse_lock_state(void) {
+    if (_sapp.emsc.mouse_lock_requested) {
+        _sapp.emsc.mouse_lock_requested = false;
+        sapp_js_request_pointerlock();
+    }
+}
+
+/* JS helper functions to update browser tab favicon */
+EM_JS(void, sapp_js_clear_favicon, (void), {
+    var link = document.getElementById('sokol-app-favicon');
+    if (link) {
+        document.head.removeChild(link);
+    }
+});
+
+EM_JS(void, sapp_js_set_favicon, (int w, int h, const uint8_t* pixels), {
+    var canvas = document.createElement('canvas');
+    canvas.width = w;
+    canvas.height = h;
+    var ctx = canvas.getContext('2d');
+    var img_data = ctx.createImageData(w, h);
+    img_data.data.set(HEAPU8.subarray(pixels, pixels + w*h*4));
+    ctx.putImageData(img_data, 0, 0);
+    var new_link = document.createElement('link');
+    new_link.id = 'sokol-app-favicon';
+    new_link.rel = 'shortcut icon';
+    new_link.href = canvas.toDataURL();
+    document.head.appendChild(new_link);
+});
+
+_SOKOL_PRIVATE void _sapp_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) {
+    SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES));
+    sapp_js_clear_favicon();
+    // find the best matching image candidate for 16x16 pixels
+    int img_index = _sapp_image_bestmatch(icon_desc->images, num_images, 16, 16);
+    const sapp_image_desc* img_desc = &icon_desc->images[img_index];
+    sapp_js_set_favicon(img_desc->width, img_desc->height, (const uint8_t*) img_desc->pixels.ptr);
+}
+
+#if defined(SOKOL_WGPU)
+_SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_create(void);
+_SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_discard(void);
+#endif
+
+_SOKOL_PRIVATE uint32_t _sapp_emsc_mouse_button_mods(uint16_t buttons) {
+    uint32_t m = 0;
+    if (0 != (buttons & (1<<0))) { m |= SAPP_MODIFIER_LMB; }
+    if (0 != (buttons & (1<<1))) { m |= SAPP_MODIFIER_RMB; } // not a bug
+    if (0 != (buttons & (1<<2))) { m |= SAPP_MODIFIER_MMB; } // not a bug
+    return m;
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_emsc_mouse_event_mods(const EmscriptenMouseEvent* ev) {
+    uint32_t m = 0;
+    if (ev->ctrlKey)    { m |= SAPP_MODIFIER_CTRL; }
+    if (ev->shiftKey)   { m |= SAPP_MODIFIER_SHIFT; }
+    if (ev->altKey)     { m |= SAPP_MODIFIER_ALT; }
+    if (ev->metaKey)    { m |= SAPP_MODIFIER_SUPER; }
+    m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons);
+    return m;
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_emsc_key_event_mods(const EmscriptenKeyboardEvent* ev) {
+    uint32_t m = 0;
+    if (ev->ctrlKey)    { m |= SAPP_MODIFIER_CTRL; }
+    if (ev->shiftKey)   { m |= SAPP_MODIFIER_SHIFT; }
+    if (ev->altKey)     { m |= SAPP_MODIFIER_ALT; }
+    if (ev->metaKey)    { m |= SAPP_MODIFIER_SUPER; }
+    m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons);
+    return m;
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_emsc_touch_event_mods(const EmscriptenTouchEvent* ev) {
+    uint32_t m = 0;
+    if (ev->ctrlKey)    { m |= SAPP_MODIFIER_CTRL; }
+    if (ev->shiftKey)   { m |= SAPP_MODIFIER_SHIFT; }
+    if (ev->altKey)     { m |= SAPP_MODIFIER_ALT; }
+    if (ev->metaKey)    { m |= SAPP_MODIFIER_SUPER; }
+    m |= _sapp_emsc_mouse_button_mods(_sapp.emsc.mouse_buttons);
+    return m;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_size_changed(int event_type, const EmscriptenUiEvent* ui_event, void* user_data) {
+    _SOKOL_UNUSED(event_type);
+    _SOKOL_UNUSED(user_data);
+    double w, h;
+    emscripten_get_element_css_size(_sapp.html5_canvas_selector, &w, &h);
+    /* The above method might report zero when toggling HTML5 fullscreen,
+       in that case use the window's inner width reported by the
+       emscripten event. This works ok when toggling *into* fullscreen
+       but doesn't properly restore the previous canvas size when switching
+       back from fullscreen.
+
+       In general, due to the HTML5's fullscreen API's flaky nature it is
+       recommended to use 'soft fullscreen' (stretching the WebGL canvas
+       over the browser windows client rect) with a CSS definition like this:
+
+            position: absolute;
+            top: 0px;
+            left: 0px;
+            margin: 0px;
+            border: 0;
+            width: 100%;
+            height: 100%;
+            overflow: hidden;
+            display: block;
+    */
+    if (w < 1.0) {
+        w = ui_event->windowInnerWidth;
+    }
+    else {
+        _sapp.window_width = (int) w;
+    }
+    if (h < 1.0) {
+        h = ui_event->windowInnerHeight;
+    }
+    else {
+        _sapp.window_height = (int) h;
+    }
+    if (_sapp.desc.high_dpi) {
+        _sapp.dpi_scale = emscripten_get_device_pixel_ratio();
+    }
+    _sapp.framebuffer_width = (int) (w * _sapp.dpi_scale);
+    _sapp.framebuffer_height = (int) (h * _sapp.dpi_scale);
+    SOKOL_ASSERT((_sapp.framebuffer_width > 0) && (_sapp.framebuffer_height > 0));
+    emscripten_set_canvas_element_size(_sapp.html5_canvas_selector, _sapp.framebuffer_width, _sapp.framebuffer_height);
+    #if defined(SOKOL_WGPU)
+        /* on WebGPU: recreate size-dependent rendering surfaces */
+        _sapp_emsc_wgpu_surfaces_discard();
+        _sapp_emsc_wgpu_surfaces_create();
+    #endif
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_RESIZED);
+        _sapp_call_event(&_sapp.event);
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_mouse_cb(int emsc_type, const EmscriptenMouseEvent* emsc_event, void* user_data) {
+    _SOKOL_UNUSED(user_data);
+    _sapp.emsc.mouse_buttons = emsc_event->buttons;
+    if (_sapp.mouse.locked) {
+        _sapp.mouse.dx = (float) emsc_event->movementX;
+        _sapp.mouse.dy = (float) emsc_event->movementY;
+    }
+    else {
+        float new_x = emsc_event->targetX * _sapp.dpi_scale;
+        float new_y = emsc_event->targetY * _sapp.dpi_scale;
+        if (_sapp.mouse.pos_valid) {
+            _sapp.mouse.dx = new_x - _sapp.mouse.x;
+            _sapp.mouse.dy = new_y - _sapp.mouse.y;
+        }
+        _sapp.mouse.x = new_x;
+        _sapp.mouse.y = new_y;
+        _sapp.mouse.pos_valid = true;
+    }
+    if (_sapp_events_enabled() && (emsc_event->button >= 0) && (emsc_event->button < SAPP_MAX_MOUSEBUTTONS)) {
+        sapp_event_type type;
+        bool is_button_event = false;
+        switch (emsc_type) {
+            case EMSCRIPTEN_EVENT_MOUSEDOWN:
+                type = SAPP_EVENTTYPE_MOUSE_DOWN;
+                is_button_event = true;
+                break;
+            case EMSCRIPTEN_EVENT_MOUSEUP:
+                type = SAPP_EVENTTYPE_MOUSE_UP;
+                is_button_event = true;
+                break;
+            case EMSCRIPTEN_EVENT_MOUSEMOVE:
+                type = SAPP_EVENTTYPE_MOUSE_MOVE;
+                break;
+            case EMSCRIPTEN_EVENT_MOUSEENTER:
+                type = SAPP_EVENTTYPE_MOUSE_ENTER;
+                break;
+            case EMSCRIPTEN_EVENT_MOUSELEAVE:
+                type = SAPP_EVENTTYPE_MOUSE_LEAVE;
+                break;
+            default:
+                type = SAPP_EVENTTYPE_INVALID;
+                break;
+        }
+        if (type != SAPP_EVENTTYPE_INVALID) {
+            _sapp_init_event(type);
+            _sapp.event.modifiers = _sapp_emsc_mouse_event_mods(emsc_event);
+            if (is_button_event) {
+                switch (emsc_event->button) {
+                    case 0: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_LEFT; break;
+                    case 1: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_MIDDLE; break;
+                    case 2: _sapp.event.mouse_button = SAPP_MOUSEBUTTON_RIGHT; break;
+                    default: _sapp.event.mouse_button = (sapp_mousebutton)emsc_event->button; break;
+                }
+            }
+            else {
+                _sapp.event.mouse_button = SAPP_MOUSEBUTTON_INVALID;
+            }
+            _sapp_call_event(&_sapp.event);
+        }
+        /* mouse lock can only be activated in mouse button events (not in move, enter or leave) */
+        if (is_button_event) {
+            _sapp_emsc_update_mouse_lock_state();
+        }
+    }
+    _sapp_emsc_update_keyboard_state();
+    return true;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_wheel_cb(int emsc_type, const EmscriptenWheelEvent* emsc_event, void* user_data) {
+    _SOKOL_UNUSED(emsc_type);
+    _SOKOL_UNUSED(user_data);
+    _sapp.emsc.mouse_buttons = emsc_event->mouse.buttons;
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL);
+        _sapp.event.modifiers = _sapp_emsc_mouse_event_mods(&emsc_event->mouse);
+        /* see https://github.com/floooh/sokol/issues/339 */
+        float scale;
+        switch (emsc_event->deltaMode) {
+            case DOM_DELTA_PIXEL: scale = -0.04f; break;
+            case DOM_DELTA_LINE:  scale = -1.33f; break;
+            case DOM_DELTA_PAGE:  scale = -10.0f; break; // FIXME: this is a guess
+            default:              scale = -0.1f; break;  // shouldn't happen
+        }
+        _sapp.event.scroll_x = scale * (float)emsc_event->deltaX;
+        _sapp.event.scroll_y = scale * (float)emsc_event->deltaY;
+        _sapp_call_event(&_sapp.event);
+    }
+    _sapp_emsc_update_keyboard_state();
+    _sapp_emsc_update_mouse_lock_state();
+    return true;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_key_cb(int emsc_type, const EmscriptenKeyboardEvent* emsc_event, void* user_data) {
+    _SOKOL_UNUSED(user_data);
+    bool retval = true;
+    if (_sapp_events_enabled()) {
+        sapp_event_type type;
+        switch (emsc_type) {
+            case EMSCRIPTEN_EVENT_KEYDOWN:
+                type = SAPP_EVENTTYPE_KEY_DOWN;
+                break;
+            case EMSCRIPTEN_EVENT_KEYUP:
+                type = SAPP_EVENTTYPE_KEY_UP;
+                break;
+            case EMSCRIPTEN_EVENT_KEYPRESS:
+                type = SAPP_EVENTTYPE_CHAR;
+                break;
+            default:
+                type = SAPP_EVENTTYPE_INVALID;
+                break;
+        }
+        if (type != SAPP_EVENTTYPE_INVALID) {
+            bool send_keyup_followup = false;
+            _sapp_init_event(type);
+            _sapp.event.key_repeat = emsc_event->repeat;
+            _sapp.event.modifiers = _sapp_emsc_key_event_mods(emsc_event);
+            if (type == SAPP_EVENTTYPE_CHAR) {
+                _sapp.event.char_code = emsc_event->charCode;
+                /* workaround to make Cmd+V work on Safari */
+                if ((emsc_event->metaKey) && (emsc_event->charCode == 118)) {
+                    retval = false;
+                }
+            }
+            else {
+                _sapp.event.key_code = _sapp_translate_key((int)emsc_event->keyCode);
+                /* Special hack for macOS: if the Super key is pressed, macOS doesn't
+                    send keyUp events. As a workaround, to prevent keys from
+                    "sticking", we'll send a keyup event following a keydown
+                    when the SUPER key is pressed
+                */
+                if ((type == SAPP_EVENTTYPE_KEY_DOWN) &&
+                    (_sapp.event.key_code != SAPP_KEYCODE_LEFT_SUPER) &&
+                    (_sapp.event.key_code != SAPP_KEYCODE_RIGHT_SUPER) &&
+                    (_sapp.event.modifiers & SAPP_MODIFIER_SUPER))
+                {
+                    send_keyup_followup = true;
+                }
+                /* only forward a certain key ranges to the browser */
+                switch (_sapp.event.key_code) {
+                    case SAPP_KEYCODE_WORLD_1:
+                    case SAPP_KEYCODE_WORLD_2:
+                    case SAPP_KEYCODE_ESCAPE:
+                    case SAPP_KEYCODE_ENTER:
+                    case SAPP_KEYCODE_TAB:
+                    case SAPP_KEYCODE_BACKSPACE:
+                    case SAPP_KEYCODE_INSERT:
+                    case SAPP_KEYCODE_DELETE:
+                    case SAPP_KEYCODE_RIGHT:
+                    case SAPP_KEYCODE_LEFT:
+                    case SAPP_KEYCODE_DOWN:
+                    case SAPP_KEYCODE_UP:
+                    case SAPP_KEYCODE_PAGE_UP:
+                    case SAPP_KEYCODE_PAGE_DOWN:
+                    case SAPP_KEYCODE_HOME:
+                    case SAPP_KEYCODE_END:
+                    case SAPP_KEYCODE_CAPS_LOCK:
+                    case SAPP_KEYCODE_SCROLL_LOCK:
+                    case SAPP_KEYCODE_NUM_LOCK:
+                    case SAPP_KEYCODE_PRINT_SCREEN:
+                    case SAPP_KEYCODE_PAUSE:
+                    case SAPP_KEYCODE_F1:
+                    case SAPP_KEYCODE_F2:
+                    case SAPP_KEYCODE_F3:
+                    case SAPP_KEYCODE_F4:
+                    case SAPP_KEYCODE_F5:
+                    case SAPP_KEYCODE_F6:
+                    case SAPP_KEYCODE_F7:
+                    case SAPP_KEYCODE_F8:
+                    case SAPP_KEYCODE_F9:
+                    case SAPP_KEYCODE_F10:
+                    case SAPP_KEYCODE_F11:
+                    case SAPP_KEYCODE_F12:
+                    case SAPP_KEYCODE_F13:
+                    case SAPP_KEYCODE_F14:
+                    case SAPP_KEYCODE_F15:
+                    case SAPP_KEYCODE_F16:
+                    case SAPP_KEYCODE_F17:
+                    case SAPP_KEYCODE_F18:
+                    case SAPP_KEYCODE_F19:
+                    case SAPP_KEYCODE_F20:
+                    case SAPP_KEYCODE_F21:
+                    case SAPP_KEYCODE_F22:
+                    case SAPP_KEYCODE_F23:
+                    case SAPP_KEYCODE_F24:
+                    case SAPP_KEYCODE_F25:
+                    case SAPP_KEYCODE_LEFT_SHIFT:
+                    case SAPP_KEYCODE_LEFT_CONTROL:
+                    case SAPP_KEYCODE_LEFT_ALT:
+                    case SAPP_KEYCODE_LEFT_SUPER:
+                    case SAPP_KEYCODE_RIGHT_SHIFT:
+                    case SAPP_KEYCODE_RIGHT_CONTROL:
+                    case SAPP_KEYCODE_RIGHT_ALT:
+                    case SAPP_KEYCODE_RIGHT_SUPER:
+                    case SAPP_KEYCODE_MENU:
+                        /* consume the event */
+                        break;
+                    default:
+                        /* forward key to browser */
+                        retval = false;
+                        break;
+                }
+            }
+            if (_sapp_call_event(&_sapp.event)) {
+                /* consume event via sapp_consume_event() */
+                retval = true;
+            }
+            if (send_keyup_followup) {
+                _sapp.event.type = SAPP_EVENTTYPE_KEY_UP;
+                if (_sapp_call_event(&_sapp.event)) {
+                    retval = true;
+                }
+            }
+        }
+    }
+    _sapp_emsc_update_keyboard_state();
+    _sapp_emsc_update_mouse_lock_state();
+    return retval;
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_touch_cb(int emsc_type, const EmscriptenTouchEvent* emsc_event, void* user_data) {
+    _SOKOL_UNUSED(user_data);
+    bool retval = true;
+    if (_sapp_events_enabled()) {
+        sapp_event_type type;
+        switch (emsc_type) {
+            case EMSCRIPTEN_EVENT_TOUCHSTART:
+                type = SAPP_EVENTTYPE_TOUCHES_BEGAN;
+                break;
+            case EMSCRIPTEN_EVENT_TOUCHMOVE:
+                type = SAPP_EVENTTYPE_TOUCHES_MOVED;
+                break;
+            case EMSCRIPTEN_EVENT_TOUCHEND:
+                type = SAPP_EVENTTYPE_TOUCHES_ENDED;
+                break;
+            case EMSCRIPTEN_EVENT_TOUCHCANCEL:
+                type = SAPP_EVENTTYPE_TOUCHES_CANCELLED;
+                break;
+            default:
+                type = SAPP_EVENTTYPE_INVALID;
+                retval = false;
+                break;
+        }
+        if (type != SAPP_EVENTTYPE_INVALID) {
+            _sapp_init_event(type);
+            _sapp.event.modifiers = _sapp_emsc_touch_event_mods(emsc_event);
+            _sapp.event.num_touches = emsc_event->numTouches;
+            if (_sapp.event.num_touches > SAPP_MAX_TOUCHPOINTS) {
+                _sapp.event.num_touches = SAPP_MAX_TOUCHPOINTS;
+            }
+            for (int i = 0; i < _sapp.event.num_touches; i++) {
+                const EmscriptenTouchPoint* src = &emsc_event->touches[i];
+                sapp_touchpoint* dst = &_sapp.event.touches[i];
+                dst->identifier = (uintptr_t)src->identifier;
+                dst->pos_x = src->targetX * _sapp.dpi_scale;
+                dst->pos_y = src->targetY * _sapp.dpi_scale;
+                dst->changed = src->isChanged;
+            }
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+    _sapp_emsc_update_keyboard_state();
+    return retval;
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_keytable_init(void) {
+    _sapp.keycodes[8]   = SAPP_KEYCODE_BACKSPACE;
+    _sapp.keycodes[9]   = SAPP_KEYCODE_TAB;
+    _sapp.keycodes[13]  = SAPP_KEYCODE_ENTER;
+    _sapp.keycodes[16]  = SAPP_KEYCODE_LEFT_SHIFT;
+    _sapp.keycodes[17]  = SAPP_KEYCODE_LEFT_CONTROL;
+    _sapp.keycodes[18]  = SAPP_KEYCODE_LEFT_ALT;
+    _sapp.keycodes[19]  = SAPP_KEYCODE_PAUSE;
+    _sapp.keycodes[27]  = SAPP_KEYCODE_ESCAPE;
+    _sapp.keycodes[32]  = SAPP_KEYCODE_SPACE;
+    _sapp.keycodes[33]  = SAPP_KEYCODE_PAGE_UP;
+    _sapp.keycodes[34]  = SAPP_KEYCODE_PAGE_DOWN;
+    _sapp.keycodes[35]  = SAPP_KEYCODE_END;
+    _sapp.keycodes[36]  = SAPP_KEYCODE_HOME;
+    _sapp.keycodes[37]  = SAPP_KEYCODE_LEFT;
+    _sapp.keycodes[38]  = SAPP_KEYCODE_UP;
+    _sapp.keycodes[39]  = SAPP_KEYCODE_RIGHT;
+    _sapp.keycodes[40]  = SAPP_KEYCODE_DOWN;
+    _sapp.keycodes[45]  = SAPP_KEYCODE_INSERT;
+    _sapp.keycodes[46]  = SAPP_KEYCODE_DELETE;
+    _sapp.keycodes[48]  = SAPP_KEYCODE_0;
+    _sapp.keycodes[49]  = SAPP_KEYCODE_1;
+    _sapp.keycodes[50]  = SAPP_KEYCODE_2;
+    _sapp.keycodes[51]  = SAPP_KEYCODE_3;
+    _sapp.keycodes[52]  = SAPP_KEYCODE_4;
+    _sapp.keycodes[53]  = SAPP_KEYCODE_5;
+    _sapp.keycodes[54]  = SAPP_KEYCODE_6;
+    _sapp.keycodes[55]  = SAPP_KEYCODE_7;
+    _sapp.keycodes[56]  = SAPP_KEYCODE_8;
+    _sapp.keycodes[57]  = SAPP_KEYCODE_9;
+    _sapp.keycodes[59]  = SAPP_KEYCODE_SEMICOLON;
+    _sapp.keycodes[64]  = SAPP_KEYCODE_EQUAL;
+    _sapp.keycodes[65]  = SAPP_KEYCODE_A;
+    _sapp.keycodes[66]  = SAPP_KEYCODE_B;
+    _sapp.keycodes[67]  = SAPP_KEYCODE_C;
+    _sapp.keycodes[68]  = SAPP_KEYCODE_D;
+    _sapp.keycodes[69]  = SAPP_KEYCODE_E;
+    _sapp.keycodes[70]  = SAPP_KEYCODE_F;
+    _sapp.keycodes[71]  = SAPP_KEYCODE_G;
+    _sapp.keycodes[72]  = SAPP_KEYCODE_H;
+    _sapp.keycodes[73]  = SAPP_KEYCODE_I;
+    _sapp.keycodes[74]  = SAPP_KEYCODE_J;
+    _sapp.keycodes[75]  = SAPP_KEYCODE_K;
+    _sapp.keycodes[76]  = SAPP_KEYCODE_L;
+    _sapp.keycodes[77]  = SAPP_KEYCODE_M;
+    _sapp.keycodes[78]  = SAPP_KEYCODE_N;
+    _sapp.keycodes[79]  = SAPP_KEYCODE_O;
+    _sapp.keycodes[80]  = SAPP_KEYCODE_P;
+    _sapp.keycodes[81]  = SAPP_KEYCODE_Q;
+    _sapp.keycodes[82]  = SAPP_KEYCODE_R;
+    _sapp.keycodes[83]  = SAPP_KEYCODE_S;
+    _sapp.keycodes[84]  = SAPP_KEYCODE_T;
+    _sapp.keycodes[85]  = SAPP_KEYCODE_U;
+    _sapp.keycodes[86]  = SAPP_KEYCODE_V;
+    _sapp.keycodes[87]  = SAPP_KEYCODE_W;
+    _sapp.keycodes[88]  = SAPP_KEYCODE_X;
+    _sapp.keycodes[89]  = SAPP_KEYCODE_Y;
+    _sapp.keycodes[90]  = SAPP_KEYCODE_Z;
+    _sapp.keycodes[91]  = SAPP_KEYCODE_LEFT_SUPER;
+    _sapp.keycodes[93]  = SAPP_KEYCODE_MENU;
+    _sapp.keycodes[96]  = SAPP_KEYCODE_KP_0;
+    _sapp.keycodes[97]  = SAPP_KEYCODE_KP_1;
+    _sapp.keycodes[98]  = SAPP_KEYCODE_KP_2;
+    _sapp.keycodes[99]  = SAPP_KEYCODE_KP_3;
+    _sapp.keycodes[100] = SAPP_KEYCODE_KP_4;
+    _sapp.keycodes[101] = SAPP_KEYCODE_KP_5;
+    _sapp.keycodes[102] = SAPP_KEYCODE_KP_6;
+    _sapp.keycodes[103] = SAPP_KEYCODE_KP_7;
+    _sapp.keycodes[104] = SAPP_KEYCODE_KP_8;
+    _sapp.keycodes[105] = SAPP_KEYCODE_KP_9;
+    _sapp.keycodes[106] = SAPP_KEYCODE_KP_MULTIPLY;
+    _sapp.keycodes[107] = SAPP_KEYCODE_KP_ADD;
+    _sapp.keycodes[109] = SAPP_KEYCODE_KP_SUBTRACT;
+    _sapp.keycodes[110] = SAPP_KEYCODE_KP_DECIMAL;
+    _sapp.keycodes[111] = SAPP_KEYCODE_KP_DIVIDE;
+    _sapp.keycodes[112] = SAPP_KEYCODE_F1;
+    _sapp.keycodes[113] = SAPP_KEYCODE_F2;
+    _sapp.keycodes[114] = SAPP_KEYCODE_F3;
+    _sapp.keycodes[115] = SAPP_KEYCODE_F4;
+    _sapp.keycodes[116] = SAPP_KEYCODE_F5;
+    _sapp.keycodes[117] = SAPP_KEYCODE_F6;
+    _sapp.keycodes[118] = SAPP_KEYCODE_F7;
+    _sapp.keycodes[119] = SAPP_KEYCODE_F8;
+    _sapp.keycodes[120] = SAPP_KEYCODE_F9;
+    _sapp.keycodes[121] = SAPP_KEYCODE_F10;
+    _sapp.keycodes[122] = SAPP_KEYCODE_F11;
+    _sapp.keycodes[123] = SAPP_KEYCODE_F12;
+    _sapp.keycodes[144] = SAPP_KEYCODE_NUM_LOCK;
+    _sapp.keycodes[145] = SAPP_KEYCODE_SCROLL_LOCK;
+    _sapp.keycodes[173] = SAPP_KEYCODE_MINUS;
+    _sapp.keycodes[186] = SAPP_KEYCODE_SEMICOLON;
+    _sapp.keycodes[187] = SAPP_KEYCODE_EQUAL;
+    _sapp.keycodes[188] = SAPP_KEYCODE_COMMA;
+    _sapp.keycodes[189] = SAPP_KEYCODE_MINUS;
+    _sapp.keycodes[190] = SAPP_KEYCODE_PERIOD;
+    _sapp.keycodes[191] = SAPP_KEYCODE_SLASH;
+    _sapp.keycodes[192] = SAPP_KEYCODE_GRAVE_ACCENT;
+    _sapp.keycodes[219] = SAPP_KEYCODE_LEFT_BRACKET;
+    _sapp.keycodes[220] = SAPP_KEYCODE_BACKSLASH;
+    _sapp.keycodes[221] = SAPP_KEYCODE_RIGHT_BRACKET;
+    _sapp.keycodes[222] = SAPP_KEYCODE_APOSTROPHE;
+    _sapp.keycodes[224] = SAPP_KEYCODE_LEFT_SUPER;
+}
+
+#if defined(SOKOL_GLES2) || defined(SOKOL_GLES3)
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_webgl_context_cb(int emsc_type, const void* reserved, void* user_data) {
+    _SOKOL_UNUSED(reserved);
+    _SOKOL_UNUSED(user_data);
+    sapp_event_type type;
+    switch (emsc_type) {
+        case EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST:     type = SAPP_EVENTTYPE_SUSPENDED; break;
+        case EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED: type = SAPP_EVENTTYPE_RESUMED; break;
+        default:                                    type = SAPP_EVENTTYPE_INVALID; break;
+    }
+    if (_sapp_events_enabled() && (SAPP_EVENTTYPE_INVALID != type)) {
+        _sapp_init_event(type);
+        _sapp_call_event(&_sapp.event);
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_webgl_init(void) {
+    EmscriptenWebGLContextAttributes attrs;
+    emscripten_webgl_init_context_attributes(&attrs);
+    attrs.alpha = _sapp.desc.alpha;
+    attrs.depth = true;
+    attrs.stencil = true;
+    attrs.antialias = _sapp.sample_count > 1;
+    attrs.premultipliedAlpha = _sapp.desc.html5_premultiplied_alpha;
+    attrs.preserveDrawingBuffer = _sapp.desc.html5_preserve_drawing_buffer;
+    attrs.enableExtensionsByDefault = true;
+    #if defined(SOKOL_GLES3)
+        if (_sapp.desc.gl_force_gles2) {
+            attrs.majorVersion = 1;
+            _sapp.gles2_fallback = true;
+        }
+        else {
+            attrs.majorVersion = 2;
+        }
+    #endif
+    EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp.html5_canvas_selector, &attrs);
+    if (!ctx) {
+        attrs.majorVersion = 1;
+        ctx = emscripten_webgl_create_context(_sapp.html5_canvas_selector, &attrs);
+        _sapp.gles2_fallback = true;
+    }
+    emscripten_webgl_make_context_current(ctx);
+
+    /* some WebGL extension are not enabled automatically by emscripten */
+    emscripten_webgl_enable_extension(ctx, "WEBKIT_WEBGL_compressed_texture_pvrtc");
+}
+#endif
+
+#if defined(SOKOL_WGPU)
+#define _SAPP_EMSC_WGPU_STATE_INITIAL (0)
+#define _SAPP_EMSC_WGPU_STATE_READY (1)
+#define _SAPP_EMSC_WGPU_STATE_RUNNING (2)
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+/* called when the asynchronous WebGPU device + swapchain init code in JS has finished */
+EMSCRIPTEN_KEEPALIVE void _sapp_emsc_wgpu_ready(int device_id, int swapchain_id, int swapchain_fmt) {
+    SOKOL_ASSERT(0 == _sapp.emsc.wgpu.device);
+    _sapp.emsc.wgpu.device = (WGPUDevice) device_id;
+    _sapp.emsc.wgpu.swapchain = (WGPUSwapChain) swapchain_id;
+    _sapp.emsc.wgpu.render_format = (WGPUTextureFormat) swapchain_fmt;
+    _sapp.emsc.wgpu.state = _SAPP_EMSC_WGPU_STATE_READY;
+}
+#if defined(__cplusplus)
+} // extern "C"
+#endif
+
+/* embedded JS function to handle all the asynchronous WebGPU setup */
+EM_JS(void, sapp_js_wgpu_init, (), {
+    WebGPU.initManagers();
+    // FIXME: the extension activation must be more clever here
+    navigator.gpu.requestAdapter().then(function(adapter) {
+        console.log("wgpu adapter extensions: " + adapter.extensions);
+        adapter.requestDevice({ extensions: ["textureCompressionBC"]}).then(function(device) {
+            var gpuContext = document.getElementById("canvas").getContext("gpupresent");
+            console.log("wgpu device extensions: " + adapter.extensions);
+            gpuContext.getSwapChainPreferredFormat(device).then(function(fmt) {
+                var swapChainDescriptor = { device: device, format: fmt };
+                var swapChain = gpuContext.configureSwapChain(swapChainDescriptor);
+                var deviceId = WebGPU.mgrDevice.create(device);
+                var swapChainId = WebGPU.mgrSwapChain.create(swapChain);
+                var fmtId = WebGPU.TextureFormat.findIndex(function(elm) { return elm==fmt; });
+                console.log("wgpu device: " + device);
+                console.log("wgpu swap chain: " + swapChain);
+                console.log("wgpu preferred format: " + fmt + " (" + fmtId + ")");
+                __sapp_emsc_wgpu_ready(deviceId, swapChainId, fmtId);
+            });
+        });
+    });
+});
+
+_SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_create(void) {
+    SOKOL_ASSERT(_sapp.emsc.wgpu.device);
+    SOKOL_ASSERT(_sapp.emsc.wgpu.swapchain);
+    SOKOL_ASSERT(0 == _sapp.emsc.wgpu.depth_stencil_tex);
+    SOKOL_ASSERT(0 == _sapp.emsc.wgpu.depth_stencil_view);
+    SOKOL_ASSERT(0 == _sapp.emsc.wgpu.msaa_tex);
+    SOKOL_ASSERT(0 == _sapp.emsc.wgpu.msaa_view);
+
+    WGPUTextureDescriptor ds_desc;
+    memset(&ds_desc, 0, sizeof(ds_desc));
+    ds_desc.usage = WGPUTextureUsage_OutputAttachment;
+    ds_desc.dimension = WGPUTextureDimension_2D;
+    ds_desc.size.width = (uint32_t) _sapp.framebuffer_width;
+    ds_desc.size.height = (uint32_t) _sapp.framebuffer_height;
+    ds_desc.size.depth = 1;
+    ds_desc.arrayLayerCount = 1;
+    ds_desc.format = WGPUTextureFormat_Depth24PlusStencil8;
+    ds_desc.mipLevelCount = 1;
+    ds_desc.sampleCount = _sapp.sample_count;
+    _sapp.emsc.wgpu.depth_stencil_tex = wgpuDeviceCreateTexture(_sapp.emsc.wgpu.device, &ds_desc);
+    _sapp.emsc.wgpu.depth_stencil_view = wgpuTextureCreateView(_sapp.emsc.wgpu.depth_stencil_tex, 0);
+
+    if (_sapp.sample_count > 1) {
+        WGPUTextureDescriptor msaa_desc;
+        memset(&msaa_desc, 0, sizeof(msaa_desc));
+        msaa_desc.usage = WGPUTextureUsage_OutputAttachment;
+        msaa_desc.dimension = WGPUTextureDimension_2D;
+        msaa_desc.size.width = (uint32_t) _sapp.framebuffer_width;
+        msaa_desc.size.height = (uint32_t) _sapp.framebuffer_height;
+        msaa_desc.size.depth = 1;
+        msaa_desc.arrayLayerCount = 1;
+        msaa_desc.format = _sapp.emsc.wgpu.render_format;
+        msaa_desc.mipLevelCount = 1;
+        msaa_desc.sampleCount = _sapp.sample_count;
+        _sapp.emsc.wgpu.msaa_tex = wgpuDeviceCreateTexture(_sapp.emsc.wgpu.device, &msaa_desc);
+        _sapp.emsc.wgpu.msaa_view = wgpuTextureCreateView(_sapp.emsc.wgpu.msaa_tex, 0);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_wgpu_surfaces_discard(void) {
+    if (_sapp.emsc.wgpu.msaa_tex) {
+        wgpuTextureRelease(_sapp.emsc.wgpu.msaa_tex);
+        _sapp.emsc.wgpu.msaa_tex = 0;
+    }
+    if (_sapp.emsc.wgpu.msaa_view) {
+        wgpuTextureViewRelease(_sapp.emsc.wgpu.msaa_view);
+        _sapp.emsc.wgpu.msaa_view = 0;
+    }
+    if (_sapp.emsc.wgpu.depth_stencil_tex) {
+        wgpuTextureRelease(_sapp.emsc.wgpu.depth_stencil_tex);
+        _sapp.emsc.wgpu.depth_stencil_tex = 0;
+    }
+    if (_sapp.emsc.wgpu.depth_stencil_view) {
+        wgpuTextureViewRelease(_sapp.emsc.wgpu.depth_stencil_view);
+        _sapp.emsc.wgpu.depth_stencil_view = 0;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_wgpu_next_frame(void) {
+    if (_sapp.emsc.wgpu.swapchain_view) {
+        wgpuTextureViewRelease(_sapp.emsc.wgpu.swapchain_view);
+    }
+    _sapp.emsc.wgpu.swapchain_view = wgpuSwapChainGetCurrentTextureView(_sapp.emsc.wgpu.swapchain);
+}
+#endif
+
+_SOKOL_PRIVATE void _sapp_emsc_register_eventhandlers(void) {
+    emscripten_set_mousedown_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb);
+    emscripten_set_mouseup_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb);
+    emscripten_set_mousemove_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb);
+    emscripten_set_mouseenter_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb);
+    emscripten_set_mouseleave_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_mouse_cb);
+    emscripten_set_wheel_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_wheel_cb);
+    emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_emsc_key_cb);
+    emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_emsc_key_cb);
+    emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_emsc_key_cb);
+    emscripten_set_touchstart_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb);
+    emscripten_set_touchmove_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb);
+    emscripten_set_touchend_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb);
+    emscripten_set_touchcancel_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_touch_cb);
+    emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_emsc_pointerlockchange_cb);
+    emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_emsc_pointerlockerror_cb);
+    sapp_js_add_beforeunload_listener();
+    if (_sapp.clipboard.enabled) {
+        sapp_js_add_clipboard_listener();
+    }
+    if (_sapp.drop.enabled) {
+        sapp_js_add_dragndrop_listeners(&_sapp.html5_canvas_selector[1]);
+    }
+    #if defined(SOKOL_GLES2) || defined(SOKOL_GLES3)
+        emscripten_set_webglcontextlost_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_webgl_context_cb);
+        emscripten_set_webglcontextrestored_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_webgl_context_cb);
+    #endif
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_unregister_eventhandlers() {
+    emscripten_set_mousedown_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_mouseup_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_mousemove_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_mouseenter_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_mouseleave_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_wheel_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0);
+    emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0);
+    emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0);
+    emscripten_set_touchstart_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_touchmove_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_touchend_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_touchcancel_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0);
+    emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0);
+    sapp_js_remove_beforeunload_listener();
+    if (_sapp.clipboard.enabled) {
+        sapp_js_remove_clipboard_listener();
+    }
+    if (_sapp.drop.enabled) {
+        sapp_js_remove_dragndrop_listeners(&_sapp.html5_canvas_selector[1]);
+    }
+    #if defined(SOKOL_GLES2) || defined(SOKOL_GLES3)
+        emscripten_set_webglcontextlost_callback(_sapp.html5_canvas_selector, 0, true, 0);
+        emscripten_set_webglcontextrestored_callback(_sapp.html5_canvas_selector, 0, true, 0);
+    #endif
+}
+
+_SOKOL_PRIVATE EM_BOOL _sapp_emsc_frame(double time, void* userData) {
+    _SOKOL_UNUSED(time);
+    _SOKOL_UNUSED(userData);
+
+    #if defined(SOKOL_WGPU)
+        /*
+            on WebGPU, the emscripten frame callback will already be called while
+            the asynchronous WebGPU device and swapchain initialization is still
+            in progress
+        */
+        switch (_sapp.emsc.wgpu.state) {
+            case _SAPP_EMSC_WGPU_STATE_INITIAL:
+                /* async JS init hasn't finished yet */
+                break;
+            case _SAPP_EMSC_WGPU_STATE_READY:
+                /* perform post-async init stuff */
+                _sapp_emsc_wgpu_surfaces_create();
+                _sapp.emsc.wgpu.state = _SAPP_EMSC_WGPU_STATE_RUNNING;
+                break;
+            case _SAPP_EMSC_WGPU_STATE_RUNNING:
+                /* a regular frame */
+                _sapp_emsc_wgpu_next_frame();
+                _sapp_frame();
+                break;
+        }
+    #else
+        /* WebGL code path */
+        _sapp_frame();
+    #endif
+
+    /* quit-handling */
+    if (_sapp.quit_requested) {
+        _sapp_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED);
+        _sapp_call_event(&_sapp.event);
+        if (_sapp.quit_requested) {
+            _sapp.quit_ordered = true;
+        }
+    }
+    if (_sapp.quit_ordered) {
+        _sapp_emsc_unregister_eventhandlers();
+        _sapp_call_cleanup();
+        _sapp_discard_state();
+        return EM_FALSE;
+    }
+    return EM_TRUE;
+}
+
+_SOKOL_PRIVATE void _sapp_emsc_run(const sapp_desc* desc) {
+    _sapp_init_state(desc);
+    sapp_js_pointer_init(&_sapp.html5_canvas_selector[1]);
+    _sapp_emsc_keytable_init();
+    double w, h;
+    if (_sapp.desc.html5_canvas_resize) {
+        w = (double) _sapp.desc.width;
+        h = (double) _sapp.desc.height;
+    }
+    else {
+        emscripten_get_element_css_size(_sapp.html5_canvas_selector, &w, &h);
+        emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, false, _sapp_emsc_size_changed);
+    }
+    if (_sapp.desc.high_dpi) {
+        _sapp.dpi_scale = emscripten_get_device_pixel_ratio();
+    }
+    _sapp.window_width = (int) w;
+    _sapp.window_height = (int) h;
+    _sapp.framebuffer_width = (int) (w * _sapp.dpi_scale);
+    _sapp.framebuffer_height = (int) (h * _sapp.dpi_scale);
+    emscripten_set_canvas_element_size(_sapp.html5_canvas_selector, _sapp.framebuffer_width, _sapp.framebuffer_height);
+    #if defined(SOKOL_GLES2) || defined(SOKOL_GLES3)
+        _sapp_emsc_webgl_init();
+    #elif defined(SOKOL_WGPU)
+        sapp_js_wgpu_init();
+    #endif
+    _sapp.valid = true;
+    _sapp_emsc_register_eventhandlers();
+    sapp_set_icon(&desc->icon);
+
+    /* start the frame loop */
+    emscripten_request_animation_frame_loop(_sapp_emsc_frame, 0);
+
+    /* NOT A BUG: do not call _sapp_discard_state() here, instead this is
+       called in _sapp_emsc_frame() when the application is ordered to quit
+     */
+}
+
+#if !defined(SOKOL_NO_ENTRY)
+int main(int argc, char* argv[]) {
+    sapp_desc desc = sokol_main(argc, argv);
+    _sapp_emsc_run(&desc);
+    return 0;
+}
+#endif /* SOKOL_NO_ENTRY */
+#endif /* _SAPP_EMSCRIPTEN */
+
+/*== MISC GL SUPPORT FUNCTIONS ================================================*/
+#if defined(SOKOL_GLCORE33)
+typedef struct {
+    int         red_bits;
+    int         green_bits;
+    int         blue_bits;
+    int         alpha_bits;
+    int         depth_bits;
+    int         stencil_bits;
+    int         samples;
+    bool        doublebuffer;
+    uintptr_t   handle;
+} _sapp_gl_fbconfig;
+
+_SOKOL_PRIVATE void _sapp_gl_init_fbconfig(_sapp_gl_fbconfig* fbconfig) {
+    memset(fbconfig, 0, sizeof(_sapp_gl_fbconfig));
+    /* -1 means "don't care" */
+    fbconfig->red_bits = -1;
+    fbconfig->green_bits = -1;
+    fbconfig->blue_bits = -1;
+    fbconfig->alpha_bits = -1;
+    fbconfig->depth_bits = -1;
+    fbconfig->stencil_bits = -1;
+    fbconfig->samples = -1;
+}
+
+_SOKOL_PRIVATE const _sapp_gl_fbconfig* _sapp_gl_choose_fbconfig(const _sapp_gl_fbconfig* desired, const _sapp_gl_fbconfig* alternatives, int count) {
+    int missing, least_missing = 1000000;
+    int color_diff, least_color_diff = 10000000;
+    int extra_diff, least_extra_diff = 10000000;
+    const _sapp_gl_fbconfig* current;
+    const _sapp_gl_fbconfig* closest = 0;
+    for (int i = 0;  i < count;  i++) {
+        current = alternatives + i;
+        if (desired->doublebuffer != current->doublebuffer) {
+            continue;
+        }
+        missing = 0;
+        if (desired->alpha_bits > 0 && current->alpha_bits == 0) {
+            missing++;
+        }
+        if (desired->depth_bits > 0 && current->depth_bits == 0) {
+            missing++;
+        }
+        if (desired->stencil_bits > 0 && current->stencil_bits == 0) {
+            missing++;
+        }
+        if (desired->samples > 0 && current->samples == 0) {
+            /* Technically, several multisampling buffers could be
+                involved, but that's a lower level implementation detail and
+                not important to us here, so we count them as one
+            */
+            missing++;
+        }
+
+        /* These polynomials make many small channel size differences matter
+            less than one large channel size difference
+            Calculate color channel size difference value
+        */
+        color_diff = 0;
+        if (desired->red_bits != -1) {
+            color_diff += (desired->red_bits - current->red_bits) * (desired->red_bits - current->red_bits);
+        }
+        if (desired->green_bits != -1) {
+            color_diff += (desired->green_bits - current->green_bits) * (desired->green_bits - current->green_bits);
+        }
+        if (desired->blue_bits != -1) {
+            color_diff += (desired->blue_bits - current->blue_bits) * (desired->blue_bits - current->blue_bits);
+        }
+
+        /* Calculate non-color channel size difference value */
+        extra_diff = 0;
+        if (desired->alpha_bits != -1) {
+            extra_diff += (desired->alpha_bits - current->alpha_bits) * (desired->alpha_bits - current->alpha_bits);
+        }
+        if (desired->depth_bits != -1) {
+            extra_diff += (desired->depth_bits - current->depth_bits) * (desired->depth_bits - current->depth_bits);
+        }
+        if (desired->stencil_bits != -1) {
+            extra_diff += (desired->stencil_bits - current->stencil_bits) * (desired->stencil_bits - current->stencil_bits);
+        }
+        if (desired->samples != -1) {
+            extra_diff += (desired->samples - current->samples) * (desired->samples - current->samples);
+        }
+
+        /* Figure out if the current one is better than the best one found so far
+            Least number of missing buffers is the most important heuristic,
+            then color buffer size match and lastly size match for other buffers
+        */
+        if (missing < least_missing) {
+            closest = current;
+        }
+        else if (missing == least_missing) {
+            if ((color_diff < least_color_diff) ||
+                (color_diff == least_color_diff && extra_diff < least_extra_diff))
+            {
+                closest = current;
+            }
+        }
+        if (current == closest) {
+            least_missing = missing;
+            least_color_diff = color_diff;
+            least_extra_diff = extra_diff;
+        }
+    }
+    return closest;
+}
+#endif
+
+/*== WINDOWS DESKTOP and UWP====================================================*/
+#if defined(_SAPP_WIN32) || defined(_SAPP_UWP)
+_SOKOL_PRIVATE bool _sapp_win32_uwp_utf8_to_wide(const char* src, wchar_t* dst, int dst_num_bytes) {
+    SOKOL_ASSERT(src && dst && (dst_num_bytes > 1));
+    memset(dst, 0, (size_t)dst_num_bytes);
+    const int dst_chars = dst_num_bytes / (int)sizeof(wchar_t);
+    const int dst_needed = MultiByteToWideChar(CP_UTF8, 0, src, -1, 0, 0);
+    if ((dst_needed > 0) && (dst_needed < dst_chars)) {
+        MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst_chars);
+        return true;
+    }
+    else {
+        /* input string doesn't fit into destination buffer */
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_uwp_app_event(sapp_event_type type) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_uwp_init_keytable(void) {
+    /* same as GLFW */
+    _sapp.keycodes[0x00B] = SAPP_KEYCODE_0;
+    _sapp.keycodes[0x002] = SAPP_KEYCODE_1;
+    _sapp.keycodes[0x003] = SAPP_KEYCODE_2;
+    _sapp.keycodes[0x004] = SAPP_KEYCODE_3;
+    _sapp.keycodes[0x005] = SAPP_KEYCODE_4;
+    _sapp.keycodes[0x006] = SAPP_KEYCODE_5;
+    _sapp.keycodes[0x007] = SAPP_KEYCODE_6;
+    _sapp.keycodes[0x008] = SAPP_KEYCODE_7;
+    _sapp.keycodes[0x009] = SAPP_KEYCODE_8;
+    _sapp.keycodes[0x00A] = SAPP_KEYCODE_9;
+    _sapp.keycodes[0x01E] = SAPP_KEYCODE_A;
+    _sapp.keycodes[0x030] = SAPP_KEYCODE_B;
+    _sapp.keycodes[0x02E] = SAPP_KEYCODE_C;
+    _sapp.keycodes[0x020] = SAPP_KEYCODE_D;
+    _sapp.keycodes[0x012] = SAPP_KEYCODE_E;
+    _sapp.keycodes[0x021] = SAPP_KEYCODE_F;
+    _sapp.keycodes[0x022] = SAPP_KEYCODE_G;
+    _sapp.keycodes[0x023] = SAPP_KEYCODE_H;
+    _sapp.keycodes[0x017] = SAPP_KEYCODE_I;
+    _sapp.keycodes[0x024] = SAPP_KEYCODE_J;
+    _sapp.keycodes[0x025] = SAPP_KEYCODE_K;
+    _sapp.keycodes[0x026] = SAPP_KEYCODE_L;
+    _sapp.keycodes[0x032] = SAPP_KEYCODE_M;
+    _sapp.keycodes[0x031] = SAPP_KEYCODE_N;
+    _sapp.keycodes[0x018] = SAPP_KEYCODE_O;
+    _sapp.keycodes[0x019] = SAPP_KEYCODE_P;
+    _sapp.keycodes[0x010] = SAPP_KEYCODE_Q;
+    _sapp.keycodes[0x013] = SAPP_KEYCODE_R;
+    _sapp.keycodes[0x01F] = SAPP_KEYCODE_S;
+    _sapp.keycodes[0x014] = SAPP_KEYCODE_T;
+    _sapp.keycodes[0x016] = SAPP_KEYCODE_U;
+    _sapp.keycodes[0x02F] = SAPP_KEYCODE_V;
+    _sapp.keycodes[0x011] = SAPP_KEYCODE_W;
+    _sapp.keycodes[0x02D] = SAPP_KEYCODE_X;
+    _sapp.keycodes[0x015] = SAPP_KEYCODE_Y;
+    _sapp.keycodes[0x02C] = SAPP_KEYCODE_Z;
+    _sapp.keycodes[0x028] = SAPP_KEYCODE_APOSTROPHE;
+    _sapp.keycodes[0x02B] = SAPP_KEYCODE_BACKSLASH;
+    _sapp.keycodes[0x033] = SAPP_KEYCODE_COMMA;
+    _sapp.keycodes[0x00D] = SAPP_KEYCODE_EQUAL;
+    _sapp.keycodes[0x029] = SAPP_KEYCODE_GRAVE_ACCENT;
+    _sapp.keycodes[0x01A] = SAPP_KEYCODE_LEFT_BRACKET;
+    _sapp.keycodes[0x00C] = SAPP_KEYCODE_MINUS;
+    _sapp.keycodes[0x034] = SAPP_KEYCODE_PERIOD;
+    _sapp.keycodes[0x01B] = SAPP_KEYCODE_RIGHT_BRACKET;
+    _sapp.keycodes[0x027] = SAPP_KEYCODE_SEMICOLON;
+    _sapp.keycodes[0x035] = SAPP_KEYCODE_SLASH;
+    _sapp.keycodes[0x056] = SAPP_KEYCODE_WORLD_2;
+    _sapp.keycodes[0x00E] = SAPP_KEYCODE_BACKSPACE;
+    _sapp.keycodes[0x153] = SAPP_KEYCODE_DELETE;
+    _sapp.keycodes[0x14F] = SAPP_KEYCODE_END;
+    _sapp.keycodes[0x01C] = SAPP_KEYCODE_ENTER;
+    _sapp.keycodes[0x001] = SAPP_KEYCODE_ESCAPE;
+    _sapp.keycodes[0x147] = SAPP_KEYCODE_HOME;
+    _sapp.keycodes[0x152] = SAPP_KEYCODE_INSERT;
+    _sapp.keycodes[0x15D] = SAPP_KEYCODE_MENU;
+    _sapp.keycodes[0x151] = SAPP_KEYCODE_PAGE_DOWN;
+    _sapp.keycodes[0x149] = SAPP_KEYCODE_PAGE_UP;
+    _sapp.keycodes[0x045] = SAPP_KEYCODE_PAUSE;
+    _sapp.keycodes[0x146] = SAPP_KEYCODE_PAUSE;
+    _sapp.keycodes[0x039] = SAPP_KEYCODE_SPACE;
+    _sapp.keycodes[0x00F] = SAPP_KEYCODE_TAB;
+    _sapp.keycodes[0x03A] = SAPP_KEYCODE_CAPS_LOCK;
+    _sapp.keycodes[0x145] = SAPP_KEYCODE_NUM_LOCK;
+    _sapp.keycodes[0x046] = SAPP_KEYCODE_SCROLL_LOCK;
+    _sapp.keycodes[0x03B] = SAPP_KEYCODE_F1;
+    _sapp.keycodes[0x03C] = SAPP_KEYCODE_F2;
+    _sapp.keycodes[0x03D] = SAPP_KEYCODE_F3;
+    _sapp.keycodes[0x03E] = SAPP_KEYCODE_F4;
+    _sapp.keycodes[0x03F] = SAPP_KEYCODE_F5;
+    _sapp.keycodes[0x040] = SAPP_KEYCODE_F6;
+    _sapp.keycodes[0x041] = SAPP_KEYCODE_F7;
+    _sapp.keycodes[0x042] = SAPP_KEYCODE_F8;
+    _sapp.keycodes[0x043] = SAPP_KEYCODE_F9;
+    _sapp.keycodes[0x044] = SAPP_KEYCODE_F10;
+    _sapp.keycodes[0x057] = SAPP_KEYCODE_F11;
+    _sapp.keycodes[0x058] = SAPP_KEYCODE_F12;
+    _sapp.keycodes[0x064] = SAPP_KEYCODE_F13;
+    _sapp.keycodes[0x065] = SAPP_KEYCODE_F14;
+    _sapp.keycodes[0x066] = SAPP_KEYCODE_F15;
+    _sapp.keycodes[0x067] = SAPP_KEYCODE_F16;
+    _sapp.keycodes[0x068] = SAPP_KEYCODE_F17;
+    _sapp.keycodes[0x069] = SAPP_KEYCODE_F18;
+    _sapp.keycodes[0x06A] = SAPP_KEYCODE_F19;
+    _sapp.keycodes[0x06B] = SAPP_KEYCODE_F20;
+    _sapp.keycodes[0x06C] = SAPP_KEYCODE_F21;
+    _sapp.keycodes[0x06D] = SAPP_KEYCODE_F22;
+    _sapp.keycodes[0x06E] = SAPP_KEYCODE_F23;
+    _sapp.keycodes[0x076] = SAPP_KEYCODE_F24;
+    _sapp.keycodes[0x038] = SAPP_KEYCODE_LEFT_ALT;
+    _sapp.keycodes[0x01D] = SAPP_KEYCODE_LEFT_CONTROL;
+    _sapp.keycodes[0x02A] = SAPP_KEYCODE_LEFT_SHIFT;
+    _sapp.keycodes[0x15B] = SAPP_KEYCODE_LEFT_SUPER;
+    _sapp.keycodes[0x137] = SAPP_KEYCODE_PRINT_SCREEN;
+    _sapp.keycodes[0x138] = SAPP_KEYCODE_RIGHT_ALT;
+    _sapp.keycodes[0x11D] = SAPP_KEYCODE_RIGHT_CONTROL;
+    _sapp.keycodes[0x036] = SAPP_KEYCODE_RIGHT_SHIFT;
+    _sapp.keycodes[0x15C] = SAPP_KEYCODE_RIGHT_SUPER;
+    _sapp.keycodes[0x150] = SAPP_KEYCODE_DOWN;
+    _sapp.keycodes[0x14B] = SAPP_KEYCODE_LEFT;
+    _sapp.keycodes[0x14D] = SAPP_KEYCODE_RIGHT;
+    _sapp.keycodes[0x148] = SAPP_KEYCODE_UP;
+    _sapp.keycodes[0x052] = SAPP_KEYCODE_KP_0;
+    _sapp.keycodes[0x04F] = SAPP_KEYCODE_KP_1;
+    _sapp.keycodes[0x050] = SAPP_KEYCODE_KP_2;
+    _sapp.keycodes[0x051] = SAPP_KEYCODE_KP_3;
+    _sapp.keycodes[0x04B] = SAPP_KEYCODE_KP_4;
+    _sapp.keycodes[0x04C] = SAPP_KEYCODE_KP_5;
+    _sapp.keycodes[0x04D] = SAPP_KEYCODE_KP_6;
+    _sapp.keycodes[0x047] = SAPP_KEYCODE_KP_7;
+    _sapp.keycodes[0x048] = SAPP_KEYCODE_KP_8;
+    _sapp.keycodes[0x049] = SAPP_KEYCODE_KP_9;
+    _sapp.keycodes[0x04E] = SAPP_KEYCODE_KP_ADD;
+    _sapp.keycodes[0x053] = SAPP_KEYCODE_KP_DECIMAL;
+    _sapp.keycodes[0x135] = SAPP_KEYCODE_KP_DIVIDE;
+    _sapp.keycodes[0x11C] = SAPP_KEYCODE_KP_ENTER;
+    _sapp.keycodes[0x037] = SAPP_KEYCODE_KP_MULTIPLY;
+    _sapp.keycodes[0x04A] = SAPP_KEYCODE_KP_SUBTRACT;
+}
+#endif // _SAPP_WIN32 || _SAPP_UWP
+
+/*== WINDOWS DESKTOP===========================================================*/
+#if defined(_SAPP_WIN32)
+
+#if defined(SOKOL_D3D11)
+
+#if defined(__cplusplus)
+#define _sapp_d3d11_Release(self) (self)->Release()
+#else
+#define _sapp_d3d11_Release(self) (self)->lpVtbl->Release(self)
+#endif
+
+#define _SAPP_SAFE_RELEASE(obj) if (obj) { _sapp_d3d11_Release(obj); obj=0; }
+
+static const IID _sapp_IID_ID3D11Texture2D = { 0x6f15aaf2,0xd208,0x4e89,0x9a,0xb4,0x48,0x95,0x35,0xd3,0x4f,0x9c };
+
+static inline HRESULT _sapp_dxgi_GetBuffer(IDXGISwapChain* self, UINT Buffer, REFIID riid, void** ppSurface) {
+    #if defined(__cplusplus)
+        return self->GetBuffer(Buffer, riid, ppSurface);
+    #else
+        return self->lpVtbl->GetBuffer(self, Buffer, riid, ppSurface);
+    #endif
+}
+
+static inline HRESULT _sapp_d3d11_CreateRenderTargetView(ID3D11Device* self, ID3D11Resource *pResource, const D3D11_RENDER_TARGET_VIEW_DESC* pDesc, ID3D11RenderTargetView** ppRTView) {
+    #if defined(__cplusplus)
+        return self->CreateRenderTargetView(pResource, pDesc, ppRTView);
+    #else
+        return self->lpVtbl->CreateRenderTargetView(self, pResource, pDesc, ppRTView);
+    #endif
+}
+
+static inline HRESULT _sapp_d3d11_CreateTexture2D(ID3D11Device* self, const D3D11_TEXTURE2D_DESC* pDesc, const D3D11_SUBRESOURCE_DATA* pInitialData, ID3D11Texture2D** ppTexture2D) {
+    #if defined(__cplusplus)
+        return self->CreateTexture2D(pDesc, pInitialData, ppTexture2D);
+    #else
+        return self->lpVtbl->CreateTexture2D(self, pDesc, pInitialData, ppTexture2D);
+    #endif
+}
+
+static inline HRESULT _sapp_d3d11_CreateDepthStencilView(ID3D11Device* self, ID3D11Resource* pResource, const D3D11_DEPTH_STENCIL_VIEW_DESC* pDesc, ID3D11DepthStencilView** ppDepthStencilView) {
+    #if defined(__cplusplus)
+        return self->CreateDepthStencilView(pResource, pDesc, ppDepthStencilView);
+    #else
+        return self->lpVtbl->CreateDepthStencilView(self, pResource, pDesc, ppDepthStencilView);
+    #endif
+}
+
+static inline void _sapp_d3d11_ResolveSubresource(ID3D11DeviceContext* self, ID3D11Resource* pDstResource, UINT DstSubresource, ID3D11Resource* pSrcResource, UINT SrcSubresource, DXGI_FORMAT Format) {
+    #if defined(__cplusplus)
+        self->ResolveSubresource(pDstResource, DstSubresource, pSrcResource, SrcSubresource, Format);
+    #else
+        self->lpVtbl->ResolveSubresource(self, pDstResource, DstSubresource, pSrcResource, SrcSubresource, Format);
+    #endif
+}
+
+static inline HRESULT _sapp_dxgi_ResizeBuffers(IDXGISwapChain* self, UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags) {
+    #if defined(__cplusplus)
+        return self->ResizeBuffers(BufferCount, Width, Height, NewFormat, SwapChainFlags);
+    #else
+        return self->lpVtbl->ResizeBuffers(self, BufferCount, Width, Height, NewFormat, SwapChainFlags);
+    #endif
+}
+
+static inline HRESULT _sapp_dxgi_Present(IDXGISwapChain* self, UINT SyncInterval, UINT Flags) {
+    #if defined(__cplusplus)
+        return self->Present(SyncInterval, Flags);
+    #else
+        return self->lpVtbl->Present(self, SyncInterval, Flags);
+    #endif
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_create_device_and_swapchain(void) {
+    DXGI_SWAP_CHAIN_DESC* sc_desc = &_sapp.d3d11.swap_chain_desc;
+    sc_desc->BufferDesc.Width = (UINT)_sapp.framebuffer_width;
+    sc_desc->BufferDesc.Height = (UINT)_sapp.framebuffer_height;
+    sc_desc->BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+    sc_desc->BufferDesc.RefreshRate.Numerator = 60;
+    sc_desc->BufferDesc.RefreshRate.Denominator = 1;
+    sc_desc->OutputWindow = _sapp.win32.hwnd;
+    sc_desc->Windowed = true;
+    if (_sapp.win32.is_win10_or_greater) {
+        sc_desc->BufferCount = 2;
+        sc_desc->SwapEffect = (DXGI_SWAP_EFFECT) _SAPP_DXGI_SWAP_EFFECT_FLIP_DISCARD;
+    }
+    else {
+        sc_desc->BufferCount = 1;
+        sc_desc->SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
+    }
+    sc_desc->SampleDesc.Count = 1;
+    sc_desc->SampleDesc.Quality = 0;
+    sc_desc->BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+    UINT create_flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT;
+    #if defined(SOKOL_DEBUG)
+        create_flags |= D3D11_CREATE_DEVICE_DEBUG;
+    #endif
+    D3D_FEATURE_LEVEL feature_level;
+    HRESULT hr = D3D11CreateDeviceAndSwapChain(
+        NULL,                           /* pAdapter (use default) */
+        D3D_DRIVER_TYPE_HARDWARE,       /* DriverType */
+        NULL,                           /* Software */
+        create_flags,                   /* Flags */
+        NULL,                           /* pFeatureLevels */
+        0,                              /* FeatureLevels */
+        D3D11_SDK_VERSION,              /* SDKVersion */
+        sc_desc,                        /* pSwapChainDesc */
+        &_sapp.d3d11.swap_chain,        /* ppSwapChain */
+        &_sapp.d3d11.device,            /* ppDevice */
+        &feature_level,                 /* pFeatureLevel */
+        &_sapp.d3d11.device_context);   /* ppImmediateContext */
+    _SOKOL_UNUSED(hr);
+    SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.swap_chain && _sapp.d3d11.device && _sapp.d3d11.device_context);
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_destroy_device_and_swapchain(void) {
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.swap_chain);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.device_context);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.device);
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_create_default_render_target(void) {
+    SOKOL_ASSERT(0 == _sapp.d3d11.rt);
+    SOKOL_ASSERT(0 == _sapp.d3d11.rtv);
+    SOKOL_ASSERT(0 == _sapp.d3d11.msaa_rt);
+    SOKOL_ASSERT(0 == _sapp.d3d11.msaa_rtv);
+    SOKOL_ASSERT(0 == _sapp.d3d11.ds);
+    SOKOL_ASSERT(0 == _sapp.d3d11.dsv);
+
+    HRESULT hr;
+
+    /* view for the swapchain-created framebuffer */
+    #ifdef __cplusplus
+    hr = _sapp_dxgi_GetBuffer(_sapp.d3d11.swap_chain, 0, _sapp_IID_ID3D11Texture2D, (void**)&_sapp.d3d11.rt);
+    #else
+    hr = _sapp_dxgi_GetBuffer(_sapp.d3d11.swap_chain, 0, &_sapp_IID_ID3D11Texture2D, (void**)&_sapp.d3d11.rt);
+    #endif
+    SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.rt);
+    hr = _sapp_d3d11_CreateRenderTargetView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.rt, NULL, &_sapp.d3d11.rtv);
+    SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.rtv);
+
+    /* common desc for MSAA and depth-stencil texture */
+    D3D11_TEXTURE2D_DESC tex_desc;
+    memset(&tex_desc, 0, sizeof(tex_desc));
+    tex_desc.Width = (UINT)_sapp.framebuffer_width;
+    tex_desc.Height = (UINT)_sapp.framebuffer_height;
+    tex_desc.MipLevels = 1;
+    tex_desc.ArraySize = 1;
+    tex_desc.Usage = D3D11_USAGE_DEFAULT;
+    tex_desc.BindFlags = D3D11_BIND_RENDER_TARGET;
+    tex_desc.SampleDesc.Count = (UINT) _sapp.sample_count;
+    tex_desc.SampleDesc.Quality = (UINT) (_sapp.sample_count > 1 ? D3D11_STANDARD_MULTISAMPLE_PATTERN : 0);
+
+    /* create MSAA texture and view if antialiasing requested */
+    if (_sapp.sample_count > 1) {
+        tex_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+        hr = _sapp_d3d11_CreateTexture2D(_sapp.d3d11.device, &tex_desc, NULL, &_sapp.d3d11.msaa_rt);
+        SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.msaa_rt);
+        hr = _sapp_d3d11_CreateRenderTargetView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.msaa_rt, NULL, &_sapp.d3d11.msaa_rtv);
+        SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.msaa_rtv);
+    }
+
+    /* texture and view for the depth-stencil-surface */
+    tex_desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
+    tex_desc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
+    hr = _sapp_d3d11_CreateTexture2D(_sapp.d3d11.device, &tex_desc, NULL, &_sapp.d3d11.ds);
+    SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.ds);
+    hr = _sapp_d3d11_CreateDepthStencilView(_sapp.d3d11.device, (ID3D11Resource*)_sapp.d3d11.ds, NULL, &_sapp.d3d11.dsv);
+    SOKOL_ASSERT(SUCCEEDED(hr) && _sapp.d3d11.dsv);
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_destroy_default_render_target(void) {
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.rt);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.rtv);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.msaa_rt);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.msaa_rtv);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.ds);
+    _SAPP_SAFE_RELEASE(_sapp.d3d11.dsv);
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_resize_default_render_target(void) {
+    if (_sapp.d3d11.swap_chain) {
+        _sapp_d3d11_destroy_default_render_target();
+        _sapp_dxgi_ResizeBuffers(_sapp.d3d11.swap_chain, _sapp.d3d11.swap_chain_desc.BufferCount, (UINT)_sapp.framebuffer_width, (UINT)_sapp.framebuffer_height, DXGI_FORMAT_B8G8R8A8_UNORM, 0);
+        _sapp_d3d11_create_default_render_target();
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_d3d11_present(void) {
+    /* do MSAA resolve if needed */
+    if (_sapp.sample_count > 1) {
+        SOKOL_ASSERT(_sapp.d3d11.rt);
+        SOKOL_ASSERT(_sapp.d3d11.msaa_rt);
+        _sapp_d3d11_ResolveSubresource(_sapp.d3d11.device_context, (ID3D11Resource*)_sapp.d3d11.rt, 0, (ID3D11Resource*)_sapp.d3d11.msaa_rt, 0, DXGI_FORMAT_B8G8R8A8_UNORM);
+    }
+    _sapp_dxgi_Present(_sapp.d3d11.swap_chain, (UINT)_sapp.swap_interval, 0);
+}
+
+#endif /* SOKOL_D3D11 */
+
+#if defined(SOKOL_GLCORE33)
+_SOKOL_PRIVATE void _sapp_wgl_init(void) {
+    _sapp.wgl.opengl32 = LoadLibraryA("opengl32.dll");
+    if (!_sapp.wgl.opengl32) {
+        _sapp_fail("Failed to load opengl32.dll\n");
+    }
+    SOKOL_ASSERT(_sapp.wgl.opengl32);
+    _sapp.wgl.CreateContext = (PFN_wglCreateContext)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglCreateContext");
+    SOKOL_ASSERT(_sapp.wgl.CreateContext);
+    _sapp.wgl.DeleteContext = (PFN_wglDeleteContext)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglDeleteContext");
+    SOKOL_ASSERT(_sapp.wgl.DeleteContext);
+    _sapp.wgl.GetProcAddress = (PFN_wglGetProcAddress)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglGetProcAddress");
+    SOKOL_ASSERT(_sapp.wgl.GetProcAddress);
+    _sapp.wgl.GetCurrentDC = (PFN_wglGetCurrentDC)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglGetCurrentDC");
+    SOKOL_ASSERT(_sapp.wgl.GetCurrentDC);
+    _sapp.wgl.MakeCurrent = (PFN_wglMakeCurrent)(void*) GetProcAddress(_sapp.wgl.opengl32, "wglMakeCurrent");
+    SOKOL_ASSERT(_sapp.wgl.MakeCurrent);
+
+    _sapp.wgl.msg_hwnd = CreateWindowExW(WS_EX_OVERLAPPEDWINDOW,
+        L"SOKOLAPP",
+        L"sokol-app message window",
+        WS_CLIPSIBLINGS|WS_CLIPCHILDREN,
+        0, 0, 1, 1,
+        NULL, NULL,
+        GetModuleHandleW(NULL),
+        NULL);
+    if (!_sapp.wgl.msg_hwnd) {
+        _sapp_fail("Win32: failed to create helper window!\n");
+    }
+    SOKOL_ASSERT(_sapp.wgl.msg_hwnd);
+    ShowWindow(_sapp.wgl.msg_hwnd, SW_HIDE);
+    MSG msg;
+    while (PeekMessageW(&msg, _sapp.wgl.msg_hwnd, 0, 0, PM_REMOVE)) {
+        TranslateMessage(&msg);
+        DispatchMessageW(&msg);
+    }
+    _sapp.wgl.msg_dc = GetDC(_sapp.wgl.msg_hwnd);
+    if (!_sapp.wgl.msg_dc) {
+        _sapp_fail("Win32: failed to obtain helper window DC!\n");
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_wgl_shutdown(void) {
+    SOKOL_ASSERT(_sapp.wgl.opengl32 && _sapp.wgl.msg_hwnd);
+    DestroyWindow(_sapp.wgl.msg_hwnd); _sapp.wgl.msg_hwnd = 0;
+    FreeLibrary(_sapp.wgl.opengl32); _sapp.wgl.opengl32 = 0;
+}
+
+_SOKOL_PRIVATE bool _sapp_wgl_has_ext(const char* ext, const char* extensions) {
+    SOKOL_ASSERT(ext && extensions);
+    const char* start = extensions;
+    while (true) {
+        const char* where = strstr(start, ext);
+        if (!where) {
+            return false;
+        }
+        const char* terminator = where + strlen(ext);
+        if ((where == start) || (*(where - 1) == ' ')) {
+            if (*terminator == ' ' || *terminator == '\0') {
+                break;
+            }
+        }
+        start = terminator;
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE bool _sapp_wgl_ext_supported(const char* ext) {
+    SOKOL_ASSERT(ext);
+    if (_sapp.wgl.GetExtensionsStringEXT) {
+        const char* extensions = _sapp.wgl.GetExtensionsStringEXT();
+        if (extensions) {
+            if (_sapp_wgl_has_ext(ext, extensions)) {
+                return true;
+            }
+        }
+    }
+    if (_sapp.wgl.GetExtensionsStringARB) {
+        const char* extensions = _sapp.wgl.GetExtensionsStringARB(_sapp.wgl.GetCurrentDC());
+        if (extensions) {
+            if (_sapp_wgl_has_ext(ext, extensions)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+_SOKOL_PRIVATE void _sapp_wgl_load_extensions(void) {
+    SOKOL_ASSERT(_sapp.wgl.msg_dc);
+    PIXELFORMATDESCRIPTOR pfd;
+    memset(&pfd, 0, sizeof(pfd));
+    pfd.nSize = sizeof(pfd);
+    pfd.nVersion = 1;
+    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
+    pfd.iPixelType = PFD_TYPE_RGBA;
+    pfd.cColorBits = 24;
+    if (!SetPixelFormat(_sapp.wgl.msg_dc, ChoosePixelFormat(_sapp.wgl.msg_dc, &pfd), &pfd)) {
+        _sapp_fail("WGL: failed to set pixel format for dummy context\n");
+    }
+    HGLRC rc = _sapp.wgl.CreateContext(_sapp.wgl.msg_dc);
+    if (!rc) {
+        _sapp_fail("WGL: Failed to create dummy context\n");
+    }
+    if (!_sapp.wgl.MakeCurrent(_sapp.wgl.msg_dc, rc)) {
+        _sapp_fail("WGL: Failed to make context current\n");
+    }
+    _sapp.wgl.GetExtensionsStringEXT = (PFNWGLGETEXTENSIONSSTRINGEXTPROC)(void*) _sapp.wgl.GetProcAddress("wglGetExtensionsStringEXT");
+    _sapp.wgl.GetExtensionsStringARB = (PFNWGLGETEXTENSIONSSTRINGARBPROC)(void*) _sapp.wgl.GetProcAddress("wglGetExtensionsStringARB");
+    _sapp.wgl.CreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)(void*) _sapp.wgl.GetProcAddress("wglCreateContextAttribsARB");
+    _sapp.wgl.SwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)(void*) _sapp.wgl.GetProcAddress("wglSwapIntervalEXT");
+    _sapp.wgl.GetPixelFormatAttribivARB = (PFNWGLGETPIXELFORMATATTRIBIVARBPROC)(void*) _sapp.wgl.GetProcAddress("wglGetPixelFormatAttribivARB");
+    _sapp.wgl.arb_multisample = _sapp_wgl_ext_supported("WGL_ARB_multisample");
+    _sapp.wgl.arb_create_context = _sapp_wgl_ext_supported("WGL_ARB_create_context");
+    _sapp.wgl.arb_create_context_profile = _sapp_wgl_ext_supported("WGL_ARB_create_context_profile");
+    _sapp.wgl.ext_swap_control = _sapp_wgl_ext_supported("WGL_EXT_swap_control");
+    _sapp.wgl.arb_pixel_format = _sapp_wgl_ext_supported("WGL_ARB_pixel_format");
+    _sapp.wgl.MakeCurrent(_sapp.wgl.msg_dc, 0);
+    _sapp.wgl.DeleteContext(rc);
+}
+
+_SOKOL_PRIVATE int _sapp_wgl_attrib(int pixel_format, int attrib) {
+    SOKOL_ASSERT(_sapp.wgl.arb_pixel_format);
+    int value = 0;
+    if (!_sapp.wgl.GetPixelFormatAttribivARB(_sapp.win32.dc, pixel_format, 0, 1, &attrib, &value)) {
+        _sapp_fail("WGL: Failed to retrieve pixel format attribute\n");
+    }
+    return value;
+}
+
+_SOKOL_PRIVATE int _sapp_wgl_find_pixel_format(void) {
+    SOKOL_ASSERT(_sapp.win32.dc);
+    SOKOL_ASSERT(_sapp.wgl.arb_pixel_format);
+    const _sapp_gl_fbconfig* closest;
+
+    int native_count = _sapp_wgl_attrib(1, WGL_NUMBER_PIXEL_FORMATS_ARB);
+    _sapp_gl_fbconfig* usable_configs = (_sapp_gl_fbconfig*) SOKOL_CALLOC((size_t)native_count, sizeof(_sapp_gl_fbconfig));
+    SOKOL_ASSERT(usable_configs);
+    int usable_count = 0;
+    for (int i = 0; i < native_count; i++) {
+        const int n = i + 1;
+        _sapp_gl_fbconfig* u = usable_configs + usable_count;
+        _sapp_gl_init_fbconfig(u);
+        if (!_sapp_wgl_attrib(n, WGL_SUPPORT_OPENGL_ARB) || !_sapp_wgl_attrib(n, WGL_DRAW_TO_WINDOW_ARB)) {
+            continue;
+        }
+        if (_sapp_wgl_attrib(n, WGL_PIXEL_TYPE_ARB) != WGL_TYPE_RGBA_ARB) {
+            continue;
+        }
+        if (_sapp_wgl_attrib(n, WGL_ACCELERATION_ARB) == WGL_NO_ACCELERATION_ARB) {
+            continue;
+        }
+        u->red_bits     = _sapp_wgl_attrib(n, WGL_RED_BITS_ARB);
+        u->green_bits   = _sapp_wgl_attrib(n, WGL_GREEN_BITS_ARB);
+        u->blue_bits    = _sapp_wgl_attrib(n, WGL_BLUE_BITS_ARB);
+        u->alpha_bits   = _sapp_wgl_attrib(n, WGL_ALPHA_BITS_ARB);
+        u->depth_bits   = _sapp_wgl_attrib(n, WGL_DEPTH_BITS_ARB);
+        u->stencil_bits = _sapp_wgl_attrib(n, WGL_STENCIL_BITS_ARB);
+        if (_sapp_wgl_attrib(n, WGL_DOUBLE_BUFFER_ARB)) {
+            u->doublebuffer = true;
+        }
+        if (_sapp.wgl.arb_multisample) {
+            u->samples = _sapp_wgl_attrib(n, WGL_SAMPLES_ARB);
+        }
+        u->handle = (uintptr_t)n;
+        usable_count++;
+    }
+    SOKOL_ASSERT(usable_count > 0);
+    _sapp_gl_fbconfig desired;
+    _sapp_gl_init_fbconfig(&desired);
+    desired.red_bits = 8;
+    desired.green_bits = 8;
+    desired.blue_bits = 8;
+    desired.alpha_bits = 8;
+    desired.depth_bits = 24;
+    desired.stencil_bits = 8;
+    desired.doublebuffer = true;
+    desired.samples = _sapp.sample_count > 1 ? _sapp.sample_count : 0;
+    closest = _sapp_gl_choose_fbconfig(&desired, usable_configs, usable_count);
+    int pixel_format = 0;
+    if (closest) {
+        pixel_format = (int) closest->handle;
+    }
+    SOKOL_FREE(usable_configs);
+    return pixel_format;
+}
+
+_SOKOL_PRIVATE void _sapp_wgl_create_context(void) {
+    int pixel_format = _sapp_wgl_find_pixel_format();
+    if (0 == pixel_format) {
+        _sapp_fail("WGL: Didn't find matching pixel format.\n");
+    }
+    PIXELFORMATDESCRIPTOR pfd;
+    if (!DescribePixelFormat(_sapp.win32.dc, pixel_format, sizeof(pfd), &pfd)) {
+        _sapp_fail("WGL: Failed to retrieve PFD for selected pixel format!\n");
+    }
+    if (!SetPixelFormat(_sapp.win32.dc, pixel_format, &pfd)) {
+        _sapp_fail("WGL: Failed to set selected pixel format!\n");
+    }
+    if (!_sapp.wgl.arb_create_context) {
+        _sapp_fail("WGL: ARB_create_context required!\n");
+    }
+    if (!_sapp.wgl.arb_create_context_profile) {
+        _sapp_fail("WGL: ARB_create_context_profile required!\n");
+    }
+    const int attrs[] = {
+        WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
+        WGL_CONTEXT_MINOR_VERSION_ARB, 3,
+        WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
+        WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
+        0, 0
+    };
+    _sapp.wgl.gl_ctx = _sapp.wgl.CreateContextAttribsARB(_sapp.win32.dc, 0, attrs);
+    if (!_sapp.wgl.gl_ctx) {
+        const DWORD err = GetLastError();
+        if (err == (0xc0070000 | ERROR_INVALID_VERSION_ARB)) {
+            _sapp_fail("WGL: Driver does not support OpenGL version 3.3\n");
+        }
+        else if (err == (0xc0070000 | ERROR_INVALID_PROFILE_ARB)) {
+            _sapp_fail("WGL: Driver does not support the requested OpenGL profile");
+        }
+        else if (err == (0xc0070000 | ERROR_INCOMPATIBLE_DEVICE_CONTEXTS_ARB)) {
+            _sapp_fail("WGL: The share context is not compatible with the requested context");
+        }
+        else {
+            _sapp_fail("WGL: Failed to create OpenGL context");
+        }
+    }
+    _sapp.wgl.MakeCurrent(_sapp.win32.dc, _sapp.wgl.gl_ctx);
+    if (_sapp.wgl.ext_swap_control) {
+        /* FIXME: DwmIsCompositionEnabled() (see GLFW) */
+        _sapp.wgl.SwapIntervalEXT(_sapp.swap_interval);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_wgl_destroy_context(void) {
+    SOKOL_ASSERT(_sapp.wgl.gl_ctx);
+    _sapp.wgl.DeleteContext(_sapp.wgl.gl_ctx);
+    _sapp.wgl.gl_ctx = 0;
+}
+
+_SOKOL_PRIVATE void _sapp_wgl_swap_buffers(void) {
+    SOKOL_ASSERT(_sapp.win32.dc);
+    /* FIXME: DwmIsCompositionEnabled? (see GLFW) */
+    SwapBuffers(_sapp.win32.dc);
+}
+#endif /* SOKOL_GLCORE33 */
+
+_SOKOL_PRIVATE bool _sapp_win32_wide_to_utf8(const wchar_t* src, char* dst, int dst_num_bytes) {
+    SOKOL_ASSERT(src && dst && (dst_num_bytes > 1));
+    memset(dst, 0, (size_t)dst_num_bytes);
+    const int bytes_needed = WideCharToMultiByte(CP_UTF8, 0, src, -1, NULL, 0, NULL, NULL);
+    if (bytes_needed <= dst_num_bytes) {
+        WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst_num_bytes, NULL, NULL);
+        return true;
+    }
+    else {
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_toggle_fullscreen(void) {
+    HMONITOR monitor = MonitorFromWindow(_sapp.win32.hwnd, MONITOR_DEFAULTTONEAREST);
+    MONITORINFO minfo;
+    memset(&minfo, 0, sizeof(minfo));
+    minfo.cbSize = sizeof(MONITORINFO);
+    GetMonitorInfo(monitor, &minfo);
+    const RECT mr = minfo.rcMonitor;
+    const int monitor_w = mr.right - mr.left;
+    const int monitor_h = mr.bottom - mr.top;
+
+    const DWORD win_ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
+    DWORD win_style;
+    RECT rect = { 0, 0, 0, 0 };
+
+    _sapp.fullscreen = !_sapp.fullscreen;
+    if (!_sapp.fullscreen) {
+        win_style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX;
+        rect.right = (int) ((float)_sapp.desc.width * _sapp.win32.dpi.window_scale);
+        rect.bottom = (int) ((float)_sapp.desc.height * _sapp.win32.dpi.window_scale);
+    }
+    else {
+        win_style = WS_POPUP | WS_SYSMENU | WS_VISIBLE;
+        rect.right = monitor_w;
+        rect.bottom = monitor_h;
+    }
+    AdjustWindowRectEx(&rect, win_style, FALSE, win_ex_style);
+    int win_width = rect.right - rect.left;
+    int win_height = rect.bottom - rect.top;
+    if (!_sapp.fullscreen) {
+        rect.left = (monitor_w - win_width) / 2;
+        rect.top = (monitor_h - win_height) / 2;
+    }
+
+    SetWindowLongPtr(_sapp.win32.hwnd, GWL_STYLE, win_style);
+    SetWindowPos(_sapp.win32.hwnd, HWND_TOP, mr.left + rect.left, mr.top + rect.top, win_width, win_height, SWP_SHOWWINDOW | SWP_FRAMECHANGED);
+}
+
+_SOKOL_PRIVATE void _sapp_win32_show_mouse(bool visible) {
+    /* NOTE: this function is only called when the mouse visibility actually changes */
+    ShowCursor((BOOL)visible);
+}
+
+_SOKOL_PRIVATE void _sapp_win32_capture_mouse(uint8_t btn_mask) {
+    if (0 == _sapp.win32.mouse_capture_mask) {
+        SetCapture(_sapp.win32.hwnd);
+    }
+    _sapp.win32.mouse_capture_mask |= btn_mask;
+}
+
+_SOKOL_PRIVATE void _sapp_win32_release_mouse(uint8_t btn_mask) {
+    if (0 != _sapp.win32.mouse_capture_mask) {
+        _sapp.win32.mouse_capture_mask &= ~btn_mask;
+        if (0 == _sapp.win32.mouse_capture_mask) {
+            ReleaseCapture();
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_lock_mouse(bool lock) {
+    if (lock == _sapp.mouse.locked) {
+        return;
+    }
+    _sapp.mouse.dx = 0.0f;
+    _sapp.mouse.dy = 0.0f;
+    _sapp.mouse.locked = lock;
+    _sapp_win32_release_mouse(0xFF);
+    if (_sapp.mouse.locked) {
+        /* store the current mouse position, so it can be restored when unlocked */
+        POINT pos;
+        BOOL res = GetCursorPos(&pos);
+        SOKOL_ASSERT(res); _SOKOL_UNUSED(res);
+        _sapp.win32.mouse_locked_x = pos.x;
+        _sapp.win32.mouse_locked_y = pos.y;
+
+        /* while the mouse is locked, make the mouse cursor invisible and
+           confine the mouse movement to a small rectangle inside our window
+           (so that we dont miss any mouse up events)
+        */
+        RECT client_rect = {
+            _sapp.win32.mouse_locked_x,
+            _sapp.win32.mouse_locked_y,
+            _sapp.win32.mouse_locked_x,
+            _sapp.win32.mouse_locked_y
+        };
+        ClipCursor(&client_rect);
+
+        /* make the mouse cursor invisible, this will stack with sapp_show_mouse() */
+        ShowCursor(FALSE);
+
+        /* enable raw input for mouse, starts sending WM_INPUT messages to WinProc (see GLFW) */
+        const RAWINPUTDEVICE rid = {
+            0x01,   // usUsagePage: HID_USAGE_PAGE_GENERIC
+            0x02,   // usUsage: HID_USAGE_GENERIC_MOUSE
+            0,      // dwFlags
+            _sapp.win32.hwnd    // hwndTarget
+        };
+        if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) {
+            SOKOL_LOG("RegisterRawInputDevices() failed (on mouse lock).\n");
+        }
+        /* in case the raw mouse device only supports absolute position reporting,
+           we need to skip the dx/dy compution for the first WM_INPUT event
+        */
+        _sapp.win32.raw_input_mousepos_valid = false;
+    }
+    else {
+        /* disable raw input for mouse */
+        const RAWINPUTDEVICE rid = { 0x01, 0x02, RIDEV_REMOVE, NULL };
+        if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) {
+            SOKOL_LOG("RegisterRawInputDevices() failed (on mouse unlock).\n");
+        }
+
+        /* let the mouse roam freely again */
+        ClipCursor(NULL);
+        ShowCursor(TRUE);
+
+        /* restore the 'pre-locked' mouse position */
+        BOOL res = SetCursorPos(_sapp.win32.mouse_locked_x, _sapp.win32.mouse_locked_y);
+        SOKOL_ASSERT(res); _SOKOL_UNUSED(res);
+    }
+}
+
+/* updates current window and framebuffer size from the window's client rect, returns true if size has changed */
+_SOKOL_PRIVATE bool _sapp_win32_update_dimensions(void) {
+    RECT rect;
+    if (GetClientRect(_sapp.win32.hwnd, &rect)) {
+        _sapp.window_width = (int)((float)(rect.right - rect.left) / _sapp.win32.dpi.window_scale);
+        _sapp.window_height = (int)((float)(rect.bottom - rect.top) / _sapp.win32.dpi.window_scale);
+        int fb_width = (int)((float)_sapp.window_width * _sapp.win32.dpi.content_scale);
+        int fb_height = (int)((float)_sapp.window_height * _sapp.win32.dpi.content_scale);
+        /* prevent a framebuffer size of 0 when window is minimized */
+        if (0 == fb_width) {
+            fb_width = 1;
+        }
+        if (0 == fb_height) {
+            fb_height = 1;
+        }
+        if ((fb_width != _sapp.framebuffer_width) || (fb_height != _sapp.framebuffer_height)) {
+            _sapp.framebuffer_width = fb_width;
+            _sapp.framebuffer_height = fb_height;
+            return true;
+        }
+    }
+    else {
+        _sapp.window_width = _sapp.window_height = 1;
+        _sapp.framebuffer_width = _sapp.framebuffer_height = 1;
+    }
+    return false;
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_win32_mods(void) {
+    uint32_t mods = 0;
+    if (GetKeyState(VK_SHIFT) & (1<<15)) {
+        mods |= SAPP_MODIFIER_SHIFT;
+    }
+    if (GetKeyState(VK_CONTROL) & (1<<15)) {
+        mods |= SAPP_MODIFIER_CTRL;
+    }
+    if (GetKeyState(VK_MENU) & (1<<15)) {
+        mods |= SAPP_MODIFIER_ALT;
+    }
+    if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & (1<<15)) {
+        mods |= SAPP_MODIFIER_SUPER;
+    }
+    const bool swapped = (TRUE == GetSystemMetrics(SM_SWAPBUTTON));
+    if (GetAsyncKeyState(VK_LBUTTON)) {
+        mods |= swapped ? SAPP_MODIFIER_RMB : SAPP_MODIFIER_LMB;
+    }
+    if (GetAsyncKeyState(VK_RBUTTON)) {
+        mods |= swapped ? SAPP_MODIFIER_LMB : SAPP_MODIFIER_RMB;
+    }
+    if (GetAsyncKeyState(VK_MBUTTON)) {
+        mods |= SAPP_MODIFIER_MMB;
+    }
+    return mods;
+}
+
+_SOKOL_PRIVATE void _sapp_win32_mouse_event(sapp_event_type type, sapp_mousebutton btn) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.modifiers = _sapp_win32_mods();
+        _sapp.event.mouse_button = btn;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_scroll_event(float x, float y) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL);
+        _sapp.event.modifiers = _sapp_win32_mods();
+        _sapp.event.scroll_x = -x / 30.0f;
+        _sapp.event.scroll_y = y / 30.0f;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_key_event(sapp_event_type type, int vk, bool repeat) {
+    if (_sapp_events_enabled() && (vk < SAPP_MAX_KEYCODES)) {
+        _sapp_init_event(type);
+        _sapp.event.modifiers = _sapp_win32_mods();
+        _sapp.event.key_code = _sapp.keycodes[vk];
+        _sapp.event.key_repeat = repeat;
+        _sapp_call_event(&_sapp.event);
+        /* check if a CLIPBOARD_PASTED event must be sent too */
+        if (_sapp.clipboard.enabled &&
+            (type == SAPP_EVENTTYPE_KEY_DOWN) &&
+            (_sapp.event.modifiers == SAPP_MODIFIER_CTRL) &&
+            (_sapp.event.key_code == SAPP_KEYCODE_V))
+        {
+            _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_char_event(uint32_t c, bool repeat) {
+    if (_sapp_events_enabled() && (c >= 32)) {
+        _sapp_init_event(SAPP_EVENTTYPE_CHAR);
+        _sapp.event.modifiers = _sapp_win32_mods();
+        _sapp.event.char_code = c;
+        _sapp.event.key_repeat = repeat;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_files_dropped(HDROP hdrop) {
+    if (!_sapp.drop.enabled) {
+        return;
+    }
+    _sapp_clear_drop_buffer();
+    bool drop_failed = false;
+    const int count = (int) DragQueryFileW(hdrop, 0xffffffff, NULL, 0);
+    _sapp.drop.num_files = (count > _sapp.drop.max_files) ? _sapp.drop.max_files : count;
+    for (UINT i = 0;  i < (UINT)_sapp.drop.num_files;  i++) {
+        const UINT num_chars = DragQueryFileW(hdrop, i, NULL, 0) + 1;
+        WCHAR* buffer = (WCHAR*) SOKOL_CALLOC(num_chars, sizeof(WCHAR));
+        DragQueryFileW(hdrop, i, buffer, num_chars);
+        if (!_sapp_win32_wide_to_utf8(buffer, _sapp_dropped_file_path_ptr((int)i), _sapp.drop.max_path_length)) {
+            SOKOL_LOG("sokol_app.h: dropped file path too long (sapp_desc.max_dropped_file_path_length)\n");
+            drop_failed = true;
+        }
+        SOKOL_FREE(buffer);
+    }
+    DragFinish(hdrop);
+    if (!drop_failed) {
+        if (_sapp_events_enabled()) {
+            _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+    else {
+        _sapp_clear_drop_buffer();
+        _sapp.drop.num_files = 0;
+    }
+}
+
+_SOKOL_PRIVATE LRESULT CALLBACK _sapp_win32_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
+    if (!_sapp.win32.in_create_window) {
+        switch (uMsg) {
+            case WM_CLOSE:
+                /* only give user a chance to intervene when sapp_quit() wasn't already called */
+                if (!_sapp.quit_ordered) {
+                    /* if window should be closed and event handling is enabled, give user code
+                        a change to intervene via sapp_cancel_quit()
+                    */
+                    _sapp.quit_requested = true;
+                    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED);
+                    /* if user code hasn't intervened, quit the app */
+                    if (_sapp.quit_requested) {
+                        _sapp.quit_ordered = true;
+                    }
+                }
+                if (_sapp.quit_ordered) {
+                    PostQuitMessage(0);
+                }
+                return 0;
+            case WM_SYSCOMMAND:
+                switch (wParam & 0xFFF0) {
+                    case SC_SCREENSAVE:
+                    case SC_MONITORPOWER:
+                        if (_sapp.fullscreen) {
+                            /* disable screen saver and blanking in fullscreen mode */
+                            return 0;
+                        }
+                        break;
+                    case SC_KEYMENU:
+                        /* user trying to access menu via ALT */
+                        return 0;
+                }
+                break;
+            case WM_ERASEBKGND:
+                return 1;
+            case WM_SIZE:
+                {
+                    const bool iconified = wParam == SIZE_MINIMIZED;
+                    if (iconified != _sapp.win32.iconified) {
+                        _sapp.win32.iconified = iconified;
+                        if (iconified) {
+                            _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_ICONIFIED);
+                        }
+                        else {
+                            _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESTORED);
+                        }
+                    }
+                }
+                break;
+            case WM_SETCURSOR:
+                if (_sapp.desc.user_cursor) {
+                    if (LOWORD(lParam) == HTCLIENT) {
+                        _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_UPDATE_CURSOR);
+                        return 1;
+                    }
+                }
+                break;
+            case WM_LBUTTONDOWN:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT);
+                _sapp_win32_capture_mouse(1<<SAPP_MOUSEBUTTON_LEFT);
+                break;
+            case WM_RBUTTONDOWN:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_RIGHT);
+                _sapp_win32_capture_mouse(1<<SAPP_MOUSEBUTTON_RIGHT);
+                break;
+            case WM_MBUTTONDOWN:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_MIDDLE);
+                _sapp_win32_capture_mouse(1<<SAPP_MOUSEBUTTON_MIDDLE);
+                break;
+            case WM_LBUTTONUP:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_LEFT);
+                _sapp_win32_release_mouse(1<<SAPP_MOUSEBUTTON_LEFT);
+                break;
+            case WM_RBUTTONUP:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_RIGHT);
+                _sapp_win32_release_mouse(1<<SAPP_MOUSEBUTTON_RIGHT);
+                break;
+            case WM_MBUTTONUP:
+                _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_MIDDLE);
+                _sapp_win32_release_mouse(1<<SAPP_MOUSEBUTTON_MIDDLE);
+                break;
+            case WM_MOUSEMOVE:
+                if (!_sapp.mouse.locked) {
+                    const float new_x  = (float)GET_X_LPARAM(lParam) * _sapp.win32.dpi.mouse_scale;
+                    const float new_y = (float)GET_Y_LPARAM(lParam) * _sapp.win32.dpi.mouse_scale;
+                    /* don't update dx/dy in the very first event */
+                    if (_sapp.mouse.pos_valid) {
+                        _sapp.mouse.dx = new_x - _sapp.mouse.x;
+                        _sapp.mouse.dy = new_y - _sapp.mouse.y;
+                    }
+                    _sapp.mouse.x = new_x;
+                    _sapp.mouse.y = new_y;
+                    _sapp.mouse.pos_valid = true;
+                    if (!_sapp.win32.mouse_tracked) {
+                        _sapp.win32.mouse_tracked = true;
+                        TRACKMOUSEEVENT tme;
+                        memset(&tme, 0, sizeof(tme));
+                        tme.cbSize = sizeof(tme);
+                        tme.dwFlags = TME_LEAVE;
+                        tme.hwndTrack = _sapp.win32.hwnd;
+                        TrackMouseEvent(&tme);
+                        _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID);
+                    }
+                    _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID);
+                }
+                break;
+            case WM_INPUT:
+                /* raw mouse input during mouse-lock */
+                if (_sapp.mouse.locked) {
+                    HRAWINPUT ri = (HRAWINPUT) lParam;
+                    UINT size = sizeof(_sapp.win32.raw_input_data);
+                    // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getrawinputdata
+                    if ((UINT)-1 == GetRawInputData(ri, RID_INPUT, &_sapp.win32.raw_input_data, &size, sizeof(RAWINPUTHEADER))) {
+                        SOKOL_LOG("GetRawInputData() failed\n");
+                        break;
+                    }
+                    const RAWINPUT* raw_mouse_data = (const RAWINPUT*) &_sapp.win32.raw_input_data;
+                    if (raw_mouse_data->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) {
+                        /* mouse only reports absolute position
+                           NOTE: THIS IS UNTESTED, it's unclear from reading the
+                           Win32 RawInput docs under which circumstances absolute
+                           positions are sent.
+                        */
+                        if (_sapp.win32.raw_input_mousepos_valid) {
+                            LONG new_x = raw_mouse_data->data.mouse.lLastX;
+                            LONG new_y = raw_mouse_data->data.mouse.lLastY;
+                            _sapp.mouse.dx = (float) (new_x - _sapp.win32.raw_input_mousepos_x);
+                            _sapp.mouse.dy = (float) (new_y - _sapp.win32.raw_input_mousepos_y);
+                            _sapp.win32.raw_input_mousepos_x = new_x;
+                            _sapp.win32.raw_input_mousepos_y = new_y;
+                            _sapp.win32.raw_input_mousepos_valid = true;
+                        }
+                    }
+                    else {
+                        /* mouse reports movement delta (this seems to be the common case) */
+                        _sapp.mouse.dx = (float) raw_mouse_data->data.mouse.lLastX;
+                        _sapp.mouse.dy = (float) raw_mouse_data->data.mouse.lLastY;
+                    }
+                    _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID);
+                }
+                break;
+
+            case WM_MOUSELEAVE:
+                if (!_sapp.mouse.locked) {
+                    _sapp.win32.mouse_tracked = false;
+                    _sapp_win32_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID);
+                }
+                break;
+            case WM_MOUSEWHEEL:
+                _sapp_win32_scroll_event(0.0f, (float)((SHORT)HIWORD(wParam)));
+                break;
+            case WM_MOUSEHWHEEL:
+                _sapp_win32_scroll_event((float)((SHORT)HIWORD(wParam)), 0.0f);
+                break;
+            case WM_CHAR:
+                _sapp_win32_char_event((uint32_t)wParam, !!(lParam&0x40000000));
+                break;
+            case WM_KEYDOWN:
+            case WM_SYSKEYDOWN:
+                _sapp_win32_key_event(SAPP_EVENTTYPE_KEY_DOWN, (int)(HIWORD(lParam)&0x1FF), !!(lParam&0x40000000));
+                break;
+            case WM_KEYUP:
+            case WM_SYSKEYUP:
+                _sapp_win32_key_event(SAPP_EVENTTYPE_KEY_UP, (int)(HIWORD(lParam)&0x1FF), false);
+                break;
+            case WM_ENTERSIZEMOVE:
+                SetTimer(_sapp.win32.hwnd, 1, USER_TIMER_MINIMUM, NULL);
+                break;
+            case WM_EXITSIZEMOVE:
+                KillTimer(_sapp.win32.hwnd, 1);
+                break;
+            case WM_TIMER:
+                _sapp_frame();
+                #if defined(SOKOL_D3D11)
+                    _sapp_d3d11_present();
+                #endif
+                #if defined(SOKOL_GLCORE33)
+                    _sapp_wgl_swap_buffers();
+                #endif
+                /* NOTE: resizing the swap-chain during resize leads to a substantial
+                   memory spike (hundreds of megabytes for a few seconds).
+
+                if (_sapp_win32_update_dimensions()) {
+                    #if defined(SOKOL_D3D11)
+                    _sapp_d3d11_resize_default_render_target();
+                    #endif
+                    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESIZED);
+                }
+                */
+                break;
+            case WM_DROPFILES:
+                _sapp_win32_files_dropped((HDROP)wParam);
+                break;
+            default:
+                break;
+        }
+    }
+    return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+}
+
+_SOKOL_PRIVATE void _sapp_win32_create_window(void) {
+    WNDCLASSW wndclassw;
+    memset(&wndclassw, 0, sizeof(wndclassw));
+    wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
+    wndclassw.lpfnWndProc = (WNDPROC) _sapp_win32_wndproc;
+    wndclassw.hInstance = GetModuleHandleW(NULL);
+    wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW);
+    wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO);
+    wndclassw.lpszClassName = L"SOKOLAPP";
+    RegisterClassW(&wndclassw);
+
+    DWORD win_style;
+    const DWORD win_ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
+    RECT rect = { 0, 0, 0, 0 };
+    if (_sapp.fullscreen) {
+        win_style = WS_POPUP | WS_SYSMENU | WS_VISIBLE;
+        rect.right = GetSystemMetrics(SM_CXSCREEN);
+        rect.bottom = GetSystemMetrics(SM_CYSCREEN);
+    }
+    else {
+        win_style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX;
+        rect.right = (int) ((float)_sapp.window_width * _sapp.win32.dpi.window_scale);
+        rect.bottom = (int) ((float)_sapp.window_height * _sapp.win32.dpi.window_scale);
+    }
+    AdjustWindowRectEx(&rect, win_style, FALSE, win_ex_style);
+    const int win_width = rect.right - rect.left;
+    const int win_height = rect.bottom - rect.top;
+    _sapp.win32.in_create_window = true;
+    _sapp.win32.hwnd = CreateWindowExW(
+        win_ex_style,               /* dwExStyle */
+        L"SOKOLAPP",                /* lpClassName */
+        _sapp.window_title_wide,    /* lpWindowName */
+        win_style,                  /* dwStyle */
+        CW_USEDEFAULT,              /* X */
+        CW_USEDEFAULT,              /* Y */
+        win_width,                  /* nWidth */
+        win_height,                 /* nHeight */
+        NULL,                       /* hWndParent */
+        NULL,                       /* hMenu */
+        GetModuleHandle(NULL),      /* hInstance */
+        NULL);                      /* lParam */
+    ShowWindow(_sapp.win32.hwnd, SW_SHOW);
+    _sapp.win32.in_create_window = false;
+    _sapp.win32.dc = GetDC(_sapp.win32.hwnd);
+    SOKOL_ASSERT(_sapp.win32.dc);
+    _sapp_win32_update_dimensions();
+
+    DragAcceptFiles(_sapp.win32.hwnd, 1);
+}
+
+_SOKOL_PRIVATE void _sapp_win32_destroy_window(void) {
+    DestroyWindow(_sapp.win32.hwnd); _sapp.win32.hwnd = 0;
+    UnregisterClassW(L"SOKOLAPP", GetModuleHandleW(NULL));
+}
+
+_SOKOL_PRIVATE void _sapp_win32_destroy_icons(void) {
+    if (_sapp.win32.big_icon) {
+        DestroyIcon(_sapp.win32.big_icon);
+        _sapp.win32.big_icon = 0;
+    }
+    if (_sapp.win32.small_icon) {
+        DestroyIcon(_sapp.win32.small_icon);
+        _sapp.win32.small_icon = 0;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_init_console(void) {
+    if (_sapp.desc.win32_console_create || _sapp.desc.win32_console_attach) {
+        BOOL con_valid = FALSE;
+        if (_sapp.desc.win32_console_create) {
+            con_valid = AllocConsole();
+        }
+        else if (_sapp.desc.win32_console_attach) {
+            con_valid = AttachConsole(ATTACH_PARENT_PROCESS);
+        }
+        if (con_valid) {
+            FILE* res_fp = 0;
+            errno_t err;
+            err = freopens(&res_fp, "CON", "w", stdout);
+            err = freopens(&res_fp, "CON", "w", stderr);
+            (void)err;
+        }
+    }
+    if (_sapp.desc.win32_console_utf8) {
+        _sapp.win32.orig_codepage = GetConsoleOutputCP();
+        SetConsoleOutputCP(CP_UTF8);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_restore_console(void) {
+    if (_sapp.desc.win32_console_utf8) {
+        SetConsoleOutputCP(_sapp.win32.orig_codepage);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_init_dpi(void) {
+
+    typedef BOOL(WINAPI * SETPROCESSDPIAWARE_T)(void);
+    typedef HRESULT(WINAPI * SETPROCESSDPIAWARENESS_T)(PROCESS_DPI_AWARENESS);
+    typedef HRESULT(WINAPI * GETDPIFORMONITOR_T)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*);
+
+    SETPROCESSDPIAWARE_T fn_setprocessdpiaware = 0;
+    SETPROCESSDPIAWARENESS_T fn_setprocessdpiawareness = 0;
+    GETDPIFORMONITOR_T fn_getdpiformonitor = 0;
+    HINSTANCE user32 = LoadLibraryA("user32.dll");
+    if (user32) {
+        fn_setprocessdpiaware = (SETPROCESSDPIAWARE_T)(void*) GetProcAddress(user32, "SetProcessDPIAware");
+    }
+    HINSTANCE shcore = LoadLibraryA("shcore.dll");
+    if (shcore) {
+        fn_setprocessdpiawareness = (SETPROCESSDPIAWARENESS_T)(void*) GetProcAddress(shcore, "SetProcessDpiAwareness");
+        fn_getdpiformonitor = (GETDPIFORMONITOR_T)(void*) GetProcAddress(shcore, "GetDpiForMonitor");
+    }
+    if (fn_setprocessdpiawareness) {
+        /* if the app didn't request HighDPI rendering, let Windows do the upscaling */
+        PROCESS_DPI_AWARENESS process_dpi_awareness = PROCESS_SYSTEM_DPI_AWARE;
+        _sapp.win32.dpi.aware = true;
+        if (!_sapp.desc.high_dpi) {
+            process_dpi_awareness = PROCESS_DPI_UNAWARE;
+            _sapp.win32.dpi.aware = false;
+        }
+        fn_setprocessdpiawareness(process_dpi_awareness);
+    }
+    else if (fn_setprocessdpiaware) {
+        fn_setprocessdpiaware();
+        _sapp.win32.dpi.aware = true;
+    }
+    /* get dpi scale factor for main monitor */
+    if (fn_getdpiformonitor && _sapp.win32.dpi.aware) {
+        POINT pt = { 1, 1 };
+        HMONITOR hm = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
+        UINT dpix, dpiy;
+        HRESULT hr = fn_getdpiformonitor(hm, MDT_EFFECTIVE_DPI, &dpix, &dpiy);
+        _SOKOL_UNUSED(hr);
+        SOKOL_ASSERT(SUCCEEDED(hr));
+        /* clamp window scale to an integer factor */
+        _sapp.win32.dpi.window_scale = (float)dpix / 96.0f;
+    }
+    else {
+        _sapp.win32.dpi.window_scale = 1.0f;
+    }
+    if (_sapp.desc.high_dpi) {
+        _sapp.win32.dpi.content_scale = _sapp.win32.dpi.window_scale;
+        _sapp.win32.dpi.mouse_scale = 1.0f;
+    }
+    else {
+        _sapp.win32.dpi.content_scale = 1.0f;
+        _sapp.win32.dpi.mouse_scale = 1.0f / _sapp.win32.dpi.window_scale;
+    }
+    _sapp.dpi_scale = _sapp.win32.dpi.content_scale;
+    if (user32) {
+        FreeLibrary(user32);
+    }
+    if (shcore) {
+        FreeLibrary(shcore);
+    }
+}
+
+_SOKOL_PRIVATE bool _sapp_win32_set_clipboard_string(const char* str) {
+    SOKOL_ASSERT(str);
+    SOKOL_ASSERT(_sapp.win32.hwnd);
+    SOKOL_ASSERT(_sapp.clipboard.enabled && (_sapp.clipboard.buf_size > 0));
+
+    wchar_t* wchar_buf = 0;
+    const SIZE_T wchar_buf_size = (SIZE_T)_sapp.clipboard.buf_size * sizeof(wchar_t);
+    HANDLE object = GlobalAlloc(GMEM_MOVEABLE, wchar_buf_size);
+    if (!object) {
+        goto error;
+    }
+    wchar_buf = (wchar_t*) GlobalLock(object);
+    if (!wchar_buf) {
+        goto error;
+    }
+    if (!_sapp_win32_uwp_utf8_to_wide(str, wchar_buf, (int)wchar_buf_size)) {
+        goto error;
+    }
+    GlobalUnlock(wchar_buf);
+    wchar_buf = 0;
+    if (!OpenClipboard(_sapp.win32.hwnd)) {
+        goto error;
+    }
+    EmptyClipboard();
+    SetClipboardData(CF_UNICODETEXT, object);
+    CloseClipboard();
+    return true;
+
+error:
+    if (wchar_buf) {
+        GlobalUnlock(object);
+    }
+    if (object) {
+        GlobalFree(object);
+    }
+    return false;
+}
+
+_SOKOL_PRIVATE const char* _sapp_win32_get_clipboard_string(void) {
+    SOKOL_ASSERT(_sapp.clipboard.enabled && _sapp.clipboard.buffer);
+    SOKOL_ASSERT(_sapp.win32.hwnd);
+    if (!OpenClipboard(_sapp.win32.hwnd)) {
+        /* silently ignore any errors and just return the current
+           content of the local clipboard buffer
+        */
+        return _sapp.clipboard.buffer;
+    }
+    HANDLE object = GetClipboardData(CF_UNICODETEXT);
+    if (!object) {
+        CloseClipboard();
+        return _sapp.clipboard.buffer;
+    }
+    const wchar_t* wchar_buf = (const wchar_t*) GlobalLock(object);
+    if (!wchar_buf) {
+        CloseClipboard();
+        return _sapp.clipboard.buffer;
+    }
+    if (!_sapp_win32_wide_to_utf8(wchar_buf, _sapp.clipboard.buffer, _sapp.clipboard.buf_size)) {
+        SOKOL_LOG("sokol_app.h: clipboard string didn't fit into clipboard buffer\n");
+    }
+    GlobalUnlock(object);
+    CloseClipboard();
+    return _sapp.clipboard.buffer;
+}
+
+_SOKOL_PRIVATE void _sapp_win32_update_window_title(void) {
+    _sapp_win32_uwp_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide));
+    SetWindowTextW(_sapp.win32.hwnd, _sapp.window_title_wide);
+}
+
+_SOKOL_PRIVATE HICON _sapp_win32_create_icon_from_image(const sapp_image_desc* desc) {
+    BITMAPV5HEADER bi;
+    memset(&bi, 0, sizeof(bi));
+    bi.bV5Size = sizeof(bi);
+    bi.bV5Width = desc->width;
+    bi.bV5Height = -desc->height;   // NOTE the '-' here to indicate that origin is top-left
+    bi.bV5Planes = 1;
+    bi.bV5BitCount = 32;
+    bi.bV5Compression = BI_BITFIELDS;
+    bi.bV5RedMask = 0x00FF0000;
+    bi.bV5GreenMask = 0x0000FF00;
+    bi.bV5BlueMask = 0x000000FF;
+    bi.bV5AlphaMask = 0xFF000000;
+
+    uint8_t* target = 0;
+    const uint8_t* source = (const uint8_t*)desc->pixels.ptr;
+
+    HDC dc = GetDC(NULL);
+    HBITMAP color = CreateDIBSection(dc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, (void**)&target, NULL, (DWORD)0);
+    ReleaseDC(NULL, dc);
+    if (0 == color) {
+        return NULL;
+    }
+    SOKOL_ASSERT(target);
+
+    HBITMAP mask = CreateBitmap(desc->width, desc->height, 1, 1, NULL);
+    if (0 == mask) {
+        DeleteObject(color);
+        return NULL;
+    }
+
+    for (int i = 0; i < (desc->width*desc->height); i++) {
+        target[0] = source[2];
+        target[1] = source[1];
+        target[2] = source[0];
+        target[3] = source[3];
+        target += 4;
+        source += 4;
+    }
+
+    ICONINFO icon_info;
+    memset(&icon_info, 0, sizeof(icon_info));
+    icon_info.fIcon = true;
+    icon_info.xHotspot = 0;
+    icon_info.yHotspot = 0;
+    icon_info.hbmMask = mask;
+    icon_info.hbmColor = color;
+    HICON icon_handle = CreateIconIndirect(&icon_info);
+    DeleteObject(color);
+    DeleteObject(mask);
+
+    return icon_handle;
+}
+
+_SOKOL_PRIVATE void _sapp_win32_set_icon(const sapp_icon_desc* icon_desc, int num_images) {
+    SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES));
+
+    int big_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON));
+    int sml_img_index = _sapp_image_bestmatch(icon_desc->images, num_images, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON));
+    HICON big_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[big_img_index]);
+    HICON sml_icon = _sapp_win32_create_icon_from_image(&icon_desc->images[sml_img_index]);
+
+    // if icon creation or lookup has failed for some reason, leave the currently set icon untouched
+    if (0 != big_icon) {
+        SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_BIG, (LPARAM) big_icon);
+        if (0 != _sapp.win32.big_icon) {
+            DestroyIcon(_sapp.win32.big_icon);
+        }
+        _sapp.win32.big_icon = big_icon;
+    }
+    if (0 != sml_icon) {
+        SendMessage(_sapp.win32.hwnd, WM_SETICON, ICON_SMALL, (LPARAM) sml_icon);
+        if (0 != _sapp.win32.small_icon) {
+            DestroyIcon(_sapp.win32.small_icon);
+        }
+        _sapp.win32.small_icon = sml_icon;
+    }
+}
+
+/* don't laugh, but this seems to be the easiest and most robust
+   way to check if we're running on Win10
+
+   From: https://github.com/videolan/vlc/blob/232fb13b0d6110c4d1b683cde24cf9a7f2c5c2ea/modules/video_output/win32/d3d11_swapchain.c#L263
+*/
+_SOKOL_PRIVATE bool _sapp_win32_is_win10_or_greater(void) {
+    HMODULE h = GetModuleHandleW(L"kernel32.dll");
+    if (NULL != h) {
+        return (NULL != GetProcAddress(h, "GetSystemCpuSetInformation"));
+    }
+    else {
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_win32_run(const sapp_desc* desc) {
+    _sapp_init_state(desc);
+    _sapp_win32_init_console();
+    _sapp.win32.is_win10_or_greater = _sapp_win32_is_win10_or_greater();
+    _sapp_win32_uwp_init_keytable();
+    _sapp_win32_uwp_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide));
+    _sapp_win32_init_dpi();
+    _sapp_win32_create_window();
+    sapp_set_icon(&desc->icon);
+    #if defined(SOKOL_D3D11)
+        _sapp_d3d11_create_device_and_swapchain();
+        _sapp_d3d11_create_default_render_target();
+    #endif
+    #if defined(SOKOL_GLCORE33)
+        _sapp_wgl_init();
+        _sapp_wgl_load_extensions();
+        _sapp_wgl_create_context();
+    #endif
+    _sapp.valid = true;
+
+    bool done = false;
+    while (!(done || _sapp.quit_ordered)) {
+        MSG msg;
+        while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) {
+            if (WM_QUIT == msg.message) {
+                done = true;
+                continue;
+            }
+            else {
+                TranslateMessage(&msg);
+                DispatchMessage(&msg);
+            }
+        }
+        _sapp_frame();
+        #if defined(SOKOL_D3D11)
+            _sapp_d3d11_present();
+            if (IsIconic(_sapp.win32.hwnd)) {
+                Sleep((DWORD)(16 * _sapp.swap_interval));
+            }
+        #endif
+        #if defined(SOKOL_GLCORE33)
+            _sapp_wgl_swap_buffers();
+        #endif
+        /* check for window resized, this cannot happen in WM_SIZE as it explodes memory usage */
+        if (_sapp_win32_update_dimensions()) {
+            #if defined(SOKOL_D3D11)
+            _sapp_d3d11_resize_default_render_target();
+            #endif
+            _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESIZED);
+        }
+        if (_sapp.quit_requested) {
+            PostMessage(_sapp.win32.hwnd, WM_CLOSE, 0, 0);
+        }
+    }
+    _sapp_call_cleanup();
+
+    #if defined(SOKOL_D3D11)
+        _sapp_d3d11_destroy_default_render_target();
+        _sapp_d3d11_destroy_device_and_swapchain();
+    #else
+        _sapp_wgl_destroy_context();
+        _sapp_wgl_shutdown();
+    #endif
+    _sapp_win32_destroy_window();
+    _sapp_win32_destroy_icons();
+    _sapp_win32_restore_console();
+    _sapp_discard_state();
+}
+
+_SOKOL_PRIVATE char** _sapp_win32_command_line_to_utf8_argv(LPWSTR w_command_line, int* o_argc) {
+    int argc = 0;
+    char** argv = 0;
+    char* args;
+
+    LPWSTR* w_argv = CommandLineToArgvW(w_command_line, &argc);
+    if (w_argv == NULL) {
+        _sapp_fail("Win32: failed to parse command line");
+    } else {
+        size_t size = wcslen(w_command_line) * 4;
+        argv = (char**) SOKOL_CALLOC(1, ((size_t)argc + 1) * sizeof(char*) + size);
+        SOKOL_ASSERT(argv);
+        args = (char*) &argv[argc + 1];
+        int n;
+        for (int i = 0; i < argc; ++i) {
+            n = WideCharToMultiByte(CP_UTF8, 0, w_argv[i], -1, args, (int)size, NULL, NULL);
+            if (n == 0) {
+                _sapp_fail("Win32: failed to convert all arguments to utf8");
+                break;
+            }
+            argv[i] = args;
+            size -= (size_t)n;
+            args += n;
+        }
+        LocalFree(w_argv);
+    }
+    *o_argc = argc;
+    return argv;
+}
+
+#if !defined(SOKOL_NO_ENTRY)
+#if defined(SOKOL_WIN32_FORCE_MAIN)
+int main(int argc, char* argv[]) {
+    sapp_desc desc = sokol_main(argc, argv);
+    _sapp_win32_run(&desc);
+    return 0;
+}
+#else
+int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) {
+    _SOKOL_UNUSED(hInstance);
+    _SOKOL_UNUSED(hPrevInstance);
+    _SOKOL_UNUSED(lpCmdLine);
+    _SOKOL_UNUSED(nCmdShow);
+    int argc_utf8 = 0;
+    char** argv_utf8 = _sapp_win32_command_line_to_utf8_argv(GetCommandLineW(), &argc_utf8);
+    sapp_desc desc = sokol_main(argc_utf8, argv_utf8);
+    _sapp_win32_run(&desc);
+    SOKOL_FREE(argv_utf8);
+    return 0;
+}
+#endif /* SOKOL_WIN32_FORCE_MAIN */
+#endif /* SOKOL_NO_ENTRY */
+
+#ifdef _MSC_VER
+    #pragma warning(pop)
+#endif
+
+#endif /* _SAPP_WIN32 */
+
+/*== UWP ================================================================*/
+#if defined(_SAPP_UWP)
+
+// Helper functions
+_SOKOL_PRIVATE void _sapp_uwp_configure_dpi(float monitor_dpi) {
+    _sapp.uwp.dpi.window_scale = monitor_dpi / 96.0f;
+    if (_sapp.desc.high_dpi) {
+        _sapp.uwp.dpi.content_scale = _sapp.uwp.dpi.window_scale;
+        _sapp.uwp.dpi.mouse_scale = 1.0f * _sapp.uwp.dpi.window_scale;
+    }
+    else {
+        _sapp.uwp.dpi.content_scale = 1.0f;
+        _sapp.uwp.dpi.mouse_scale = 1.0f;
+    }
+    _sapp.dpi_scale = _sapp.uwp.dpi.content_scale;
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_show_mouse(bool visible) {
+    using namespace winrt::Windows::UI::Core;
+
+    /* NOTE: this function is only called when the mouse visibility actually changes */
+    CoreWindow::GetForCurrentThread().PointerCursor(visible ?
+        CoreCursor(CoreCursorType::Arrow, 0) :
+        CoreCursor(nullptr));
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_uwp_mods(winrt::Windows::UI::Core::CoreWindow const& sender_window) {
+    using namespace winrt::Windows::System;
+    using namespace winrt::Windows::UI::Core;
+
+    uint32_t mods = 0;
+    if ((sender_window.GetKeyState(VirtualKey::Shift) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) {
+        mods |= SAPP_MODIFIER_SHIFT;
+    }
+    if ((sender_window.GetKeyState(VirtualKey::Control) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) {
+        mods |= SAPP_MODIFIER_CTRL;
+    }
+    if ((sender_window.GetKeyState(VirtualKey::Menu) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) {
+        mods |= SAPP_MODIFIER_ALT;
+    }
+    if (((sender_window.GetKeyState(VirtualKey::LeftWindows) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down) ||
+        ((sender_window.GetKeyState(VirtualKey::RightWindows) & CoreVirtualKeyStates::Down) == CoreVirtualKeyStates::Down))
+    {
+        mods |= SAPP_MODIFIER_SUPER;
+    }
+    if (0 != (_sapp.uwp.mouse_buttons & (1<<SAPP_MOUSEBUTTON_LEFT))) {
+        mods |= SAPP_MODIFIER_LMB;
+    }
+    if (0 != (_sapp.uwp.mouse_buttons & (1<<SAPP_MOUSEBUTTON_MIDDLE))) {
+        mods |= SAPP_MODIFIER_MMB;
+    }
+    if (0 != (_sapp.uwp.mouse_buttons & (1<<SAPP_MOUSEBUTTON_RIGHT))) {
+        mods |= SAPP_MODIFIER_RMB;
+    }
+    return mods;
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_mouse_event(sapp_event_type type, sapp_mousebutton btn, winrt::Windows::UI::Core::CoreWindow const& sender_window) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.modifiers = _sapp_uwp_mods(sender_window);
+        _sapp.event.mouse_button = btn;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_scroll_event(float delta, bool horizontal, winrt::Windows::UI::Core::CoreWindow const& sender_window) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL);
+        _sapp.event.modifiers = _sapp_uwp_mods(sender_window);
+        _sapp.event.scroll_x = horizontal ? (-delta / 30.0f) : 0.0f;
+        _sapp.event.scroll_y = horizontal ? 0.0f : (delta / 30.0f);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_extract_mouse_button_events(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+
+    // we need to figure out ourselves what mouse buttons have been pressed and released,
+    // because UWP doesn't properly send down/up mouse button events when multiple buttons
+    // are pressed down, so we also need to check the mouse button state in other mouse events
+    // to track what buttons have been pressed down and released
+    //
+    auto properties = args.CurrentPoint().Properties();
+    const uint8_t lmb_bit = (1 << SAPP_MOUSEBUTTON_LEFT);
+    const uint8_t rmb_bit = (1 << SAPP_MOUSEBUTTON_RIGHT);
+    const uint8_t mmb_bit = (1 << SAPP_MOUSEBUTTON_MIDDLE);
+    uint8_t new_btns = 0;
+    if (properties.IsLeftButtonPressed()) {
+        new_btns |= lmb_bit;
+    }
+    if (properties.IsRightButtonPressed()) {
+        new_btns |= rmb_bit;
+    }
+    if (properties.IsMiddleButtonPressed()) {
+        new_btns |= mmb_bit;
+    }
+    const uint8_t old_btns = _sapp.uwp.mouse_buttons;
+    const uint8_t chg_btns = new_btns ^ old_btns;
+
+    _sapp.uwp.mouse_buttons = new_btns;
+
+    sapp_event_type type = SAPP_EVENTTYPE_INVALID;
+    sapp_mousebutton btn = SAPP_MOUSEBUTTON_INVALID;
+    if (chg_btns & lmb_bit) {
+        btn = SAPP_MOUSEBUTTON_LEFT;
+        type = (new_btns & lmb_bit) ? SAPP_EVENTTYPE_MOUSE_DOWN : SAPP_EVENTTYPE_MOUSE_UP;
+    }
+    if (chg_btns & rmb_bit) {
+        btn = SAPP_MOUSEBUTTON_RIGHT;
+        type = (new_btns & rmb_bit) ? SAPP_EVENTTYPE_MOUSE_DOWN : SAPP_EVENTTYPE_MOUSE_UP;
+    }
+    if (chg_btns & mmb_bit) {
+        btn = SAPP_MOUSEBUTTON_MIDDLE;
+        type = (new_btns & mmb_bit) ? SAPP_EVENTTYPE_MOUSE_DOWN : SAPP_EVENTTYPE_MOUSE_UP;
+    }
+    if (type != SAPP_EVENTTYPE_INVALID) {
+        _sapp_uwp_mouse_event(type, btn, sender);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_key_event(sapp_event_type type, winrt::Windows::UI::Core::CoreWindow const& sender_window, winrt::Windows::UI::Core::KeyEventArgs const& args) {
+    auto key_status = args.KeyStatus();
+    uint32_t ext_scan_code = key_status.ScanCode | (key_status.IsExtendedKey ? 0x100 : 0);
+    if (_sapp_events_enabled() && (ext_scan_code < SAPP_MAX_KEYCODES)) {
+        _sapp_init_event(type);
+        _sapp.event.modifiers = _sapp_uwp_mods(sender_window);
+        _sapp.event.key_code = _sapp.keycodes[ext_scan_code];
+        _sapp.event.key_repeat = type == SAPP_EVENTTYPE_KEY_UP ? false : key_status.WasKeyDown;
+        _sapp_call_event(&_sapp.event);
+        /* check if a CLIPBOARD_PASTED event must be sent too */
+        if (_sapp.clipboard.enabled &&
+            (type == SAPP_EVENTTYPE_KEY_DOWN) &&
+            (_sapp.event.modifiers == SAPP_MODIFIER_CTRL) &&
+            (_sapp.event.key_code == SAPP_KEYCODE_V))
+        {
+            _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_char_event(uint32_t c, bool repeat, winrt::Windows::UI::Core::CoreWindow const& sender_window) {
+    if (_sapp_events_enabled() && (c >= 32)) {
+        _sapp_init_event(SAPP_EVENTTYPE_CHAR);
+        _sapp.event.modifiers = _sapp_uwp_mods(sender_window);
+        _sapp.event.char_code = c;
+        _sapp.event.key_repeat = repeat;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_uwp_toggle_fullscreen(void) {
+    auto appView = winrt::Windows::UI::ViewManagement::ApplicationView::GetForCurrentView();
+    _sapp.fullscreen = appView.IsFullScreenMode();
+    if (!_sapp.fullscreen) {
+        appView.TryEnterFullScreenMode();
+    }
+    else {
+        appView.ExitFullScreenMode();
+    }
+    _sapp.fullscreen = appView.IsFullScreenMode();
+}
+
+namespace {/* Empty namespace to ensure internal linkage (same as _SOKOL_PRIVATE) */
+
+// Controls all the DirectX device resources.
+class DeviceResources {
+public:
+    // Provides an interface for an application that owns DeviceResources to be notified of the device being lost or created.
+    interface IDeviceNotify {
+        virtual void OnDeviceLost() = 0;
+        virtual void OnDeviceRestored() = 0;
+    };
+
+    DeviceResources();
+    ~DeviceResources();
+    void SetWindow(winrt::Windows::UI::Core::CoreWindow const& window);
+    void SetLogicalSize(winrt::Windows::Foundation::Size logicalSize);
+    void SetCurrentOrientation(winrt::Windows::Graphics::Display::DisplayOrientations currentOrientation);
+    void SetDpi(float dpi);
+    void ValidateDevice();
+    void HandleDeviceLost();
+    void RegisterDeviceNotify(IDeviceNotify* deviceNotify);
+    void Trim();
+    void Present();
+
+private:
+
+    // Swapchain Rotation Matrices (Z-rotation)
+    static inline const DirectX::XMFLOAT4X4 DeviceResources::m_rotation0 = {
+        1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, 1.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    };
+    static inline const DirectX::XMFLOAT4X4 DeviceResources::m_rotation90 = {
+        0.0f, 1.0f, 0.0f, 0.0f,
+        -1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    };
+    static inline const DirectX::XMFLOAT4X4 DeviceResources::m_rotation180 = {
+        -1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, -1.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    };
+    static inline const DirectX::XMFLOAT4X4 DeviceResources::m_rotation270 = {
+        0.0f, -1.0f, 0.0f, 0.0f,
+        1.0f, 0.0f, 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f,
+        0.0f, 0.0f, 0.0f, 1.0f
+    };
+
+    void CreateDeviceResources();
+    void CreateWindowSizeDependentResources();
+    void UpdateRenderTargetSize();
+    DXGI_MODE_ROTATION ComputeDisplayRotation();
+    bool SdkLayersAvailable();
+
+    // Direct3D objects.
+    winrt::com_ptr<ID3D11Device3> m_d3dDevice;
+    winrt::com_ptr<ID3D11DeviceContext3> m_d3dContext;
+    winrt::com_ptr<IDXGISwapChain3> m_swapChain;
+
+    // Direct3D rendering objects. Required for 3D.
+    winrt::com_ptr<ID3D11Texture2D1> m_d3dRenderTarget;
+    winrt::com_ptr<ID3D11RenderTargetView1> m_d3dRenderTargetView;
+    winrt::com_ptr<ID3D11Texture2D1> m_d3dMSAARenderTarget;
+    winrt::com_ptr<ID3D11RenderTargetView1> m_d3dMSAARenderTargetView;
+    winrt::com_ptr<ID3D11Texture2D1> m_d3dDepthStencil;
+    winrt::com_ptr<ID3D11DepthStencilView> m_d3dDepthStencilView;
+    D3D11_VIEWPORT m_screenViewport = { };
+
+    // Cached reference to the Window.
+    winrt::agile_ref< winrt::Windows::UI::Core::CoreWindow> m_window;
+
+    // Cached device properties.
+    D3D_FEATURE_LEVEL m_d3dFeatureLevel = D3D_FEATURE_LEVEL_9_1;
+    winrt::Windows::Foundation::Size m_d3dRenderTargetSize = { };
+    winrt::Windows::Foundation::Size m_outputSize = { };
+    winrt::Windows::Foundation::Size m_logicalSize = { };
+    winrt::Windows::Graphics::Display::DisplayOrientations m_nativeOrientation = winrt::Windows::Graphics::Display::DisplayOrientations::None;
+    winrt::Windows::Graphics::Display::DisplayOrientations m_currentOrientation = winrt::Windows::Graphics::Display::DisplayOrientations::None;
+    float m_dpi = -1.0f;
+
+    // Transforms used for display orientation.
+    DirectX::XMFLOAT4X4 m_orientationTransform3D;
+
+    // The IDeviceNotify can be held directly as it owns the DeviceResources.
+    IDeviceNotify* m_deviceNotify = nullptr;
+};
+
+// Main entry point for our app. Connects the app with the Windows shell and handles application lifecycle events.
+struct App : winrt::implements<App, winrt::Windows::ApplicationModel::Core::IFrameworkViewSource, winrt::Windows::ApplicationModel::Core::IFrameworkView> {
+public:
+    // IFrameworkViewSource Methods
+    winrt::Windows::ApplicationModel::Core::IFrameworkView CreateView() { return *this; }
+
+    // IFrameworkView Methods.
+    virtual void Initialize(winrt::Windows::ApplicationModel::Core::CoreApplicationView const& applicationView);
+    virtual void SetWindow(winrt::Windows::UI::Core::CoreWindow const& window);
+    virtual void Load(winrt::hstring const& entryPoint);
+    virtual void Run();
+    virtual void Uninitialize();
+
+protected:
+    // Application lifecycle event handlers
+    void OnActivated(winrt::Windows::ApplicationModel::Core::CoreApplicationView const& applicationView, winrt::Windows::ApplicationModel::Activation::IActivatedEventArgs const& args);
+    void OnSuspending(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::ApplicationModel::SuspendingEventArgs const& args);
+    void OnResuming(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::Foundation::IInspectable const& args);
+
+    // Window event handlers
+    void OnWindowSizeChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::WindowSizeChangedEventArgs const& args);
+    void OnVisibilityChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::VisibilityChangedEventArgs const& args);
+
+    // Navigation event handlers
+    void OnBackRequested(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Core::BackRequestedEventArgs const& args);
+
+    // Input event handlers
+    void OnKeyDown(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::KeyEventArgs const& args);
+    void OnKeyUp(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::KeyEventArgs const& args);
+    void OnCharacterReceived(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::CharacterReceivedEventArgs const& args);
+
+    // Pointer event handlers
+    void OnPointerEntered(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+    void OnPointerExited(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+    void OnPointerPressed(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+    void OnPointerReleased(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+    void OnPointerMoved(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+    void OnPointerWheelChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args);
+
+    // DisplayInformation event handlers.
+    void OnDpiChanged(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args);
+    void OnOrientationChanged(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args);
+    void OnDisplayContentsInvalidated(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args);
+
+private:
+    std::unique_ptr<DeviceResources> m_deviceResources;
+    bool m_windowVisible = true;
+};
+
+DeviceResources::DeviceResources() {
+    CreateDeviceResources();
+}
+
+DeviceResources::~DeviceResources() {
+    // Cleanup Sokol Context
+    _sapp.d3d11.device = nullptr;
+    _sapp.d3d11.device_context = nullptr;
+}
+
+void DeviceResources::CreateDeviceResources() {
+    // This flag adds support for surfaces with a different color channel ordering
+    // than the API default. It is required for compatibility with Direct2D.
+    UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
+
+    #if defined(_DEBUG)
+    if (SdkLayersAvailable()) {
+        // If the project is in a debug build, enable debugging via SDK Layers with this flag.
+        creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
+    }
+    #endif
+
+    // This array defines the set of DirectX hardware feature levels this app will support.
+    // Note the ordering should be preserved.
+    // Don't forget to declare your application's minimum required feature level in its
+    // description.  All applications are assumed to support 9.1 unless otherwise stated.
+    D3D_FEATURE_LEVEL featureLevels[] = {
+        D3D_FEATURE_LEVEL_12_1,
+        D3D_FEATURE_LEVEL_12_0,
+        D3D_FEATURE_LEVEL_11_1,
+        D3D_FEATURE_LEVEL_11_0,
+        D3D_FEATURE_LEVEL_10_1,
+        D3D_FEATURE_LEVEL_10_0,
+        D3D_FEATURE_LEVEL_9_3,
+        D3D_FEATURE_LEVEL_9_2,
+        D3D_FEATURE_LEVEL_9_1
+    };
+
+    // Create the Direct3D 11 API device object and a corresponding context.
+    winrt::com_ptr<ID3D11Device> device;
+    winrt::com_ptr<ID3D11DeviceContext> context;
+
+    HRESULT hr = D3D11CreateDevice(
+        nullptr,                    // Specify nullptr to use the default adapter.
+        D3D_DRIVER_TYPE_HARDWARE,   // Create a device using the hardware graphics driver.
+        0,                          // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
+        creationFlags,              // Set debug and Direct2D compatibility flags.
+        featureLevels,              // List of feature levels this app can support.
+        ARRAYSIZE(featureLevels),   // Size of the list above.
+        D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for Microsoft Store apps.
+        device.put(),               // Returns the Direct3D device created.
+        &m_d3dFeatureLevel,         // Returns feature level of device created.
+        context.put()               // Returns the device immediate context.
+    );
+
+    if (FAILED(hr)) {
+        // If the initialization fails, fall back to the WARP device.
+        // For more information on WARP, see:
+        // https://go.microsoft.com/fwlink/?LinkId=286690
+        winrt::check_hresult(
+            D3D11CreateDevice(
+                nullptr,
+                D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
+                0,
+                creationFlags,
+                featureLevels,
+                ARRAYSIZE(featureLevels),
+                D3D11_SDK_VERSION,
+                device.put(),
+                &m_d3dFeatureLevel,
+                context.put()
+            )
+        );
+    }
+
+    // Store pointers to the Direct3D 11.3 API device and immediate context.
+    m_d3dDevice = device.as<ID3D11Device3>();
+    m_d3dContext = context.as<ID3D11DeviceContext3>();
+
+    // Setup Sokol Context
+    _sapp.d3d11.device = m_d3dDevice.get();
+    _sapp.d3d11.device_context = m_d3dContext.get();
+}
+
+void DeviceResources::CreateWindowSizeDependentResources() {
+    // Cleanup Sokol Context (these are non-owning raw pointers)
+    _sapp.d3d11.rt = nullptr;
+    _sapp.d3d11.rtv = nullptr;
+    _sapp.d3d11.msaa_rt = nullptr;
+    _sapp.d3d11.msaa_rtv = nullptr;
+    _sapp.d3d11.ds = nullptr;
+    _sapp.d3d11.dsv = nullptr;
+
+    // Clear the previous window size specific context.
+    ID3D11RenderTargetView* nullViews[] = { nullptr };
+    m_d3dContext->OMSetRenderTargets(ARRAYSIZE(nullViews), nullViews, nullptr);
+    // these are smart pointers, setting to nullptr will delete the objects
+    m_d3dRenderTarget = nullptr;
+    m_d3dRenderTargetView = nullptr;
+    m_d3dMSAARenderTarget = nullptr;
+    m_d3dMSAARenderTargetView = nullptr;
+    m_d3dDepthStencilView = nullptr;
+    m_d3dDepthStencil = nullptr;
+    m_d3dContext->Flush1(D3D11_CONTEXT_TYPE_ALL, nullptr);
+
+    UpdateRenderTargetSize();
+
+    // The width and height of the swap chain must be based on the window's
+    // natively-oriented width and height. If the window is not in the native
+    // orientation, the dimensions must be reversed.
+    DXGI_MODE_ROTATION displayRotation = ComputeDisplayRotation();
+
+    bool swapDimensions = displayRotation == DXGI_MODE_ROTATION_ROTATE90 || displayRotation == DXGI_MODE_ROTATION_ROTATE270;
+    m_d3dRenderTargetSize.Width = swapDimensions ? m_outputSize.Height : m_outputSize.Width;
+    m_d3dRenderTargetSize.Height = swapDimensions ? m_outputSize.Width : m_outputSize.Height;
+
+    if (m_swapChain != nullptr) {
+        // If the swap chain already exists, resize it.
+        HRESULT hr = m_swapChain->ResizeBuffers(
+            2, // Double-buffered swap chain.
+            lround(m_d3dRenderTargetSize.Width),
+            lround(m_d3dRenderTargetSize.Height),
+            DXGI_FORMAT_B8G8R8A8_UNORM,
+            0
+        );
+
+        if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) {
+            // If the device was removed for any reason, a new device and swap chain will need to be created.
+            HandleDeviceLost();
+
+            // Everything is set up now. Do not continue execution of this method. HandleDeviceLost will reenter this method
+            // and correctly set up the new device.
+            return;
+        }
+        else {
+            winrt::check_hresult(hr);
+        }
+    }
+    else {
+        // Otherwise, create a new one using the same adapter as the existing Direct3D device.
+        DXGI_SCALING scaling = (_sapp.uwp.dpi.content_scale == _sapp.uwp.dpi.window_scale) ? DXGI_SCALING_NONE : DXGI_SCALING_STRETCH;
+        DXGI_SWAP_CHAIN_DESC1 swapChainDesc = { 0 };
+
+        swapChainDesc.Width = lround(m_d3dRenderTargetSize.Width);      // Match the size of the window.
+        swapChainDesc.Height = lround(m_d3dRenderTargetSize.Height);
+        swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;              // This is the most common swap chain format.
+        swapChainDesc.Stereo = false;
+        swapChainDesc.SampleDesc.Count = 1;                             // Don't use multi-sampling.
+        swapChainDesc.SampleDesc.Quality = 0;
+        swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
+        swapChainDesc.BufferCount = 2;                                  // Use double-buffering to minimize latency.
+        swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;    // All Microsoft Store apps must use this SwapEffect.
+        swapChainDesc.Flags = 0;
+        swapChainDesc.Scaling = scaling;
+        swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
+
+        // This sequence obtains the DXGI factory that was used to create the Direct3D device above.
+        winrt::com_ptr<IDXGIDevice3> dxgiDevice = m_d3dDevice.as<IDXGIDevice3>();
+        winrt::com_ptr<IDXGIAdapter> dxgiAdapter;
+        winrt::check_hresult(dxgiDevice->GetAdapter(dxgiAdapter.put()));
+        winrt::com_ptr<IDXGIFactory4> dxgiFactory;
+        winrt::check_hresult(dxgiAdapter->GetParent(__uuidof(IDXGIFactory4), dxgiFactory.put_void()));
+        winrt::com_ptr<IDXGISwapChain1> swapChain;
+        winrt::check_hresult(dxgiFactory->CreateSwapChainForCoreWindow(m_d3dDevice.get(), m_window.get().as<::IUnknown>().get(), &swapChainDesc, nullptr, swapChain.put()));
+        m_swapChain = swapChain.as<IDXGISwapChain3>();
+
+        // Ensure that DXGI does not queue more than one frame at a time. This both reduces latency and
+        // ensures that the application will only render after each VSync, minimizing power consumption.
+        winrt::check_hresult(dxgiDevice->SetMaximumFrameLatency(1));
+
+        // Setup Sokol Context
+        winrt::check_hresult(swapChain->GetDesc(&_sapp.d3d11.swap_chain_desc));
+        _sapp.d3d11.swap_chain = m_swapChain.as<IDXGISwapChain3>().detach();
+    }
+
+    // Set the proper orientation for the swap chain, and generate 2D and
+    // 3D matrix transformations for rendering to the rotated swap chain.
+    // Note the rotation angle for the 2D and 3D transforms are different.
+    // This is due to the difference in coordinate spaces.  Additionally,
+    // the 3D matrix is specified explicitly to avoid rounding errors.
+    switch (displayRotation) {
+        case DXGI_MODE_ROTATION_IDENTITY:
+            m_orientationTransform3D = m_rotation0;
+            break;
+
+        case DXGI_MODE_ROTATION_ROTATE90:
+            m_orientationTransform3D = m_rotation270;
+            break;
+
+        case DXGI_MODE_ROTATION_ROTATE180:
+            m_orientationTransform3D = m_rotation180;
+            break;
+
+        case DXGI_MODE_ROTATION_ROTATE270:
+            m_orientationTransform3D = m_rotation90;
+            break;
+    }
+    winrt::check_hresult(m_swapChain->SetRotation(displayRotation));
+
+    // Create a render target view of the swap chain back buffer.
+    winrt::check_hresult(m_swapChain->GetBuffer(0, IID_PPV_ARGS(&m_d3dRenderTarget)));
+    winrt::check_hresult(m_d3dDevice->CreateRenderTargetView1(m_d3dRenderTarget.get(), nullptr, m_d3dRenderTargetView.put()));
+
+    // Create MSAA texture and view if needed
+    if (_sapp.sample_count > 1) {
+        CD3D11_TEXTURE2D_DESC1 msaaTexDesc(
+            DXGI_FORMAT_B8G8R8A8_UNORM,
+            lround(m_d3dRenderTargetSize.Width),
+            lround(m_d3dRenderTargetSize.Height),
+            1,  // arraySize
+            1,  // mipLevels
+            D3D11_BIND_RENDER_TARGET,
+            D3D11_USAGE_DEFAULT,
+            0,  // cpuAccessFlags
+            _sapp.sample_count,
+            _sapp.sample_count > 1 ? D3D11_STANDARD_MULTISAMPLE_PATTERN : 0
+        );
+        winrt::check_hresult(m_d3dDevice->CreateTexture2D1(&msaaTexDesc, nullptr, m_d3dMSAARenderTarget.put()));
+        winrt::check_hresult(m_d3dDevice->CreateRenderTargetView1(m_d3dMSAARenderTarget.get(), nullptr, m_d3dMSAARenderTargetView.put()));
+    }
+
+    // Create a depth stencil view for use with 3D rendering if needed.
+    CD3D11_TEXTURE2D_DESC1 depthStencilDesc(
+        DXGI_FORMAT_D24_UNORM_S8_UINT,
+        lround(m_d3dRenderTargetSize.Width),
+        lround(m_d3dRenderTargetSize.Height),
+        1, // This depth stencil view has only one texture.
+        1, // Use a single mipmap level.
+        D3D11_BIND_DEPTH_STENCIL,
+        D3D11_USAGE_DEFAULT,
+        0,  // cpuAccessFlag
+        _sapp.sample_count,
+        _sapp.sample_count > 1 ? D3D11_STANDARD_MULTISAMPLE_PATTERN : 0
+    );
+    winrt::check_hresult(m_d3dDevice->CreateTexture2D1(&depthStencilDesc, nullptr, m_d3dDepthStencil.put()));
+
+    CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
+    winrt::check_hresult(m_d3dDevice->CreateDepthStencilView(m_d3dDepthStencil.get(), nullptr, m_d3dDepthStencilView.put()));
+
+    // Set sokol window and framebuffer sizes
+    _sapp.window_width = (int) m_logicalSize.Width;
+    _sapp.window_height = (int) m_logicalSize.Height;
+    _sapp.framebuffer_width = lround(m_d3dRenderTargetSize.Width);
+    _sapp.framebuffer_height = lround(m_d3dRenderTargetSize.Height);
+
+    // Setup Sokol Context
+    _sapp.d3d11.rt = m_d3dRenderTarget.as<ID3D11Texture2D>().get();
+    _sapp.d3d11.rtv = m_d3dRenderTargetView.as<ID3D11RenderTargetView>().get();
+    _sapp.d3d11.ds = m_d3dDepthStencil.as<ID3D11Texture2D>().get();
+    _sapp.d3d11.dsv = m_d3dDepthStencilView.get();
+    if (_sapp.sample_count > 1) {
+        _sapp.d3d11.msaa_rt = m_d3dMSAARenderTarget.as<ID3D11Texture2D>().get();
+        _sapp.d3d11.msaa_rtv = m_d3dMSAARenderTargetView.as<ID3D11RenderTargetView>().get();
+    }
+
+    // Sokol app is now valid
+    _sapp.valid = true;
+}
+
+// Determine the dimensions of the render target and whether it will be scaled down.
+void DeviceResources::UpdateRenderTargetSize() {
+    // Calculate the necessary render target size in pixels.
+    m_outputSize.Width = m_logicalSize.Width * _sapp.uwp.dpi.content_scale;
+    m_outputSize.Height = m_logicalSize.Height * _sapp.uwp.dpi.content_scale;
+
+    // Prevent zero size DirectX content from being created.
+    m_outputSize.Width = std::max(m_outputSize.Width, 1.0f);
+    m_outputSize.Height = std::max(m_outputSize.Height, 1.0f);
+}
+
+// This method is called when the CoreWindow is created (or re-created).
+void DeviceResources::SetWindow(winrt::Windows::UI::Core::CoreWindow const& window) {
+    auto currentDisplayInformation = winrt::Windows::Graphics::Display::DisplayInformation::GetForCurrentView();
+    m_window = window;
+    m_logicalSize = winrt::Windows::Foundation::Size(window.Bounds().Width, window.Bounds().Height);
+    m_nativeOrientation = currentDisplayInformation.NativeOrientation();
+    m_currentOrientation = currentDisplayInformation.CurrentOrientation();
+    m_dpi = currentDisplayInformation.LogicalDpi();
+    _sapp_uwp_configure_dpi(m_dpi);
+    CreateWindowSizeDependentResources();
+}
+
+// This method is called in the event handler for the SizeChanged event.
+void DeviceResources::SetLogicalSize(winrt::Windows::Foundation::Size logicalSize) {
+    if (m_logicalSize != logicalSize) {
+        m_logicalSize = logicalSize;
+        CreateWindowSizeDependentResources();
+    }
+}
+
+// This method is called in the event handler for the DpiChanged event.
+void DeviceResources::SetDpi(float dpi) {
+    if (dpi != m_dpi) {
+        m_dpi = dpi;
+        _sapp_uwp_configure_dpi(m_dpi);
+        // When the display DPI changes, the logical size of the window (measured in Dips) also changes and needs to be updated.
+        auto window = m_window.get();
+        m_logicalSize = winrt::Windows::Foundation::Size(window.Bounds().Width, window.Bounds().Height);
+        CreateWindowSizeDependentResources();
+    }
+}
+
+// This method is called in the event handler for the OrientationChanged event.
+void DeviceResources::SetCurrentOrientation(winrt::Windows::Graphics::Display::DisplayOrientations currentOrientation) {
+    if (m_currentOrientation != currentOrientation) {
+        m_currentOrientation = currentOrientation;
+        CreateWindowSizeDependentResources();
+    }
+}
+
+// This method is called in the event handler for the DisplayContentsInvalidated event.
+void DeviceResources::ValidateDevice() {
+    // The D3D Device is no longer valid if the default adapter changed since the device
+    // was created or if the device has been removed.
+
+    // First, get the information for the default adapter from when the device was created.
+    winrt::com_ptr<IDXGIDevice3> dxgiDevice = m_d3dDevice.as< IDXGIDevice3>();
+    winrt::com_ptr<IDXGIAdapter> deviceAdapter;
+    winrt::check_hresult(dxgiDevice->GetAdapter(deviceAdapter.put()));
+    winrt::com_ptr<IDXGIFactory4> deviceFactory;
+    winrt::check_hresult(deviceAdapter->GetParent(IID_PPV_ARGS(&deviceFactory)));
+    winrt::com_ptr<IDXGIAdapter1> previousDefaultAdapter;
+    winrt::check_hresult(deviceFactory->EnumAdapters1(0, previousDefaultAdapter.put()));
+    DXGI_ADAPTER_DESC1 previousDesc;
+    winrt::check_hresult(previousDefaultAdapter->GetDesc1(&previousDesc));
+
+    // Next, get the information for the current default adapter.
+    winrt::com_ptr<IDXGIFactory4> currentFactory;
+    winrt::check_hresult(CreateDXGIFactory1(IID_PPV_ARGS(&currentFactory)));
+    winrt::com_ptr<IDXGIAdapter1> currentDefaultAdapter;
+    winrt::check_hresult(currentFactory->EnumAdapters1(0, currentDefaultAdapter.put()));
+    DXGI_ADAPTER_DESC1 currentDesc;
+    winrt::check_hresult(currentDefaultAdapter->GetDesc1(&currentDesc));
+
+    // If the adapter LUIDs don't match, or if the device reports that it has been removed,
+    // a new D3D device must be created.
+    if (previousDesc.AdapterLuid.LowPart != currentDesc.AdapterLuid.LowPart ||
+        previousDesc.AdapterLuid.HighPart != currentDesc.AdapterLuid.HighPart ||
+        FAILED(m_d3dDevice->GetDeviceRemovedReason()))
+    {
+        // Release references to resources related to the old device.
+        dxgiDevice = nullptr;
+        deviceAdapter = nullptr;
+        deviceFactory = nullptr;
+        previousDefaultAdapter = nullptr;
+
+        // Create a new device and swap chain.
+        HandleDeviceLost();
+    }
+}
+
+// Recreate all device resources and set them back to the current state.
+void DeviceResources::HandleDeviceLost() {
+    m_swapChain = nullptr;
+    if (m_deviceNotify != nullptr) {
+        m_deviceNotify->OnDeviceLost();
+    }
+    CreateDeviceResources();
+    CreateWindowSizeDependentResources();
+    if (m_deviceNotify != nullptr) {
+        m_deviceNotify->OnDeviceRestored();
+    }
+}
+
+// Register our DeviceNotify to be informed on device lost and creation.
+void DeviceResources::RegisterDeviceNotify(IDeviceNotify* deviceNotify) {
+    m_deviceNotify = deviceNotify;
+}
+
+// Call this method when the app suspends. It provides a hint to the driver that the app
+// is entering an idle state and that temporary buffers can be reclaimed for use by other apps.
+void DeviceResources::Trim() {
+    m_d3dDevice.as<IDXGIDevice3>()->Trim();
+}
+
+// Present the contents of the swap chain to the screen.
+void DeviceResources::Present() {
+
+    // MSAA resolve if needed
+    if (_sapp.sample_count > 1) {
+        m_d3dContext->ResolveSubresource(m_d3dRenderTarget.get(), 0, m_d3dMSAARenderTarget.get(), 0, DXGI_FORMAT_B8G8R8A8_UNORM);
+        m_d3dContext->DiscardView1(m_d3dMSAARenderTargetView.get(), nullptr, 0);
+    }
+
+    // The first argument instructs DXGI to block until VSync, putting the application
+    // to sleep until the next VSync. This ensures we don't waste any cycles rendering
+    // frames that will never be displayed to the screen.
+    DXGI_PRESENT_PARAMETERS parameters = { 0 };
+    HRESULT hr = m_swapChain->Present1(1, 0, &parameters);
+
+    // Discard the contents of the render target.
+    // This is a valid operation only when the existing contents will be entirely
+    // overwritten. If dirty or scroll rects are used, this call should be removed.
+    m_d3dContext->DiscardView1(m_d3dRenderTargetView.get(), nullptr, 0);
+
+    // Discard the contents of the depth stencil.
+    m_d3dContext->DiscardView1(m_d3dDepthStencilView.get(), nullptr, 0);
+
+    // If the device was removed either by a disconnection or a driver upgrade, we
+    // must recreate all device resources.
+    if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) {
+        HandleDeviceLost();
+    }
+    else {
+        winrt::check_hresult(hr);
+    }
+}
+
+// This method determines the rotation between the display device's native orientation and the
+// current display orientation.
+DXGI_MODE_ROTATION DeviceResources::ComputeDisplayRotation() {
+    DXGI_MODE_ROTATION rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
+
+    // Note: NativeOrientation can only be Landscape or Portrait even though
+    // the DisplayOrientations enum has other values.
+    switch (m_nativeOrientation) {
+        case winrt::Windows::Graphics::Display::DisplayOrientations::Landscape:
+            switch (m_currentOrientation) {
+                case winrt::Windows::Graphics::Display::DisplayOrientations::Landscape:
+                    rotation = DXGI_MODE_ROTATION_IDENTITY;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::Portrait:
+                    rotation = DXGI_MODE_ROTATION_ROTATE270;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::LandscapeFlipped:
+                    rotation = DXGI_MODE_ROTATION_ROTATE180;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::PortraitFlipped:
+                    rotation = DXGI_MODE_ROTATION_ROTATE90;
+                    break;
+            }
+            break;
+
+        case winrt::Windows::Graphics::Display::DisplayOrientations::Portrait:
+            switch (m_currentOrientation) {
+                case winrt::Windows::Graphics::Display::DisplayOrientations::Landscape:
+                    rotation = DXGI_MODE_ROTATION_ROTATE90;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::Portrait:
+                    rotation = DXGI_MODE_ROTATION_IDENTITY;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::LandscapeFlipped:
+                    rotation = DXGI_MODE_ROTATION_ROTATE270;
+                    break;
+
+                case winrt::Windows::Graphics::Display::DisplayOrientations::PortraitFlipped:
+                    rotation = DXGI_MODE_ROTATION_ROTATE180;
+                    break;
+            }
+            break;
+    }
+    return rotation;
+}
+
+// Check for SDK Layer support.
+bool DeviceResources::SdkLayersAvailable() {
+    #if defined(_DEBUG)
+        HRESULT hr = D3D11CreateDevice(
+            nullptr,
+            D3D_DRIVER_TYPE_NULL,       // There is no need to create a real hardware device.
+            0,
+            D3D11_CREATE_DEVICE_DEBUG,  // Check for the SDK layers.
+            nullptr,                    // Any feature level will do.
+            0,
+            D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for Microsoft Store apps.
+            nullptr,                    // No need to keep the D3D device reference.
+            nullptr,                    // No need to know the feature level.
+            nullptr                     // No need to keep the D3D device context reference.
+        );
+        return SUCCEEDED(hr);
+    #else
+        return false;
+    #endif
+}
+
+// The first method called when the IFrameworkView is being created.
+void App::Initialize(winrt::Windows::ApplicationModel::Core::CoreApplicationView const& applicationView) {
+    // Register event handlers for app lifecycle. This example includes Activated, so that we
+    // can make the CoreWindow active and start rendering on the window.
+    applicationView.Activated({ this, &App::OnActivated });
+
+    winrt::Windows::ApplicationModel::Core::CoreApplication::Suspending({ this, &App::OnSuspending });
+    winrt::Windows::ApplicationModel::Core::CoreApplication::Resuming({ this, &App::OnResuming });
+
+    // At this point we have access to the device.
+    // We can create the device-dependent resources.
+    m_deviceResources = std::make_unique<DeviceResources>();
+}
+
+// Called when the CoreWindow object is created (or re-created).
+void App::SetWindow(winrt::Windows::UI::Core::CoreWindow const& window) {
+    window.SizeChanged({ this, &App::OnWindowSizeChanged });
+    window.VisibilityChanged({ this, &App::OnVisibilityChanged });
+
+    window.KeyDown({ this, &App::OnKeyDown });
+    window.KeyUp({ this, &App::OnKeyUp });
+    window.CharacterReceived({ this, &App::OnCharacterReceived });
+
+    window.PointerEntered({ this, &App::OnPointerEntered });
+    window.PointerExited({ this, &App::OnPointerExited });
+    window.PointerPressed({ this, &App::OnPointerPressed });
+    window.PointerReleased({ this, &App::OnPointerReleased });
+    window.PointerMoved({ this, &App::OnPointerMoved });
+    window.PointerWheelChanged({ this, &App::OnPointerWheelChanged });
+
+    auto currentDisplayInformation = winrt::Windows::Graphics::Display::DisplayInformation::GetForCurrentView();
+
+    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });
+    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });
+    winrt::Windows::Graphics::Display::DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
+
+    winrt::Windows::UI::Core::SystemNavigationManager::GetForCurrentView().BackRequested({ this, &App::OnBackRequested });
+
+    m_deviceResources->SetWindow(window);
+}
+
+// Initializes scene resources, or loads a previously saved app state.
+void App::Load(winrt::hstring const& entryPoint) {
+    _SOKOL_UNUSED(entryPoint);
+}
+
+// This method is called after the window becomes active.
+void App::Run() {
+    // NOTE: UWP will simply terminate an application, it's not possible to detect when an application is being closed
+    while (true) {
+        if (m_windowVisible) {
+            winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(winrt::Windows::UI::Core::CoreProcessEventsOption::ProcessAllIfPresent);
+            _sapp_frame();
+            m_deviceResources->Present();
+        }
+        else {
+            winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(winrt::Windows::UI::Core::CoreProcessEventsOption::ProcessOneAndAllPending);
+        }
+    }
+}
+
+// Required for IFrameworkView.
+// Terminate events do not cause Uninitialize to be called. It will be called if your IFrameworkView
+// class is torn down while the app is in the foreground.
+void App::Uninitialize() {
+    // empty
+}
+
+// Application lifecycle event handlers.
+void App::OnActivated(winrt::Windows::ApplicationModel::Core::CoreApplicationView const& applicationView, winrt::Windows::ApplicationModel::Activation::IActivatedEventArgs const& args) {
+    _SOKOL_UNUSED(args);
+    _SOKOL_UNUSED(applicationView);
+    auto appView = winrt::Windows::UI::ViewManagement::ApplicationView::GetForCurrentView();
+    auto targetSize = winrt::Windows::Foundation::Size((float)_sapp.desc.width, (float)_sapp.desc.height);
+    appView.SetPreferredMinSize(targetSize);
+    appView.TryResizeView(targetSize);
+
+    // Disabling this since it can only append the title to the app name (Title - Appname).
+    // There's no way of just setting a string to be the window title.
+    //appView.Title(_sapp.window_title_wide);
+
+    // Run() won't start until the CoreWindow is activated.
+    winrt::Windows::UI::Core::CoreWindow::GetForCurrentThread().Activate();
+    if (_sapp.desc.fullscreen) {
+        appView.TryEnterFullScreenMode();
+    }
+    _sapp.fullscreen = appView.IsFullScreenMode();
+}
+
+void App::OnSuspending(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::ApplicationModel::SuspendingEventArgs const& args) {
+    _SOKOL_UNUSED(sender);
+    _SOKOL_UNUSED(args);
+    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_SUSPENDED);
+}
+
+void App::OnResuming(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::Foundation::IInspectable const& args) {
+    _SOKOL_UNUSED(args);
+    _SOKOL_UNUSED(sender);
+    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESUMED);
+}
+
+void App::OnWindowSizeChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::WindowSizeChangedEventArgs const& args) {
+    _SOKOL_UNUSED(args);
+    m_deviceResources->SetLogicalSize(winrt::Windows::Foundation::Size(sender.Bounds().Width, sender.Bounds().Height));
+    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESIZED);
+}
+
+void App::OnVisibilityChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::VisibilityChangedEventArgs const& args) {
+    _SOKOL_UNUSED(sender);
+    m_windowVisible = args.Visible();
+    _sapp_win32_uwp_app_event(m_windowVisible ? SAPP_EVENTTYPE_RESTORED : SAPP_EVENTTYPE_ICONIFIED);
+}
+
+void App::OnBackRequested(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Core::BackRequestedEventArgs const& args) {
+    _SOKOL_UNUSED(sender);
+    args.Handled(true);
+}
+
+void App::OnKeyDown(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::KeyEventArgs const& args) {
+    auto status = args.KeyStatus();
+    _sapp_uwp_key_event(SAPP_EVENTTYPE_KEY_DOWN, sender, args);
+}
+
+void App::OnKeyUp(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::KeyEventArgs const& args) {
+    auto status = args.KeyStatus();
+    _sapp_uwp_key_event(SAPP_EVENTTYPE_KEY_UP, sender, args);
+}
+
+void App::OnCharacterReceived(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::CharacterReceivedEventArgs const& args) {
+    _sapp_uwp_char_event(args.KeyCode(), args.KeyStatus().WasKeyDown, sender);
+}
+
+void App::OnPointerEntered(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    _SOKOL_UNUSED(args);
+    _sapp.uwp.mouse_tracked = true;
+    _sapp_uwp_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, sender);
+}
+
+void App::OnPointerExited(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    _SOKOL_UNUSED(args);
+    _sapp.uwp.mouse_tracked = false;
+    _sapp_uwp_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, sender);
+}
+
+void App::OnPointerPressed(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    _sapp_uwp_extract_mouse_button_events(sender, args);
+}
+
+// NOTE: for some reason this event handler is never called??
+void App::OnPointerReleased(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    _sapp_uwp_extract_mouse_button_events(sender, args);
+}
+
+void App::OnPointerMoved(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    auto position = args.CurrentPoint().Position();
+    const float new_x = (float)(int)(position.X * _sapp.uwp.dpi.mouse_scale + 0.5f);
+    const float new_y = (float)(int)(position.Y * _sapp.uwp.dpi.mouse_scale + 0.5f);
+    // don't update dx/dy in the very first event
+    if (_sapp.mouse.pos_valid) {
+        _sapp.mouse.dx = new_x - _sapp.mouse.x;
+        _sapp.mouse.dy = new_y - _sapp.mouse.y;
+    }
+    _sapp.mouse.x = new_x;
+    _sapp.mouse.y = new_y;
+    _sapp.mouse.pos_valid = true;
+    if (!_sapp.uwp.mouse_tracked) {
+        _sapp.uwp.mouse_tracked = true;
+        _sapp_uwp_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, sender);
+    }
+    _sapp_uwp_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, sender);
+
+    // HACK for detecting multiple mouse button presses
+    _sapp_uwp_extract_mouse_button_events(sender, args);
+}
+
+void App::OnPointerWheelChanged(winrt::Windows::UI::Core::CoreWindow const& sender, winrt::Windows::UI::Core::PointerEventArgs const& args) {
+    auto properties = args.CurrentPoint().Properties();
+    _sapp_uwp_scroll_event((float)properties.MouseWheelDelta(), properties.IsHorizontalMouseWheel(), sender);
+}
+
+void App::OnDpiChanged(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args) {
+    // NOTE: UNTESTED
+    _SOKOL_UNUSED(args);
+    m_deviceResources->SetDpi(sender.LogicalDpi());
+    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESIZED);
+}
+
+void App::OnOrientationChanged(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args) {
+    // NOTE: UNTESTED
+    _SOKOL_UNUSED(args);
+    m_deviceResources->SetCurrentOrientation(sender.CurrentOrientation());
+    _sapp_win32_uwp_app_event(SAPP_EVENTTYPE_RESIZED);
+}
+
+void App::OnDisplayContentsInvalidated(winrt::Windows::Graphics::Display::DisplayInformation const& sender, winrt::Windows::Foundation::IInspectable const& args) {
+    // NOTE: UNTESTED
+    _SOKOL_UNUSED(args);
+    _SOKOL_UNUSED(sender);
+    m_deviceResources->ValidateDevice();
+}
+
+} /* End empty namespace */
+
+_SOKOL_PRIVATE void _sapp_uwp_run(const sapp_desc* desc) {
+    _sapp_init_state(desc);
+    _sapp_win32_uwp_init_keytable();
+    _sapp_win32_uwp_utf8_to_wide(_sapp.window_title, _sapp.window_title_wide, sizeof(_sapp.window_title_wide));
+    winrt::Windows::ApplicationModel::Core::CoreApplication::Run(winrt::make<App>());
+}
+
+#if !defined(SOKOL_NO_ENTRY)
+#if defined(UNICODE)
+int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) {
+#else
+int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) {
+#endif
+    _SOKOL_UNUSED(hInstance);
+    _SOKOL_UNUSED(hPrevInstance);
+    _SOKOL_UNUSED(lpCmdLine);
+    _SOKOL_UNUSED(nCmdShow);
+    sapp_desc desc = sokol_main(0, nullptr);
+    _sapp_uwp_run(&desc);
+    return 0;
+}
+#endif /* SOKOL_NO_ENTRY */
+#endif /* _SAPP_UWP */
+
+/*== Android ================================================================*/
+#if defined(_SAPP_ANDROID)
+
+/* android loop thread */
+_SOKOL_PRIVATE bool _sapp_android_init_egl(void) {
+    SOKOL_ASSERT(_sapp.android.display == EGL_NO_DISPLAY);
+    SOKOL_ASSERT(_sapp.android.context == EGL_NO_CONTEXT);
+
+    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+    if (display == EGL_NO_DISPLAY) {
+        return false;
+    }
+    if (eglInitialize(display, NULL, NULL) == EGL_FALSE) {
+        return false;
+    }
+
+    EGLint alpha_size = _sapp.desc.alpha ? 8 : 0;
+    const EGLint cfg_attributes[] = {
+        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+        EGL_RED_SIZE, 8,
+        EGL_GREEN_SIZE, 8,
+        EGL_BLUE_SIZE, 8,
+        EGL_ALPHA_SIZE, alpha_size,
+        EGL_DEPTH_SIZE, 16,
+        EGL_STENCIL_SIZE, 0,
+        EGL_NONE,
+    };
+    EGLConfig available_cfgs[32];
+    EGLint cfg_count;
+    eglChooseConfig(display, cfg_attributes, available_cfgs, 32, &cfg_count);
+    SOKOL_ASSERT(cfg_count > 0);
+    SOKOL_ASSERT(cfg_count <= 32);
+
+    /* find config with 8-bit rgb buffer if available, ndk sample does not trust egl spec */
+    EGLConfig config;
+    bool exact_cfg_found = false;
+    for (int i = 0; i < cfg_count; ++i) {
+        EGLConfig c = available_cfgs[i];
+        EGLint r, g, b, a, d;
+        if (eglGetConfigAttrib(display, c, EGL_RED_SIZE, &r) == EGL_TRUE &&
+            eglGetConfigAttrib(display, c, EGL_GREEN_SIZE, &g) == EGL_TRUE &&
+            eglGetConfigAttrib(display, c, EGL_BLUE_SIZE, &b) == EGL_TRUE &&
+            eglGetConfigAttrib(display, c, EGL_ALPHA_SIZE, &a) == EGL_TRUE &&
+            eglGetConfigAttrib(display, c, EGL_DEPTH_SIZE, &d) == EGL_TRUE &&
+            r == 8 && g == 8 && b == 8 && (alpha_size == 0 || a == alpha_size) && d == 16) {
+            exact_cfg_found = true;
+            config = c;
+            break;
+        }
+    }
+    if (!exact_cfg_found) {
+        config = available_cfgs[0];
+    }
+
+    EGLint ctx_attributes[] = {
+        #if defined(SOKOL_GLES3)
+            EGL_CONTEXT_CLIENT_VERSION, _sapp.desc.gl_force_gles2 ? 2 : 3,
+        #else
+            EGL_CONTEXT_CLIENT_VERSION, 2,
+        #endif
+        EGL_NONE,
+    };
+    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctx_attributes);
+    if (context == EGL_NO_CONTEXT) {
+        return false;
+    }
+
+    _sapp.android.config = config;
+    _sapp.android.display = display;
+    _sapp.android.context = context;
+    return true;
+}
+
+_SOKOL_PRIVATE void _sapp_android_cleanup_egl(void) {
+    if (_sapp.android.display != EGL_NO_DISPLAY) {
+        eglMakeCurrent(_sapp.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+        if (_sapp.android.surface != EGL_NO_SURFACE) {
+            SOKOL_LOG("Destroying egl surface");
+            eglDestroySurface(_sapp.android.display, _sapp.android.surface);
+            _sapp.android.surface = EGL_NO_SURFACE;
+        }
+        if (_sapp.android.context != EGL_NO_CONTEXT) {
+            SOKOL_LOG("Destroying egl context");
+            eglDestroyContext(_sapp.android.display, _sapp.android.context);
+            _sapp.android.context = EGL_NO_CONTEXT;
+        }
+        SOKOL_LOG("Terminating egl display");
+        eglTerminate(_sapp.android.display);
+        _sapp.android.display = EGL_NO_DISPLAY;
+    }
+}
+
+_SOKOL_PRIVATE bool _sapp_android_init_egl_surface(ANativeWindow* window) {
+    SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY);
+    SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT);
+    SOKOL_ASSERT(_sapp.android.surface == EGL_NO_SURFACE);
+    SOKOL_ASSERT(window);
+
+    /* TODO: set window flags */
+    /* ANativeActivity_setWindowFlags(activity, AWINDOW_FLAG_KEEP_SCREEN_ON, 0); */
+
+    /* create egl surface and make it current */
+    EGLSurface surface = eglCreateWindowSurface(_sapp.android.display, _sapp.android.config, window, NULL);
+    if (surface == EGL_NO_SURFACE) {
+        return false;
+    }
+    if (eglMakeCurrent(_sapp.android.display, surface, surface, _sapp.android.context) == EGL_FALSE) {
+        return false;
+    }
+    _sapp.android.surface = surface;
+    return true;
+}
+
+_SOKOL_PRIVATE void _sapp_android_cleanup_egl_surface(void) {
+    if (_sapp.android.display == EGL_NO_DISPLAY) {
+        return;
+    }
+    eglMakeCurrent(_sapp.android.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+    if (_sapp.android.surface != EGL_NO_SURFACE) {
+        eglDestroySurface(_sapp.android.display, _sapp.android.surface);
+        _sapp.android.surface = EGL_NO_SURFACE;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_android_app_event(sapp_event_type type) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        SOKOL_LOG("event_cb()");
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_android_update_dimensions(ANativeWindow* window, bool force_update) {
+    SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY);
+    SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT);
+    SOKOL_ASSERT(_sapp.android.surface != EGL_NO_SURFACE);
+    SOKOL_ASSERT(window);
+
+    const int32_t win_w = ANativeWindow_getWidth(window);
+    const int32_t win_h = ANativeWindow_getHeight(window);
+    SOKOL_ASSERT(win_w >= 0 && win_h >= 0);
+    const bool win_changed = (win_w != _sapp.window_width) || (win_h != _sapp.window_height);
+    _sapp.window_width = win_w;
+    _sapp.window_height = win_h;
+    if (win_changed || force_update) {
+        if (!_sapp.desc.high_dpi) {
+            const int32_t buf_w = win_w / 2;
+            const int32_t buf_h = win_h / 2;
+            EGLint format;
+            EGLBoolean egl_result = eglGetConfigAttrib(_sapp.android.display, _sapp.android.config, EGL_NATIVE_VISUAL_ID, &format);
+            SOKOL_ASSERT(egl_result == EGL_TRUE);
+            /* NOTE: calling ANativeWindow_setBuffersGeometry() with the same dimensions
+                as the ANativeWindow size results in weird display artefacts, that's
+                why it's only called when the buffer geometry is different from
+                the window size
+            */
+            int32_t result = ANativeWindow_setBuffersGeometry(window, buf_w, buf_h, format);
+            SOKOL_ASSERT(result == 0);
+        }
+    }
+
+    /* query surface size */
+    EGLint fb_w, fb_h;
+    EGLBoolean egl_result_w = eglQuerySurface(_sapp.android.display, _sapp.android.surface, EGL_WIDTH, &fb_w);
+    EGLBoolean egl_result_h = eglQuerySurface(_sapp.android.display, _sapp.android.surface, EGL_HEIGHT, &fb_h);
+    SOKOL_ASSERT(egl_result_w == EGL_TRUE);
+    SOKOL_ASSERT(egl_result_h == EGL_TRUE);
+    const bool fb_changed = (fb_w != _sapp.framebuffer_width) || (fb_h != _sapp.framebuffer_height);
+    _sapp.framebuffer_width = fb_w;
+    _sapp.framebuffer_height = fb_h;
+    _sapp.dpi_scale = (float)_sapp.framebuffer_width / (float)_sapp.window_width;
+    if (win_changed || fb_changed || force_update) {
+        if (!_sapp.first_frame) {
+            SOKOL_LOG("SAPP_EVENTTYPE_RESIZED");
+            _sapp_android_app_event(SAPP_EVENTTYPE_RESIZED);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_android_cleanup(void) {
+    SOKOL_LOG("Cleaning up");
+    if (_sapp.android.surface != EGL_NO_SURFACE) {
+        /* egl context is bound, cleanup gracefully */
+        if (_sapp.init_called && !_sapp.cleanup_called) {
+            SOKOL_LOG("cleanup_cb()");
+            _sapp_call_cleanup();
+        }
+    }
+    /* always try to cleanup by destroying egl context */
+    _sapp_android_cleanup_egl();
+}
+
+_SOKOL_PRIVATE void _sapp_android_shutdown(void) {
+    /* try to cleanup while we still have a surface and can call cleanup_cb() */
+    _sapp_android_cleanup();
+    /* request exit */
+    ANativeActivity_finish(_sapp.android.activity);
+}
+
+_SOKOL_PRIVATE void _sapp_android_frame(void) {
+    SOKOL_ASSERT(_sapp.android.display != EGL_NO_DISPLAY);
+    SOKOL_ASSERT(_sapp.android.context != EGL_NO_CONTEXT);
+    SOKOL_ASSERT(_sapp.android.surface != EGL_NO_SURFACE);
+    _sapp_android_update_dimensions(_sapp.android.current.window, false);
+    _sapp_frame();
+    eglSwapBuffers(_sapp.android.display, _sapp.android.surface);
+}
+
+_SOKOL_PRIVATE bool _sapp_android_touch_event(const AInputEvent* e) {
+    if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_MOTION) {
+        return false;
+    }
+    if (!_sapp_events_enabled()) {
+        return false;
+    }
+    int32_t action_idx = AMotionEvent_getAction(e);
+    int32_t action = action_idx & AMOTION_EVENT_ACTION_MASK;
+    sapp_event_type type = SAPP_EVENTTYPE_INVALID;
+    switch (action) {
+        case AMOTION_EVENT_ACTION_DOWN:
+            SOKOL_LOG("Touch: down");
+        case AMOTION_EVENT_ACTION_POINTER_DOWN:
+            SOKOL_LOG("Touch: ptr down");
+            type = SAPP_EVENTTYPE_TOUCHES_BEGAN;
+            break;
+        case AMOTION_EVENT_ACTION_MOVE:
+            type = SAPP_EVENTTYPE_TOUCHES_MOVED;
+            break;
+        case AMOTION_EVENT_ACTION_UP:
+            SOKOL_LOG("Touch: up");
+        case AMOTION_EVENT_ACTION_POINTER_UP:
+            SOKOL_LOG("Touch: ptr up");
+            type = SAPP_EVENTTYPE_TOUCHES_ENDED;
+            break;
+        case AMOTION_EVENT_ACTION_CANCEL:
+            SOKOL_LOG("Touch: cancel");
+            type = SAPP_EVENTTYPE_TOUCHES_CANCELLED;
+            break;
+        default:
+            break;
+    }
+    if (type == SAPP_EVENTTYPE_INVALID) {
+        return false;
+    }
+    int32_t idx = action_idx >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+    _sapp_init_event(type);
+    _sapp.event.num_touches = (int)AMotionEvent_getPointerCount(e);
+    if (_sapp.event.num_touches > SAPP_MAX_TOUCHPOINTS) {
+        _sapp.event.num_touches = SAPP_MAX_TOUCHPOINTS;
+    }
+    for (int32_t i = 0; i < _sapp.event.num_touches; i++) {
+        sapp_touchpoint* dst = &_sapp.event.touches[i];
+        dst->identifier = (uintptr_t)AMotionEvent_getPointerId(e, (size_t)i);
+        dst->pos_x = (AMotionEvent_getRawX(e, (size_t)i) / _sapp.window_width) * _sapp.framebuffer_width;
+        dst->pos_y = (AMotionEvent_getRawY(e, (size_t)i) / _sapp.window_height) * _sapp.framebuffer_height;
+
+        if (action == AMOTION_EVENT_ACTION_POINTER_DOWN ||
+            action == AMOTION_EVENT_ACTION_POINTER_UP) {
+            dst->changed = (i == idx);
+        } else {
+            dst->changed = true;
+        }
+    }
+    _sapp_call_event(&_sapp.event);
+    return true;
+}
+
+_SOKOL_PRIVATE bool _sapp_android_key_event(const AInputEvent* e) {
+    if (AInputEvent_getType(e) != AINPUT_EVENT_TYPE_KEY) {
+        return false;
+    }
+    if (AKeyEvent_getKeyCode(e) == AKEYCODE_BACK) {
+        /* FIXME: this should be hooked into a "really quit?" mechanism
+           so the app can ask the user for confirmation, this is currently
+           generally missing in sokol_app.h
+        */
+        _sapp_android_shutdown();
+        return true;
+    }
+    return false;
+}
+
+_SOKOL_PRIVATE int _sapp_android_input_cb(int fd, int events, void* data) {
+    if ((events & ALOOPER_EVENT_INPUT) == 0) {
+        SOKOL_LOG("_sapp_android_input_cb() encountered unsupported event");
+        return 1;
+    }
+    SOKOL_ASSERT(_sapp.android.current.input);
+    AInputEvent* event = NULL;
+    while (AInputQueue_getEvent(_sapp.android.current.input, &event) >= 0) {
+        if (AInputQueue_preDispatchEvent(_sapp.android.current.input, event) != 0) {
+            continue;
+        }
+        int32_t handled = 0;
+        if (_sapp_android_touch_event(event) || _sapp_android_key_event(event)) {
+            handled = 1;
+        }
+        AInputQueue_finishEvent(_sapp.android.current.input, event, handled);
+    }
+    return 1;
+}
+
+_SOKOL_PRIVATE int _sapp_android_main_cb(int fd, int events, void* data) {
+    if ((events & ALOOPER_EVENT_INPUT) == 0) {
+        SOKOL_LOG("_sapp_android_main_cb() encountered unsupported event");
+        return 1;
+    }
+
+    _sapp_android_msg_t msg;
+    if (read(fd, &msg, sizeof(msg)) != sizeof(msg)) {
+        SOKOL_LOG("Could not write to read_from_main_fd");
+        return 1;
+    }
+
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    switch (msg) {
+        case _SOKOL_ANDROID_MSG_CREATE:
+            {
+                SOKOL_LOG("MSG_CREATE");
+                SOKOL_ASSERT(!_sapp.valid);
+                bool result = _sapp_android_init_egl();
+                SOKOL_ASSERT(result);
+                _sapp.valid = true;
+                _sapp.android.has_created = true;
+            }
+            break;
+        case _SOKOL_ANDROID_MSG_RESUME:
+            SOKOL_LOG("MSG_RESUME");
+            _sapp.android.has_resumed = true;
+            _sapp_android_app_event(SAPP_EVENTTYPE_RESUMED);
+            break;
+        case _SOKOL_ANDROID_MSG_PAUSE:
+            SOKOL_LOG("MSG_PAUSE");
+            _sapp.android.has_resumed = false;
+            _sapp_android_app_event(SAPP_EVENTTYPE_SUSPENDED);
+            break;
+        case _SOKOL_ANDROID_MSG_FOCUS:
+            SOKOL_LOG("MSG_FOCUS");
+            _sapp.android.has_focus = true;
+            break;
+        case _SOKOL_ANDROID_MSG_NO_FOCUS:
+            SOKOL_LOG("MSG_NO_FOCUS");
+            _sapp.android.has_focus = false;
+            break;
+        case _SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW:
+            SOKOL_LOG("MSG_SET_NATIVE_WINDOW");
+            if (_sapp.android.current.window != _sapp.android.pending.window) {
+                if (_sapp.android.current.window != NULL) {
+                    _sapp_android_cleanup_egl_surface();
+                }
+                if (_sapp.android.pending.window != NULL) {
+                    SOKOL_LOG("Creating egl surface ...");
+                    if (_sapp_android_init_egl_surface(_sapp.android.pending.window)) {
+                        SOKOL_LOG("... ok!");
+                        _sapp_android_update_dimensions(_sapp.android.pending.window, true);
+                    } else {
+                        SOKOL_LOG("... failed!");
+                        _sapp_android_shutdown();
+                    }
+                }
+            }
+            _sapp.android.current.window = _sapp.android.pending.window;
+            break;
+        case _SOKOL_ANDROID_MSG_SET_INPUT_QUEUE:
+            SOKOL_LOG("MSG_SET_INPUT_QUEUE");
+            if (_sapp.android.current.input != _sapp.android.pending.input) {
+                if (_sapp.android.current.input != NULL) {
+                    AInputQueue_detachLooper(_sapp.android.current.input);
+                }
+                if (_sapp.android.pending.input != NULL) {
+                    AInputQueue_attachLooper(
+                        _sapp.android.pending.input,
+                        _sapp.android.looper,
+                        ALOOPER_POLL_CALLBACK,
+                        _sapp_android_input_cb,
+                        NULL); /* data */
+                }
+            }
+            _sapp.android.current.input = _sapp.android.pending.input;
+            break;
+        case _SOKOL_ANDROID_MSG_DESTROY:
+            SOKOL_LOG("MSG_DESTROY");
+            _sapp_android_cleanup();
+            _sapp.valid = false;
+            _sapp.android.is_thread_stopping = true;
+            break;
+        default:
+            SOKOL_LOG("Unknown msg type received");
+            break;
+    }
+    pthread_cond_broadcast(&_sapp.android.pt.cond); /* signal "received" */
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+    return 1;
+}
+
+_SOKOL_PRIVATE bool _sapp_android_should_update(void) {
+    bool is_in_front = _sapp.android.has_resumed && _sapp.android.has_focus;
+    bool has_surface = _sapp.android.surface != EGL_NO_SURFACE;
+    return is_in_front && has_surface;
+}
+
+_SOKOL_PRIVATE void _sapp_android_show_keyboard(bool shown) {
+    SOKOL_ASSERT(_sapp.valid);
+    /* This seems to be broken in the NDK, but there is (a very cumbersome) workaround... */
+    if (shown) {
+        SOKOL_LOG("Showing keyboard");
+        ANativeActivity_showSoftInput(_sapp.android.activity, ANATIVEACTIVITY_SHOW_SOFT_INPUT_FORCED);
+    } else {
+        SOKOL_LOG("Hiding keyboard");
+        ANativeActivity_hideSoftInput(_sapp.android.activity, ANATIVEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS);
+    }
+}
+
+_SOKOL_PRIVATE void* _sapp_android_loop(void* arg) {
+    _SOKOL_UNUSED(arg);
+    SOKOL_LOG("Loop thread started");
+
+    _sapp.android.looper = ALooper_prepare(0 /* or ALOOPER_PREPARE_ALLOW_NON_CALLBACKS*/);
+    ALooper_addFd(_sapp.android.looper,
+        _sapp.android.pt.read_from_main_fd,
+        ALOOPER_POLL_CALLBACK,
+        ALOOPER_EVENT_INPUT,
+        _sapp_android_main_cb,
+        NULL); /* data */
+
+    /* signal start to main thread */
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp.android.is_thread_started = true;
+    pthread_cond_broadcast(&_sapp.android.pt.cond);
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+
+    /* main loop */
+    while (!_sapp.android.is_thread_stopping) {
+        /* sokol frame */
+        if (_sapp_android_should_update()) {
+            _sapp_android_frame();
+        }
+
+        /* process all events (or stop early if app is requested to quit) */
+        bool process_events = true;
+        while (process_events && !_sapp.android.is_thread_stopping) {
+            bool block_until_event = !_sapp.android.is_thread_stopping && !_sapp_android_should_update();
+            process_events = ALooper_pollOnce(block_until_event ? -1 : 0, NULL, NULL, NULL) == ALOOPER_POLL_CALLBACK;
+        }
+    }
+
+    /* cleanup thread */
+    if (_sapp.android.current.input != NULL) {
+        AInputQueue_detachLooper(_sapp.android.current.input);
+    }
+
+    /* the following causes heap corruption on exit, why??
+    ALooper_removeFd(_sapp.android.looper, _sapp.android.pt.read_from_main_fd);
+    ALooper_release(_sapp.android.looper);*/
+
+    /* signal "destroyed" */
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp.android.is_thread_stopped = true;
+    pthread_cond_broadcast(&_sapp.android.pt.cond);
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+    SOKOL_LOG("Loop thread done");
+    return NULL;
+}
+
+/* android main/ui thread */
+_SOKOL_PRIVATE void _sapp_android_msg(_sapp_android_msg_t msg) {
+    if (write(_sapp.android.pt.write_from_main_fd, &msg, sizeof(msg)) != sizeof(msg)) {
+        SOKOL_LOG("Could not write to write_from_main_fd");
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_start(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onStart()");
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_resume(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onResume()");
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_RESUME);
+}
+
+_SOKOL_PRIVATE void* _sapp_android_on_save_instance_state(ANativeActivity* activity, size_t* out_size) {
+    SOKOL_LOG("NativeActivity onSaveInstanceState()");
+    *out_size = 0;
+    return NULL;
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_window_focus_changed(ANativeActivity* activity, int has_focus) {
+    SOKOL_LOG("NativeActivity onWindowFocusChanged()");
+    if (has_focus) {
+        _sapp_android_msg(_SOKOL_ANDROID_MSG_FOCUS);
+    } else {
+        _sapp_android_msg(_SOKOL_ANDROID_MSG_NO_FOCUS);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_pause(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onPause()");
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_PAUSE);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_stop(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onStop()");
+}
+
+_SOKOL_PRIVATE void _sapp_android_msg_set_native_window(ANativeWindow* window) {
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp.android.pending.window = window;
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_SET_NATIVE_WINDOW);
+    while (_sapp.android.current.window != window) {
+        pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex);
+    }
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_native_window_created(ANativeActivity* activity, ANativeWindow* window) {
+    SOKOL_LOG("NativeActivity onNativeWindowCreated()");
+    _sapp_android_msg_set_native_window(window);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_native_window_destroyed(ANativeActivity* activity, ANativeWindow* window) {
+    SOKOL_LOG("NativeActivity onNativeWindowDestroyed()");
+    _sapp_android_msg_set_native_window(NULL);
+}
+
+_SOKOL_PRIVATE void _sapp_android_msg_set_input_queue(AInputQueue* input) {
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp.android.pending.input = input;
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_SET_INPUT_QUEUE);
+    while (_sapp.android.current.input != input) {
+        pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex);
+    }
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_input_queue_created(ANativeActivity* activity, AInputQueue* queue) {
+    SOKOL_LOG("NativeActivity onInputQueueCreated()");
+    _sapp_android_msg_set_input_queue(queue);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_input_queue_destroyed(ANativeActivity* activity, AInputQueue* queue) {
+    SOKOL_LOG("NativeActivity onInputQueueDestroyed()");
+    _sapp_android_msg_set_input_queue(NULL);
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_config_changed(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onConfigurationChanged()");
+    /* see android:configChanges in manifest */
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_low_memory(ANativeActivity* activity) {
+    SOKOL_LOG("NativeActivity onLowMemory()");
+}
+
+_SOKOL_PRIVATE void _sapp_android_on_destroy(ANativeActivity* activity) {
+    /*
+     * For some reason even an empty app using nativeactivity.h will crash (WIN DEATH)
+     * on my device (Moto X 2nd gen) when the app is removed from the task view
+     * (TaskStackView: onTaskViewDismissed).
+     *
+     * However, if ANativeActivity_finish() is explicitly called from for example
+     * _sapp_android_on_stop(), the crash disappears. Is this a bug in NativeActivity?
+     */
+    SOKOL_LOG("NativeActivity onDestroy()");
+
+    /* send destroy msg */
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_DESTROY);
+    while (!_sapp.android.is_thread_stopped) {
+        pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex);
+    }
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+
+    /* clean up main thread */
+    pthread_cond_destroy(&_sapp.android.pt.cond);
+    pthread_mutex_destroy(&_sapp.android.pt.mutex);
+
+    close(_sapp.android.pt.read_from_main_fd);
+    close(_sapp.android.pt.write_from_main_fd);
+
+    SOKOL_LOG("NativeActivity done");
+
+    /* this is a bit naughty, but causes a clean restart of the app (static globals are reset) */
+    exit(0);
+}
+
+JNIEXPORT
+void ANativeActivity_onCreate(ANativeActivity* activity, void* saved_state, size_t saved_state_size) {
+    SOKOL_LOG("NativeActivity onCreate()");
+
+    sapp_desc desc = sokol_main(0, NULL);
+    _sapp_init_state(&desc);
+
+    /* start loop thread */
+    _sapp.android.activity = activity;
+
+    int pipe_fd[2];
+    if (pipe(pipe_fd) != 0) {
+        SOKOL_LOG("Could not create thread pipe");
+        return;
+    }
+    _sapp.android.pt.read_from_main_fd = pipe_fd[0];
+    _sapp.android.pt.write_from_main_fd = pipe_fd[1];
+
+    pthread_mutex_init(&_sapp.android.pt.mutex, NULL);
+    pthread_cond_init(&_sapp.android.pt.cond, NULL);
+
+    pthread_attr_t attr;
+    pthread_attr_init(&attr);
+    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+    pthread_create(&_sapp.android.pt.thread, &attr, _sapp_android_loop, 0);
+    pthread_attr_destroy(&attr);
+
+    /* wait until main loop has started */
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    while (!_sapp.android.is_thread_started) {
+        pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex);
+    }
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+
+    /* send create msg */
+    pthread_mutex_lock(&_sapp.android.pt.mutex);
+    _sapp_android_msg(_SOKOL_ANDROID_MSG_CREATE);
+    while (!_sapp.android.has_created) {
+        pthread_cond_wait(&_sapp.android.pt.cond, &_sapp.android.pt.mutex);
+    }
+    pthread_mutex_unlock(&_sapp.android.pt.mutex);
+
+    /* register for callbacks */
+    activity->callbacks->onStart = _sapp_android_on_start;
+    activity->callbacks->onResume = _sapp_android_on_resume;
+    activity->callbacks->onSaveInstanceState = _sapp_android_on_save_instance_state;
+    activity->callbacks->onWindowFocusChanged = _sapp_android_on_window_focus_changed;
+    activity->callbacks->onPause = _sapp_android_on_pause;
+    activity->callbacks->onStop = _sapp_android_on_stop;
+    activity->callbacks->onDestroy = _sapp_android_on_destroy;
+    activity->callbacks->onNativeWindowCreated = _sapp_android_on_native_window_created;
+    /* activity->callbacks->onNativeWindowResized = _sapp_android_on_native_window_resized; */
+    /* activity->callbacks->onNativeWindowRedrawNeeded = _sapp_android_on_native_window_redraw_needed; */
+    activity->callbacks->onNativeWindowDestroyed = _sapp_android_on_native_window_destroyed;
+    activity->callbacks->onInputQueueCreated = _sapp_android_on_input_queue_created;
+    activity->callbacks->onInputQueueDestroyed = _sapp_android_on_input_queue_destroyed;
+    /* activity->callbacks->onContentRectChanged = _sapp_android_on_content_rect_changed; */
+    activity->callbacks->onConfigurationChanged = _sapp_android_on_config_changed;
+    activity->callbacks->onLowMemory = _sapp_android_on_low_memory;
+
+    SOKOL_LOG("NativeActivity successfully created");
+
+    /* NOT A BUG: do NOT call sapp_discard_state() */
+}
+
+#endif /* _SAPP_ANDROID */
+
+/*== LINUX ==================================================================*/
+#if defined(_SAPP_LINUX)
+
+/* see GLFW's xkb_unicode.c */
+static const struct _sapp_x11_codepair {
+  uint16_t keysym;
+  uint16_t ucs;
+} _sapp_x11_keysymtab[] = {
+  { 0x01a1, 0x0104 },
+  { 0x01a2, 0x02d8 },
+  { 0x01a3, 0x0141 },
+  { 0x01a5, 0x013d },
+  { 0x01a6, 0x015a },
+  { 0x01a9, 0x0160 },
+  { 0x01aa, 0x015e },
+  { 0x01ab, 0x0164 },
+  { 0x01ac, 0x0179 },
+  { 0x01ae, 0x017d },
+  { 0x01af, 0x017b },
+  { 0x01b1, 0x0105 },
+  { 0x01b2, 0x02db },
+  { 0x01b3, 0x0142 },
+  { 0x01b5, 0x013e },
+  { 0x01b6, 0x015b },
+  { 0x01b7, 0x02c7 },
+  { 0x01b9, 0x0161 },
+  { 0x01ba, 0x015f },
+  { 0x01bb, 0x0165 },
+  { 0x01bc, 0x017a },
+  { 0x01bd, 0x02dd },
+  { 0x01be, 0x017e },
+  { 0x01bf, 0x017c },
+  { 0x01c0, 0x0154 },
+  { 0x01c3, 0x0102 },
+  { 0x01c5, 0x0139 },
+  { 0x01c6, 0x0106 },
+  { 0x01c8, 0x010c },
+  { 0x01ca, 0x0118 },
+  { 0x01cc, 0x011a },
+  { 0x01cf, 0x010e },
+  { 0x01d0, 0x0110 },
+  { 0x01d1, 0x0143 },
+  { 0x01d2, 0x0147 },
+  { 0x01d5, 0x0150 },
+  { 0x01d8, 0x0158 },
+  { 0x01d9, 0x016e },
+  { 0x01db, 0x0170 },
+  { 0x01de, 0x0162 },
+  { 0x01e0, 0x0155 },
+  { 0x01e3, 0x0103 },
+  { 0x01e5, 0x013a },
+  { 0x01e6, 0x0107 },
+  { 0x01e8, 0x010d },
+  { 0x01ea, 0x0119 },
+  { 0x01ec, 0x011b },
+  { 0x01ef, 0x010f },
+  { 0x01f0, 0x0111 },
+  { 0x01f1, 0x0144 },
+  { 0x01f2, 0x0148 },
+  { 0x01f5, 0x0151 },
+  { 0x01f8, 0x0159 },
+  { 0x01f9, 0x016f },
+  { 0x01fb, 0x0171 },
+  { 0x01fe, 0x0163 },
+  { 0x01ff, 0x02d9 },
+  { 0x02a1, 0x0126 },
+  { 0x02a6, 0x0124 },
+  { 0x02a9, 0x0130 },
+  { 0x02ab, 0x011e },
+  { 0x02ac, 0x0134 },
+  { 0x02b1, 0x0127 },
+  { 0x02b6, 0x0125 },
+  { 0x02b9, 0x0131 },
+  { 0x02bb, 0x011f },
+  { 0x02bc, 0x0135 },
+  { 0x02c5, 0x010a },
+  { 0x02c6, 0x0108 },
+  { 0x02d5, 0x0120 },
+  { 0x02d8, 0x011c },
+  { 0x02dd, 0x016c },
+  { 0x02de, 0x015c },
+  { 0x02e5, 0x010b },
+  { 0x02e6, 0x0109 },
+  { 0x02f5, 0x0121 },
+  { 0x02f8, 0x011d },
+  { 0x02fd, 0x016d },
+  { 0x02fe, 0x015d },
+  { 0x03a2, 0x0138 },
+  { 0x03a3, 0x0156 },
+  { 0x03a5, 0x0128 },
+  { 0x03a6, 0x013b },
+  { 0x03aa, 0x0112 },
+  { 0x03ab, 0x0122 },
+  { 0x03ac, 0x0166 },
+  { 0x03b3, 0x0157 },
+  { 0x03b5, 0x0129 },
+  { 0x03b6, 0x013c },
+  { 0x03ba, 0x0113 },
+  { 0x03bb, 0x0123 },
+  { 0x03bc, 0x0167 },
+  { 0x03bd, 0x014a },
+  { 0x03bf, 0x014b },
+  { 0x03c0, 0x0100 },
+  { 0x03c7, 0x012e },
+  { 0x03cc, 0x0116 },
+  { 0x03cf, 0x012a },
+  { 0x03d1, 0x0145 },
+  { 0x03d2, 0x014c },
+  { 0x03d3, 0x0136 },
+  { 0x03d9, 0x0172 },
+  { 0x03dd, 0x0168 },
+  { 0x03de, 0x016a },
+  { 0x03e0, 0x0101 },
+  { 0x03e7, 0x012f },
+  { 0x03ec, 0x0117 },
+  { 0x03ef, 0x012b },
+  { 0x03f1, 0x0146 },
+  { 0x03f2, 0x014d },
+  { 0x03f3, 0x0137 },
+  { 0x03f9, 0x0173 },
+  { 0x03fd, 0x0169 },
+  { 0x03fe, 0x016b },
+  { 0x047e, 0x203e },
+  { 0x04a1, 0x3002 },
+  { 0x04a2, 0x300c },
+  { 0x04a3, 0x300d },
+  { 0x04a4, 0x3001 },
+  { 0x04a5, 0x30fb },
+  { 0x04a6, 0x30f2 },
+  { 0x04a7, 0x30a1 },
+  { 0x04a8, 0x30a3 },
+  { 0x04a9, 0x30a5 },
+  { 0x04aa, 0x30a7 },
+  { 0x04ab, 0x30a9 },
+  { 0x04ac, 0x30e3 },
+  { 0x04ad, 0x30e5 },
+  { 0x04ae, 0x30e7 },
+  { 0x04af, 0x30c3 },
+  { 0x04b0, 0x30fc },
+  { 0x04b1, 0x30a2 },
+  { 0x04b2, 0x30a4 },
+  { 0x04b3, 0x30a6 },
+  { 0x04b4, 0x30a8 },
+  { 0x04b5, 0x30aa },
+  { 0x04b6, 0x30ab },
+  { 0x04b7, 0x30ad },
+  { 0x04b8, 0x30af },
+  { 0x04b9, 0x30b1 },
+  { 0x04ba, 0x30b3 },
+  { 0x04bb, 0x30b5 },
+  { 0x04bc, 0x30b7 },
+  { 0x04bd, 0x30b9 },
+  { 0x04be, 0x30bb },
+  { 0x04bf, 0x30bd },
+  { 0x04c0, 0x30bf },
+  { 0x04c1, 0x30c1 },
+  { 0x04c2, 0x30c4 },
+  { 0x04c3, 0x30c6 },
+  { 0x04c4, 0x30c8 },
+  { 0x04c5, 0x30ca },
+  { 0x04c6, 0x30cb },
+  { 0x04c7, 0x30cc },
+  { 0x04c8, 0x30cd },
+  { 0x04c9, 0x30ce },
+  { 0x04ca, 0x30cf },
+  { 0x04cb, 0x30d2 },
+  { 0x04cc, 0x30d5 },
+  { 0x04cd, 0x30d8 },
+  { 0x04ce, 0x30db },
+  { 0x04cf, 0x30de },
+  { 0x04d0, 0x30df },
+  { 0x04d1, 0x30e0 },
+  { 0x04d2, 0x30e1 },
+  { 0x04d3, 0x30e2 },
+  { 0x04d4, 0x30e4 },
+  { 0x04d5, 0x30e6 },
+  { 0x04d6, 0x30e8 },
+  { 0x04d7, 0x30e9 },
+  { 0x04d8, 0x30ea },
+  { 0x04d9, 0x30eb },
+  { 0x04da, 0x30ec },
+  { 0x04db, 0x30ed },
+  { 0x04dc, 0x30ef },
+  { 0x04dd, 0x30f3 },
+  { 0x04de, 0x309b },
+  { 0x04df, 0x309c },
+  { 0x05ac, 0x060c },
+  { 0x05bb, 0x061b },
+  { 0x05bf, 0x061f },
+  { 0x05c1, 0x0621 },
+  { 0x05c2, 0x0622 },
+  { 0x05c3, 0x0623 },
+  { 0x05c4, 0x0624 },
+  { 0x05c5, 0x0625 },
+  { 0x05c6, 0x0626 },
+  { 0x05c7, 0x0627 },
+  { 0x05c8, 0x0628 },
+  { 0x05c9, 0x0629 },
+  { 0x05ca, 0x062a },
+  { 0x05cb, 0x062b },
+  { 0x05cc, 0x062c },
+  { 0x05cd, 0x062d },
+  { 0x05ce, 0x062e },
+  { 0x05cf, 0x062f },
+  { 0x05d0, 0x0630 },
+  { 0x05d1, 0x0631 },
+  { 0x05d2, 0x0632 },
+  { 0x05d3, 0x0633 },
+  { 0x05d4, 0x0634 },
+  { 0x05d5, 0x0635 },
+  { 0x05d6, 0x0636 },
+  { 0x05d7, 0x0637 },
+  { 0x05d8, 0x0638 },
+  { 0x05d9, 0x0639 },
+  { 0x05da, 0x063a },
+  { 0x05e0, 0x0640 },
+  { 0x05e1, 0x0641 },
+  { 0x05e2, 0x0642 },
+  { 0x05e3, 0x0643 },
+  { 0x05e4, 0x0644 },
+  { 0x05e5, 0x0645 },
+  { 0x05e6, 0x0646 },
+  { 0x05e7, 0x0647 },
+  { 0x05e8, 0x0648 },
+  { 0x05e9, 0x0649 },
+  { 0x05ea, 0x064a },
+  { 0x05eb, 0x064b },
+  { 0x05ec, 0x064c },
+  { 0x05ed, 0x064d },
+  { 0x05ee, 0x064e },
+  { 0x05ef, 0x064f },
+  { 0x05f0, 0x0650 },
+  { 0x05f1, 0x0651 },
+  { 0x05f2, 0x0652 },
+  { 0x06a1, 0x0452 },
+  { 0x06a2, 0x0453 },
+  { 0x06a3, 0x0451 },
+  { 0x06a4, 0x0454 },
+  { 0x06a5, 0x0455 },
+  { 0x06a6, 0x0456 },
+  { 0x06a7, 0x0457 },
+  { 0x06a8, 0x0458 },
+  { 0x06a9, 0x0459 },
+  { 0x06aa, 0x045a },
+  { 0x06ab, 0x045b },
+  { 0x06ac, 0x045c },
+  { 0x06ae, 0x045e },
+  { 0x06af, 0x045f },
+  { 0x06b0, 0x2116 },
+  { 0x06b1, 0x0402 },
+  { 0x06b2, 0x0403 },
+  { 0x06b3, 0x0401 },
+  { 0x06b4, 0x0404 },
+  { 0x06b5, 0x0405 },
+  { 0x06b6, 0x0406 },
+  { 0x06b7, 0x0407 },
+  { 0x06b8, 0x0408 },
+  { 0x06b9, 0x0409 },
+  { 0x06ba, 0x040a },
+  { 0x06bb, 0x040b },
+  { 0x06bc, 0x040c },
+  { 0x06be, 0x040e },
+  { 0x06bf, 0x040f },
+  { 0x06c0, 0x044e },
+  { 0x06c1, 0x0430 },
+  { 0x06c2, 0x0431 },
+  { 0x06c3, 0x0446 },
+  { 0x06c4, 0x0434 },
+  { 0x06c5, 0x0435 },
+  { 0x06c6, 0x0444 },
+  { 0x06c7, 0x0433 },
+  { 0x06c8, 0x0445 },
+  { 0x06c9, 0x0438 },
+  { 0x06ca, 0x0439 },
+  { 0x06cb, 0x043a },
+  { 0x06cc, 0x043b },
+  { 0x06cd, 0x043c },
+  { 0x06ce, 0x043d },
+  { 0x06cf, 0x043e },
+  { 0x06d0, 0x043f },
+  { 0x06d1, 0x044f },
+  { 0x06d2, 0x0440 },
+  { 0x06d3, 0x0441 },
+  { 0x06d4, 0x0442 },
+  { 0x06d5, 0x0443 },
+  { 0x06d6, 0x0436 },
+  { 0x06d7, 0x0432 },
+  { 0x06d8, 0x044c },
+  { 0x06d9, 0x044b },
+  { 0x06da, 0x0437 },
+  { 0x06db, 0x0448 },
+  { 0x06dc, 0x044d },
+  { 0x06dd, 0x0449 },
+  { 0x06de, 0x0447 },
+  { 0x06df, 0x044a },
+  { 0x06e0, 0x042e },
+  { 0x06e1, 0x0410 },
+  { 0x06e2, 0x0411 },
+  { 0x06e3, 0x0426 },
+  { 0x06e4, 0x0414 },
+  { 0x06e5, 0x0415 },
+  { 0x06e6, 0x0424 },
+  { 0x06e7, 0x0413 },
+  { 0x06e8, 0x0425 },
+  { 0x06e9, 0x0418 },
+  { 0x06ea, 0x0419 },
+  { 0x06eb, 0x041a },
+  { 0x06ec, 0x041b },
+  { 0x06ed, 0x041c },
+  { 0x06ee, 0x041d },
+  { 0x06ef, 0x041e },
+  { 0x06f0, 0x041f },
+  { 0x06f1, 0x042f },
+  { 0x06f2, 0x0420 },
+  { 0x06f3, 0x0421 },
+  { 0x06f4, 0x0422 },
+  { 0x06f5, 0x0423 },
+  { 0x06f6, 0x0416 },
+  { 0x06f7, 0x0412 },
+  { 0x06f8, 0x042c },
+  { 0x06f9, 0x042b },
+  { 0x06fa, 0x0417 },
+  { 0x06fb, 0x0428 },
+  { 0x06fc, 0x042d },
+  { 0x06fd, 0x0429 },
+  { 0x06fe, 0x0427 },
+  { 0x06ff, 0x042a },
+  { 0x07a1, 0x0386 },
+  { 0x07a2, 0x0388 },
+  { 0x07a3, 0x0389 },
+  { 0x07a4, 0x038a },
+  { 0x07a5, 0x03aa },
+  { 0x07a7, 0x038c },
+  { 0x07a8, 0x038e },
+  { 0x07a9, 0x03ab },
+  { 0x07ab, 0x038f },
+  { 0x07ae, 0x0385 },
+  { 0x07af, 0x2015 },
+  { 0x07b1, 0x03ac },
+  { 0x07b2, 0x03ad },
+  { 0x07b3, 0x03ae },
+  { 0x07b4, 0x03af },
+  { 0x07b5, 0x03ca },
+  { 0x07b6, 0x0390 },
+  { 0x07b7, 0x03cc },
+  { 0x07b8, 0x03cd },
+  { 0x07b9, 0x03cb },
+  { 0x07ba, 0x03b0 },
+  { 0x07bb, 0x03ce },
+  { 0x07c1, 0x0391 },
+  { 0x07c2, 0x0392 },
+  { 0x07c3, 0x0393 },
+  { 0x07c4, 0x0394 },
+  { 0x07c5, 0x0395 },
+  { 0x07c6, 0x0396 },
+  { 0x07c7, 0x0397 },
+  { 0x07c8, 0x0398 },
+  { 0x07c9, 0x0399 },
+  { 0x07ca, 0x039a },
+  { 0x07cb, 0x039b },
+  { 0x07cc, 0x039c },
+  { 0x07cd, 0x039d },
+  { 0x07ce, 0x039e },
+  { 0x07cf, 0x039f },
+  { 0x07d0, 0x03a0 },
+  { 0x07d1, 0x03a1 },
+  { 0x07d2, 0x03a3 },
+  { 0x07d4, 0x03a4 },
+  { 0x07d5, 0x03a5 },
+  { 0x07d6, 0x03a6 },
+  { 0x07d7, 0x03a7 },
+  { 0x07d8, 0x03a8 },
+  { 0x07d9, 0x03a9 },
+  { 0x07e1, 0x03b1 },
+  { 0x07e2, 0x03b2 },
+  { 0x07e3, 0x03b3 },
+  { 0x07e4, 0x03b4 },
+  { 0x07e5, 0x03b5 },
+  { 0x07e6, 0x03b6 },
+  { 0x07e7, 0x03b7 },
+  { 0x07e8, 0x03b8 },
+  { 0x07e9, 0x03b9 },
+  { 0x07ea, 0x03ba },
+  { 0x07eb, 0x03bb },
+  { 0x07ec, 0x03bc },
+  { 0x07ed, 0x03bd },
+  { 0x07ee, 0x03be },
+  { 0x07ef, 0x03bf },
+  { 0x07f0, 0x03c0 },
+  { 0x07f1, 0x03c1 },
+  { 0x07f2, 0x03c3 },
+  { 0x07f3, 0x03c2 },
+  { 0x07f4, 0x03c4 },
+  { 0x07f5, 0x03c5 },
+  { 0x07f6, 0x03c6 },
+  { 0x07f7, 0x03c7 },
+  { 0x07f8, 0x03c8 },
+  { 0x07f9, 0x03c9 },
+  { 0x08a1, 0x23b7 },
+  { 0x08a2, 0x250c },
+  { 0x08a3, 0x2500 },
+  { 0x08a4, 0x2320 },
+  { 0x08a5, 0x2321 },
+  { 0x08a6, 0x2502 },
+  { 0x08a7, 0x23a1 },
+  { 0x08a8, 0x23a3 },
+  { 0x08a9, 0x23a4 },
+  { 0x08aa, 0x23a6 },
+  { 0x08ab, 0x239b },
+  { 0x08ac, 0x239d },
+  { 0x08ad, 0x239e },
+  { 0x08ae, 0x23a0 },
+  { 0x08af, 0x23a8 },
+  { 0x08b0, 0x23ac },
+  { 0x08bc, 0x2264 },
+  { 0x08bd, 0x2260 },
+  { 0x08be, 0x2265 },
+  { 0x08bf, 0x222b },
+  { 0x08c0, 0x2234 },
+  { 0x08c1, 0x221d },
+  { 0x08c2, 0x221e },
+  { 0x08c5, 0x2207 },
+  { 0x08c8, 0x223c },
+  { 0x08c9, 0x2243 },
+  { 0x08cd, 0x21d4 },
+  { 0x08ce, 0x21d2 },
+  { 0x08cf, 0x2261 },
+  { 0x08d6, 0x221a },
+  { 0x08da, 0x2282 },
+  { 0x08db, 0x2283 },
+  { 0x08dc, 0x2229 },
+  { 0x08dd, 0x222a },
+  { 0x08de, 0x2227 },
+  { 0x08df, 0x2228 },
+  { 0x08ef, 0x2202 },
+  { 0x08f6, 0x0192 },
+  { 0x08fb, 0x2190 },
+  { 0x08fc, 0x2191 },
+  { 0x08fd, 0x2192 },
+  { 0x08fe, 0x2193 },
+  { 0x09e0, 0x25c6 },
+  { 0x09e1, 0x2592 },
+  { 0x09e2, 0x2409 },
+  { 0x09e3, 0x240c },
+  { 0x09e4, 0x240d },
+  { 0x09e5, 0x240a },
+  { 0x09e8, 0x2424 },
+  { 0x09e9, 0x240b },
+  { 0x09ea, 0x2518 },
+  { 0x09eb, 0x2510 },
+  { 0x09ec, 0x250c },
+  { 0x09ed, 0x2514 },
+  { 0x09ee, 0x253c },
+  { 0x09ef, 0x23ba },
+  { 0x09f0, 0x23bb },
+  { 0x09f1, 0x2500 },
+  { 0x09f2, 0x23bc },
+  { 0x09f3, 0x23bd },
+  { 0x09f4, 0x251c },
+  { 0x09f5, 0x2524 },
+  { 0x09f6, 0x2534 },
+  { 0x09f7, 0x252c },
+  { 0x09f8, 0x2502 },
+  { 0x0aa1, 0x2003 },
+  { 0x0aa2, 0x2002 },
+  { 0x0aa3, 0x2004 },
+  { 0x0aa4, 0x2005 },
+  { 0x0aa5, 0x2007 },
+  { 0x0aa6, 0x2008 },
+  { 0x0aa7, 0x2009 },
+  { 0x0aa8, 0x200a },
+  { 0x0aa9, 0x2014 },
+  { 0x0aaa, 0x2013 },
+  { 0x0aae, 0x2026 },
+  { 0x0aaf, 0x2025 },
+  { 0x0ab0, 0x2153 },
+  { 0x0ab1, 0x2154 },
+  { 0x0ab2, 0x2155 },
+  { 0x0ab3, 0x2156 },
+  { 0x0ab4, 0x2157 },
+  { 0x0ab5, 0x2158 },
+  { 0x0ab6, 0x2159 },
+  { 0x0ab7, 0x215a },
+  { 0x0ab8, 0x2105 },
+  { 0x0abb, 0x2012 },
+  { 0x0abc, 0x2329 },
+  { 0x0abe, 0x232a },
+  { 0x0ac3, 0x215b },
+  { 0x0ac4, 0x215c },
+  { 0x0ac5, 0x215d },
+  { 0x0ac6, 0x215e },
+  { 0x0ac9, 0x2122 },
+  { 0x0aca, 0x2613 },
+  { 0x0acc, 0x25c1 },
+  { 0x0acd, 0x25b7 },
+  { 0x0ace, 0x25cb },
+  { 0x0acf, 0x25af },
+  { 0x0ad0, 0x2018 },
+  { 0x0ad1, 0x2019 },
+  { 0x0ad2, 0x201c },
+  { 0x0ad3, 0x201d },
+  { 0x0ad4, 0x211e },
+  { 0x0ad6, 0x2032 },
+  { 0x0ad7, 0x2033 },
+  { 0x0ad9, 0x271d },
+  { 0x0adb, 0x25ac },
+  { 0x0adc, 0x25c0 },
+  { 0x0add, 0x25b6 },
+  { 0x0ade, 0x25cf },
+  { 0x0adf, 0x25ae },
+  { 0x0ae0, 0x25e6 },
+  { 0x0ae1, 0x25ab },
+  { 0x0ae2, 0x25ad },
+  { 0x0ae3, 0x25b3 },
+  { 0x0ae4, 0x25bd },
+  { 0x0ae5, 0x2606 },
+  { 0x0ae6, 0x2022 },
+  { 0x0ae7, 0x25aa },
+  { 0x0ae8, 0x25b2 },
+  { 0x0ae9, 0x25bc },
+  { 0x0aea, 0x261c },
+  { 0x0aeb, 0x261e },
+  { 0x0aec, 0x2663 },
+  { 0x0aed, 0x2666 },
+  { 0x0aee, 0x2665 },
+  { 0x0af0, 0x2720 },
+  { 0x0af1, 0x2020 },
+  { 0x0af2, 0x2021 },
+  { 0x0af3, 0x2713 },
+  { 0x0af4, 0x2717 },
+  { 0x0af5, 0x266f },
+  { 0x0af6, 0x266d },
+  { 0x0af7, 0x2642 },
+  { 0x0af8, 0x2640 },
+  { 0x0af9, 0x260e },
+  { 0x0afa, 0x2315 },
+  { 0x0afb, 0x2117 },
+  { 0x0afc, 0x2038 },
+  { 0x0afd, 0x201a },
+  { 0x0afe, 0x201e },
+  { 0x0ba3, 0x003c },
+  { 0x0ba6, 0x003e },
+  { 0x0ba8, 0x2228 },
+  { 0x0ba9, 0x2227 },
+  { 0x0bc0, 0x00af },
+  { 0x0bc2, 0x22a5 },
+  { 0x0bc3, 0x2229 },
+  { 0x0bc4, 0x230a },
+  { 0x0bc6, 0x005f },
+  { 0x0bca, 0x2218 },
+  { 0x0bcc, 0x2395 },
+  { 0x0bce, 0x22a4 },
+  { 0x0bcf, 0x25cb },
+  { 0x0bd3, 0x2308 },
+  { 0x0bd6, 0x222a },
+  { 0x0bd8, 0x2283 },
+  { 0x0bda, 0x2282 },
+  { 0x0bdc, 0x22a2 },
+  { 0x0bfc, 0x22a3 },
+  { 0x0cdf, 0x2017 },
+  { 0x0ce0, 0x05d0 },
+  { 0x0ce1, 0x05d1 },
+  { 0x0ce2, 0x05d2 },
+  { 0x0ce3, 0x05d3 },
+  { 0x0ce4, 0x05d4 },
+  { 0x0ce5, 0x05d5 },
+  { 0x0ce6, 0x05d6 },
+  { 0x0ce7, 0x05d7 },
+  { 0x0ce8, 0x05d8 },
+  { 0x0ce9, 0x05d9 },
+  { 0x0cea, 0x05da },
+  { 0x0ceb, 0x05db },
+  { 0x0cec, 0x05dc },
+  { 0x0ced, 0x05dd },
+  { 0x0cee, 0x05de },
+  { 0x0cef, 0x05df },
+  { 0x0cf0, 0x05e0 },
+  { 0x0cf1, 0x05e1 },
+  { 0x0cf2, 0x05e2 },
+  { 0x0cf3, 0x05e3 },
+  { 0x0cf4, 0x05e4 },
+  { 0x0cf5, 0x05e5 },
+  { 0x0cf6, 0x05e6 },
+  { 0x0cf7, 0x05e7 },
+  { 0x0cf8, 0x05e8 },
+  { 0x0cf9, 0x05e9 },
+  { 0x0cfa, 0x05ea },
+  { 0x0da1, 0x0e01 },
+  { 0x0da2, 0x0e02 },
+  { 0x0da3, 0x0e03 },
+  { 0x0da4, 0x0e04 },
+  { 0x0da5, 0x0e05 },
+  { 0x0da6, 0x0e06 },
+  { 0x0da7, 0x0e07 },
+  { 0x0da8, 0x0e08 },
+  { 0x0da9, 0x0e09 },
+  { 0x0daa, 0x0e0a },
+  { 0x0dab, 0x0e0b },
+  { 0x0dac, 0x0e0c },
+  { 0x0dad, 0x0e0d },
+  { 0x0dae, 0x0e0e },
+  { 0x0daf, 0x0e0f },
+  { 0x0db0, 0x0e10 },
+  { 0x0db1, 0x0e11 },
+  { 0x0db2, 0x0e12 },
+  { 0x0db3, 0x0e13 },
+  { 0x0db4, 0x0e14 },
+  { 0x0db5, 0x0e15 },
+  { 0x0db6, 0x0e16 },
+  { 0x0db7, 0x0e17 },
+  { 0x0db8, 0x0e18 },
+  { 0x0db9, 0x0e19 },
+  { 0x0dba, 0x0e1a },
+  { 0x0dbb, 0x0e1b },
+  { 0x0dbc, 0x0e1c },
+  { 0x0dbd, 0x0e1d },
+  { 0x0dbe, 0x0e1e },
+  { 0x0dbf, 0x0e1f },
+  { 0x0dc0, 0x0e20 },
+  { 0x0dc1, 0x0e21 },
+  { 0x0dc2, 0x0e22 },
+  { 0x0dc3, 0x0e23 },
+  { 0x0dc4, 0x0e24 },
+  { 0x0dc5, 0x0e25 },
+  { 0x0dc6, 0x0e26 },
+  { 0x0dc7, 0x0e27 },
+  { 0x0dc8, 0x0e28 },
+  { 0x0dc9, 0x0e29 },
+  { 0x0dca, 0x0e2a },
+  { 0x0dcb, 0x0e2b },
+  { 0x0dcc, 0x0e2c },
+  { 0x0dcd, 0x0e2d },
+  { 0x0dce, 0x0e2e },
+  { 0x0dcf, 0x0e2f },
+  { 0x0dd0, 0x0e30 },
+  { 0x0dd1, 0x0e31 },
+  { 0x0dd2, 0x0e32 },
+  { 0x0dd3, 0x0e33 },
+  { 0x0dd4, 0x0e34 },
+  { 0x0dd5, 0x0e35 },
+  { 0x0dd6, 0x0e36 },
+  { 0x0dd7, 0x0e37 },
+  { 0x0dd8, 0x0e38 },
+  { 0x0dd9, 0x0e39 },
+  { 0x0dda, 0x0e3a },
+  { 0x0ddf, 0x0e3f },
+  { 0x0de0, 0x0e40 },
+  { 0x0de1, 0x0e41 },
+  { 0x0de2, 0x0e42 },
+  { 0x0de3, 0x0e43 },
+  { 0x0de4, 0x0e44 },
+  { 0x0de5, 0x0e45 },
+  { 0x0de6, 0x0e46 },
+  { 0x0de7, 0x0e47 },
+  { 0x0de8, 0x0e48 },
+  { 0x0de9, 0x0e49 },
+  { 0x0dea, 0x0e4a },
+  { 0x0deb, 0x0e4b },
+  { 0x0dec, 0x0e4c },
+  { 0x0ded, 0x0e4d },
+  { 0x0df0, 0x0e50 },
+  { 0x0df1, 0x0e51 },
+  { 0x0df2, 0x0e52 },
+  { 0x0df3, 0x0e53 },
+  { 0x0df4, 0x0e54 },
+  { 0x0df5, 0x0e55 },
+  { 0x0df6, 0x0e56 },
+  { 0x0df7, 0x0e57 },
+  { 0x0df8, 0x0e58 },
+  { 0x0df9, 0x0e59 },
+  { 0x0ea1, 0x3131 },
+  { 0x0ea2, 0x3132 },
+  { 0x0ea3, 0x3133 },
+  { 0x0ea4, 0x3134 },
+  { 0x0ea5, 0x3135 },
+  { 0x0ea6, 0x3136 },
+  { 0x0ea7, 0x3137 },
+  { 0x0ea8, 0x3138 },
+  { 0x0ea9, 0x3139 },
+  { 0x0eaa, 0x313a },
+  { 0x0eab, 0x313b },
+  { 0x0eac, 0x313c },
+  { 0x0ead, 0x313d },
+  { 0x0eae, 0x313e },
+  { 0x0eaf, 0x313f },
+  { 0x0eb0, 0x3140 },
+  { 0x0eb1, 0x3141 },
+  { 0x0eb2, 0x3142 },
+  { 0x0eb3, 0x3143 },
+  { 0x0eb4, 0x3144 },
+  { 0x0eb5, 0x3145 },
+  { 0x0eb6, 0x3146 },
+  { 0x0eb7, 0x3147 },
+  { 0x0eb8, 0x3148 },
+  { 0x0eb9, 0x3149 },
+  { 0x0eba, 0x314a },
+  { 0x0ebb, 0x314b },
+  { 0x0ebc, 0x314c },
+  { 0x0ebd, 0x314d },
+  { 0x0ebe, 0x314e },
+  { 0x0ebf, 0x314f },
+  { 0x0ec0, 0x3150 },
+  { 0x0ec1, 0x3151 },
+  { 0x0ec2, 0x3152 },
+  { 0x0ec3, 0x3153 },
+  { 0x0ec4, 0x3154 },
+  { 0x0ec5, 0x3155 },
+  { 0x0ec6, 0x3156 },
+  { 0x0ec7, 0x3157 },
+  { 0x0ec8, 0x3158 },
+  { 0x0ec9, 0x3159 },
+  { 0x0eca, 0x315a },
+  { 0x0ecb, 0x315b },
+  { 0x0ecc, 0x315c },
+  { 0x0ecd, 0x315d },
+  { 0x0ece, 0x315e },
+  { 0x0ecf, 0x315f },
+  { 0x0ed0, 0x3160 },
+  { 0x0ed1, 0x3161 },
+  { 0x0ed2, 0x3162 },
+  { 0x0ed3, 0x3163 },
+  { 0x0ed4, 0x11a8 },
+  { 0x0ed5, 0x11a9 },
+  { 0x0ed6, 0x11aa },
+  { 0x0ed7, 0x11ab },
+  { 0x0ed8, 0x11ac },
+  { 0x0ed9, 0x11ad },
+  { 0x0eda, 0x11ae },
+  { 0x0edb, 0x11af },
+  { 0x0edc, 0x11b0 },
+  { 0x0edd, 0x11b1 },
+  { 0x0ede, 0x11b2 },
+  { 0x0edf, 0x11b3 },
+  { 0x0ee0, 0x11b4 },
+  { 0x0ee1, 0x11b5 },
+  { 0x0ee2, 0x11b6 },
+  { 0x0ee3, 0x11b7 },
+  { 0x0ee4, 0x11b8 },
+  { 0x0ee5, 0x11b9 },
+  { 0x0ee6, 0x11ba },
+  { 0x0ee7, 0x11bb },
+  { 0x0ee8, 0x11bc },
+  { 0x0ee9, 0x11bd },
+  { 0x0eea, 0x11be },
+  { 0x0eeb, 0x11bf },
+  { 0x0eec, 0x11c0 },
+  { 0x0eed, 0x11c1 },
+  { 0x0eee, 0x11c2 },
+  { 0x0eef, 0x316d },
+  { 0x0ef0, 0x3171 },
+  { 0x0ef1, 0x3178 },
+  { 0x0ef2, 0x317f },
+  { 0x0ef3, 0x3181 },
+  { 0x0ef4, 0x3184 },
+  { 0x0ef5, 0x3186 },
+  { 0x0ef6, 0x318d },
+  { 0x0ef7, 0x318e },
+  { 0x0ef8, 0x11eb },
+  { 0x0ef9, 0x11f0 },
+  { 0x0efa, 0x11f9 },
+  { 0x0eff, 0x20a9 },
+  { 0x13a4, 0x20ac },
+  { 0x13bc, 0x0152 },
+  { 0x13bd, 0x0153 },
+  { 0x13be, 0x0178 },
+  { 0x20ac, 0x20ac },
+  { 0xfe50,    '`' },
+  { 0xfe51, 0x00b4 },
+  { 0xfe52,    '^' },
+  { 0xfe53,    '~' },
+  { 0xfe54, 0x00af },
+  { 0xfe55, 0x02d8 },
+  { 0xfe56, 0x02d9 },
+  { 0xfe57, 0x00a8 },
+  { 0xfe58, 0x02da },
+  { 0xfe59, 0x02dd },
+  { 0xfe5a, 0x02c7 },
+  { 0xfe5b, 0x00b8 },
+  { 0xfe5c, 0x02db },
+  { 0xfe5d, 0x037a },
+  { 0xfe5e, 0x309b },
+  { 0xfe5f, 0x309c },
+  { 0xfe63,    '/' },
+  { 0xfe64, 0x02bc },
+  { 0xfe65, 0x02bd },
+  { 0xfe66, 0x02f5 },
+  { 0xfe67, 0x02f3 },
+  { 0xfe68, 0x02cd },
+  { 0xfe69, 0xa788 },
+  { 0xfe6a, 0x02f7 },
+  { 0xfe6e,    ',' },
+  { 0xfe6f, 0x00a4 },
+  { 0xfe80,    'a' }, /* XK_dead_a */
+  { 0xfe81,    'A' }, /* XK_dead_A */
+  { 0xfe82,    'e' }, /* XK_dead_e */
+  { 0xfe83,    'E' }, /* XK_dead_E */
+  { 0xfe84,    'i' }, /* XK_dead_i */
+  { 0xfe85,    'I' }, /* XK_dead_I */
+  { 0xfe86,    'o' }, /* XK_dead_o */
+  { 0xfe87,    'O' }, /* XK_dead_O */
+  { 0xfe88,    'u' }, /* XK_dead_u */
+  { 0xfe89,    'U' }, /* XK_dead_U */
+  { 0xfe8a, 0x0259 },
+  { 0xfe8b, 0x018f },
+  { 0xfe8c, 0x00b5 },
+  { 0xfe90,    '_' },
+  { 0xfe91, 0x02c8 },
+  { 0xfe92, 0x02cc },
+  { 0xff80 /*XKB_KEY_KP_Space*/,     ' ' },
+  { 0xff95 /*XKB_KEY_KP_7*/, 0x0037 },
+  { 0xff96 /*XKB_KEY_KP_4*/, 0x0034 },
+  { 0xff97 /*XKB_KEY_KP_8*/, 0x0038 },
+  { 0xff98 /*XKB_KEY_KP_6*/, 0x0036 },
+  { 0xff99 /*XKB_KEY_KP_2*/, 0x0032 },
+  { 0xff9a /*XKB_KEY_KP_9*/, 0x0039 },
+  { 0xff9b /*XKB_KEY_KP_3*/, 0x0033 },
+  { 0xff9c /*XKB_KEY_KP_1*/, 0x0031 },
+  { 0xff9d /*XKB_KEY_KP_5*/, 0x0035 },
+  { 0xff9e /*XKB_KEY_KP_0*/, 0x0030 },
+  { 0xffaa /*XKB_KEY_KP_Multiply*/,  '*' },
+  { 0xffab /*XKB_KEY_KP_Add*/,       '+' },
+  { 0xffac /*XKB_KEY_KP_Separator*/, ',' },
+  { 0xffad /*XKB_KEY_KP_Subtract*/,  '-' },
+  { 0xffae /*XKB_KEY_KP_Decimal*/,   '.' },
+  { 0xffaf /*XKB_KEY_KP_Divide*/,    '/' },
+  { 0xffb0 /*XKB_KEY_KP_0*/, 0x0030 },
+  { 0xffb1 /*XKB_KEY_KP_1*/, 0x0031 },
+  { 0xffb2 /*XKB_KEY_KP_2*/, 0x0032 },
+  { 0xffb3 /*XKB_KEY_KP_3*/, 0x0033 },
+  { 0xffb4 /*XKB_KEY_KP_4*/, 0x0034 },
+  { 0xffb5 /*XKB_KEY_KP_5*/, 0x0035 },
+  { 0xffb6 /*XKB_KEY_KP_6*/, 0x0036 },
+  { 0xffb7 /*XKB_KEY_KP_7*/, 0x0037 },
+  { 0xffb8 /*XKB_KEY_KP_8*/, 0x0038 },
+  { 0xffb9 /*XKB_KEY_KP_9*/, 0x0039 },
+  { 0xffbd /*XKB_KEY_KP_Equal*/,     '=' }
+};
+
+_SOKOL_PRIVATE int _sapp_x11_error_handler(Display* display, XErrorEvent* event) {
+    _SOKOL_UNUSED(display);
+    _sapp.x11.error_code = event->error_code;
+    return 0;
+}
+
+_SOKOL_PRIVATE void _sapp_x11_grab_error_handler(void) {
+    _sapp.x11.error_code = Success;
+    XSetErrorHandler(_sapp_x11_error_handler);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_release_error_handler(void) {
+    XSync(_sapp.x11.display, False);
+    XSetErrorHandler(NULL);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_init_extensions(void) {
+    _sapp.x11.UTF8_STRING             = XInternAtom(_sapp.x11.display, "UTF8_STRING", False);
+    _sapp.x11.WM_PROTOCOLS            = XInternAtom(_sapp.x11.display, "WM_PROTOCOLS", False);
+    _sapp.x11.WM_DELETE_WINDOW        = XInternAtom(_sapp.x11.display, "WM_DELETE_WINDOW", False);
+    _sapp.x11.WM_STATE                = XInternAtom(_sapp.x11.display, "WM_STATE", False);
+    _sapp.x11.NET_WM_NAME             = XInternAtom(_sapp.x11.display, "_NET_WM_NAME", False);
+    _sapp.x11.NET_WM_ICON_NAME        = XInternAtom(_sapp.x11.display, "_NET_WM_ICON_NAME", False);
+    _sapp.x11.NET_WM_ICON             = XInternAtom(_sapp.x11.display, "_NET_WM_ICON", False);
+    _sapp.x11.NET_WM_STATE            = XInternAtom(_sapp.x11.display, "_NET_WM_STATE", False);
+    _sapp.x11.NET_WM_STATE_FULLSCREEN = XInternAtom(_sapp.x11.display, "_NET_WM_STATE_FULLSCREEN", False);
+    if (_sapp.drop.enabled) {
+        _sapp.x11.xdnd.XdndAware        = XInternAtom(_sapp.x11.display, "XdndAware", False);
+        _sapp.x11.xdnd.XdndEnter        = XInternAtom(_sapp.x11.display, "XdndEnter", False);
+        _sapp.x11.xdnd.XdndPosition     = XInternAtom(_sapp.x11.display, "XdndPosition", False);
+        _sapp.x11.xdnd.XdndStatus       = XInternAtom(_sapp.x11.display, "XdndStatus", False);
+        _sapp.x11.xdnd.XdndActionCopy   = XInternAtom(_sapp.x11.display, "XdndActionCopy", False);
+        _sapp.x11.xdnd.XdndDrop         = XInternAtom(_sapp.x11.display, "XdndDrop", False);
+        _sapp.x11.xdnd.XdndFinished     = XInternAtom(_sapp.x11.display, "XdndFinished", False);
+        _sapp.x11.xdnd.XdndSelection    = XInternAtom(_sapp.x11.display, "XdndSelection", False);
+        _sapp.x11.xdnd.XdndTypeList     = XInternAtom(_sapp.x11.display, "XdndTypeList", False);
+        _sapp.x11.xdnd.text_uri_list    = XInternAtom(_sapp.x11.display, "text/uri-list", False);
+    }
+
+    /* check Xi extension for raw mouse input */
+    if (XQueryExtension(_sapp.x11.display, "XInputExtension", &_sapp.x11.xi.major_opcode, &_sapp.x11.xi.event_base, &_sapp.x11.xi.error_base)) {
+        _sapp.x11.xi.major = 2;
+        _sapp.x11.xi.minor = 0;
+        if (XIQueryVersion(_sapp.x11.display, &_sapp.x11.xi.major, &_sapp.x11.xi.minor) == Success) {
+            _sapp.x11.xi.available = true;
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_query_system_dpi(void) {
+    /* from GLFW:
+
+       NOTE: Default to the display-wide DPI as we don't currently have a policy
+             for which monitor a window is considered to be on
+
+        _sapp.x11.dpi = DisplayWidth(_sapp.x11.display, _sapp.x11.screen) *
+                        25.4f / DisplayWidthMM(_sapp.x11.display, _sapp.x11.screen);
+
+       NOTE: Basing the scale on Xft.dpi where available should provide the most
+             consistent user experience (matches Qt, Gtk, etc), although not
+             always the most accurate one
+    */
+    char* rms = XResourceManagerString(_sapp.x11.display);
+    if (rms) {
+        XrmDatabase db = XrmGetStringDatabase(rms);
+        if (db) {
+            XrmValue value;
+            char* type = NULL;
+            if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) {
+                if (type && strcmp(type, "String") == 0) {
+                    _sapp.x11.dpi = atof(value.addr);
+                }
+            }
+            XrmDestroyDatabase(db);
+        }
+    }
+}
+
+_SOKOL_PRIVATE bool _sapp_glx_has_ext(const char* ext, const char* extensions) {
+    SOKOL_ASSERT(ext);
+    const char* start = extensions;
+    while (true) {
+        const char* where = strstr(start, ext);
+        if (!where) {
+            return false;
+        }
+        const char* terminator = where + strlen(ext);
+        if ((where == start) || (*(where - 1) == ' ')) {
+            if (*terminator == ' ' || *terminator == '\0') {
+                break;
+            }
+        }
+        start = terminator;
+    }
+    return true;
+}
+
+_SOKOL_PRIVATE bool _sapp_glx_extsupported(const char* ext, const char* extensions) {
+    if (extensions) {
+        return _sapp_glx_has_ext(ext, extensions);
+    }
+    else {
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE void* _sapp_glx_getprocaddr(const char* procname)
+{
+    if (_sapp.glx.GetProcAddress) {
+        return (void*) _sapp.glx.GetProcAddress(procname);
+    }
+    else if (_sapp.glx.GetProcAddressARB) {
+        return (void*) _sapp.glx.GetProcAddressARB(procname);
+    }
+    else {
+        return dlsym(_sapp.glx.libgl, procname);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_glx_init() {
+    const char* sonames[] = { "libGL.so.1", "libGL.so", 0 };
+    for (int i = 0; sonames[i]; i++) {
+        _sapp.glx.libgl = dlopen(sonames[i], RTLD_LAZY|RTLD_GLOBAL);
+        if (_sapp.glx.libgl) {
+            break;
+        }
+    }
+    if (!_sapp.glx.libgl) {
+        _sapp_fail("GLX: failed to load libGL");
+    }
+    _sapp.glx.GetFBConfigs          = (PFNGLXGETFBCONFIGSPROC)          dlsym(_sapp.glx.libgl, "glXGetFBConfigs");
+    _sapp.glx.GetFBConfigAttrib     = (PFNGLXGETFBCONFIGATTRIBPROC)     dlsym(_sapp.glx.libgl, "glXGetFBConfigAttrib");
+    _sapp.glx.GetClientString       = (PFNGLXGETCLIENTSTRINGPROC)       dlsym(_sapp.glx.libgl, "glXGetClientString");
+    _sapp.glx.QueryExtension        = (PFNGLXQUERYEXTENSIONPROC)        dlsym(_sapp.glx.libgl, "glXQueryExtension");
+    _sapp.glx.QueryVersion          = (PFNGLXQUERYVERSIONPROC)          dlsym(_sapp.glx.libgl, "glXQueryVersion");
+    _sapp.glx.DestroyContext        = (PFNGLXDESTROYCONTEXTPROC)        dlsym(_sapp.glx.libgl, "glXDestroyContext");
+    _sapp.glx.MakeCurrent           = (PFNGLXMAKECURRENTPROC)           dlsym(_sapp.glx.libgl, "glXMakeCurrent");
+    _sapp.glx.SwapBuffers           = (PFNGLXSWAPBUFFERSPROC)           dlsym(_sapp.glx.libgl, "glXSwapBuffers");
+    _sapp.glx.QueryExtensionsString = (PFNGLXQUERYEXTENSIONSSTRINGPROC) dlsym(_sapp.glx.libgl, "glXQueryExtensionsString");
+    _sapp.glx.CreateWindow          = (PFNGLXCREATEWINDOWPROC)          dlsym(_sapp.glx.libgl, "glXCreateWindow");
+    _sapp.glx.DestroyWindow         = (PFNGLXDESTROYWINDOWPROC)         dlsym(_sapp.glx.libgl, "glXDestroyWindow");
+    _sapp.glx.GetProcAddress        = (PFNGLXGETPROCADDRESSPROC)        dlsym(_sapp.glx.libgl, "glXGetProcAddress");
+    _sapp.glx.GetProcAddressARB     = (PFNGLXGETPROCADDRESSPROC)        dlsym(_sapp.glx.libgl, "glXGetProcAddressARB");
+    _sapp.glx.GetVisualFromFBConfig = (PFNGLXGETVISUALFROMFBCONFIGPROC) dlsym(_sapp.glx.libgl, "glXGetVisualFromFBConfig");
+    if (!_sapp.glx.GetFBConfigs ||
+        !_sapp.glx.GetFBConfigAttrib ||
+        !_sapp.glx.GetClientString ||
+        !_sapp.glx.QueryExtension ||
+        !_sapp.glx.QueryVersion ||
+        !_sapp.glx.DestroyContext ||
+        !_sapp.glx.MakeCurrent ||
+        !_sapp.glx.SwapBuffers ||
+        !_sapp.glx.QueryExtensionsString ||
+        !_sapp.glx.CreateWindow ||
+        !_sapp.glx.DestroyWindow ||
+        !_sapp.glx.GetProcAddress ||
+        !_sapp.glx.GetProcAddressARB ||
+        !_sapp.glx.GetVisualFromFBConfig)
+    {
+        _sapp_fail("GLX: failed to load required entry points");
+    }
+
+    if (!_sapp.glx.QueryExtension(_sapp.x11.display, &_sapp.glx.error_base, &_sapp.glx.event_base)) {
+        _sapp_fail("GLX: GLX extension not found");
+    }
+    if (!_sapp.glx.QueryVersion(_sapp.x11.display, &_sapp.glx.major, &_sapp.glx.minor)) {
+        _sapp_fail("GLX: Failed to query GLX version");
+    }
+    if (_sapp.glx.major == 1 && _sapp.glx.minor < 3) {
+        _sapp_fail("GLX: GLX version 1.3 is required");
+    }
+    const char* exts = _sapp.glx.QueryExtensionsString(_sapp.x11.display, _sapp.x11.screen);
+    if (_sapp_glx_extsupported("GLX_EXT_swap_control", exts)) {
+        _sapp.glx.SwapIntervalEXT = (PFNGLXSWAPINTERVALEXTPROC) _sapp_glx_getprocaddr("glXSwapIntervalEXT");
+        _sapp.glx.EXT_swap_control = 0 != _sapp.glx.SwapIntervalEXT;
+    }
+    if (_sapp_glx_extsupported("GLX_MESA_swap_control", exts)) {
+        _sapp.glx.SwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC) _sapp_glx_getprocaddr("glXSwapIntervalMESA");
+        _sapp.glx.MESA_swap_control = 0 != _sapp.glx.SwapIntervalMESA;
+    }
+    _sapp.glx.ARB_multisample = _sapp_glx_extsupported("GLX_ARB_multisample", exts);
+    if (_sapp_glx_extsupported("GLX_ARB_create_context", exts)) {
+        _sapp.glx.CreateContextAttribsARB = (PFNGLXCREATECONTEXTATTRIBSARBPROC) _sapp_glx_getprocaddr("glXCreateContextAttribsARB");
+        _sapp.glx.ARB_create_context = 0 != _sapp.glx.CreateContextAttribsARB;
+    }
+    _sapp.glx.ARB_create_context_profile = _sapp_glx_extsupported("GLX_ARB_create_context_profile", exts);
+}
+
+_SOKOL_PRIVATE int _sapp_glx_attrib(GLXFBConfig fbconfig, int attrib) {
+    int value;
+    _sapp.glx.GetFBConfigAttrib(_sapp.x11.display, fbconfig, attrib, &value);
+    return value;
+}
+
+_SOKOL_PRIVATE GLXFBConfig _sapp_glx_choosefbconfig() {
+    GLXFBConfig* native_configs;
+    _sapp_gl_fbconfig* usable_configs;
+    const _sapp_gl_fbconfig* closest;
+    int i, native_count, usable_count;
+    const char* vendor;
+    bool trust_window_bit = true;
+
+    /* HACK: This is a (hopefully temporary) workaround for Chromium
+           (VirtualBox GL) not setting the window bit on any GLXFBConfigs
+    */
+    vendor = _sapp.glx.GetClientString(_sapp.x11.display, GLX_VENDOR);
+    if (vendor && strcmp(vendor, "Chromium") == 0) {
+        trust_window_bit = false;
+    }
+
+    native_configs = _sapp.glx.GetFBConfigs(_sapp.x11.display, _sapp.x11.screen, &native_count);
+    if (!native_configs || !native_count) {
+        _sapp_fail("GLX: No GLXFBConfigs returned");
+    }
+
+    usable_configs = (_sapp_gl_fbconfig*) SOKOL_CALLOC((size_t)native_count, sizeof(_sapp_gl_fbconfig));
+    usable_count = 0;
+    for (i = 0;  i < native_count;  i++) {
+        const GLXFBConfig n = native_configs[i];
+        _sapp_gl_fbconfig* u = usable_configs + usable_count;
+        _sapp_gl_init_fbconfig(u);
+
+        /* Only consider RGBA GLXFBConfigs */
+        if (0 == (_sapp_glx_attrib(n, GLX_RENDER_TYPE) & GLX_RGBA_BIT)) {
+            continue;
+        }
+        /* Only consider window GLXFBConfigs */
+        if (0 == (_sapp_glx_attrib(n, GLX_DRAWABLE_TYPE) & GLX_WINDOW_BIT)) {
+            if (trust_window_bit) {
+                continue;
+            }
+        }
+        u->red_bits = _sapp_glx_attrib(n, GLX_RED_SIZE);
+        u->green_bits = _sapp_glx_attrib(n, GLX_GREEN_SIZE);
+        u->blue_bits = _sapp_glx_attrib(n, GLX_BLUE_SIZE);
+        u->alpha_bits = _sapp_glx_attrib(n, GLX_ALPHA_SIZE);
+        u->depth_bits = _sapp_glx_attrib(n, GLX_DEPTH_SIZE);
+        u->stencil_bits = _sapp_glx_attrib(n, GLX_STENCIL_SIZE);
+        if (_sapp_glx_attrib(n, GLX_DOUBLEBUFFER)) {
+            u->doublebuffer = true;
+        }
+        if (_sapp.glx.ARB_multisample) {
+            u->samples = _sapp_glx_attrib(n, GLX_SAMPLES);
+        }
+        u->handle = (uintptr_t) n;
+        usable_count++;
+    }
+    _sapp_gl_fbconfig desired;
+    _sapp_gl_init_fbconfig(&desired);
+    desired.red_bits = 8;
+    desired.green_bits = 8;
+    desired.blue_bits = 8;
+    desired.alpha_bits = 8;
+    desired.depth_bits = 24;
+    desired.stencil_bits = 8;
+    desired.doublebuffer = true;
+    desired.samples = _sapp.sample_count > 1 ? _sapp.sample_count : 0;
+    closest = _sapp_gl_choose_fbconfig(&desired, usable_configs, usable_count);
+    GLXFBConfig result = 0;
+    if (closest) {
+        result = (GLXFBConfig) closest->handle;
+    }
+    XFree(native_configs);
+    SOKOL_FREE(usable_configs);
+    return result;
+}
+
+_SOKOL_PRIVATE void _sapp_glx_choose_visual(Visual** visual, int* depth) {
+    GLXFBConfig native = _sapp_glx_choosefbconfig();
+    if (0 == native) {
+        _sapp_fail("GLX: Failed to find a suitable GLXFBConfig");
+    }
+    XVisualInfo* result = _sapp.glx.GetVisualFromFBConfig(_sapp.x11.display, native);
+    if (!result) {
+        _sapp_fail("GLX: Failed to retrieve Visual for GLXFBConfig");
+    }
+    *visual = result->visual;
+    *depth = result->depth;
+    XFree(result);
+}
+
+_SOKOL_PRIVATE void _sapp_glx_create_context(void) {
+    GLXFBConfig native = _sapp_glx_choosefbconfig();
+    if (0 == native){
+        _sapp_fail("GLX: Failed to find a suitable GLXFBConfig (2)");
+    }
+    if (!(_sapp.glx.ARB_create_context && _sapp.glx.ARB_create_context_profile)) {
+        _sapp_fail("GLX: ARB_create_context and ARB_create_context_profile required");
+    }
+    _sapp_x11_grab_error_handler();
+    const int attribs[] = {
+        GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
+        GLX_CONTEXT_MINOR_VERSION_ARB, 3,
+        GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
+        GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
+        0, 0
+    };
+    _sapp.glx.ctx = _sapp.glx.CreateContextAttribsARB(_sapp.x11.display, native, NULL, True, attribs);
+    if (!_sapp.glx.ctx) {
+        _sapp_fail("GLX: failed to create GL context");
+    }
+    _sapp_x11_release_error_handler();
+    _sapp.glx.window = _sapp.glx.CreateWindow(_sapp.x11.display, native, _sapp.x11.window, NULL);
+    if (!_sapp.glx.window) {
+        _sapp_fail("GLX: failed to create window");
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_glx_destroy_context(void) {
+    if (_sapp.glx.window) {
+        _sapp.glx.DestroyWindow(_sapp.x11.display, _sapp.glx.window);
+        _sapp.glx.window = 0;
+    }
+    if (_sapp.glx.ctx) {
+        _sapp.glx.DestroyContext(_sapp.x11.display, _sapp.glx.ctx);
+        _sapp.glx.ctx = 0;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_glx_make_current(void) {
+    _sapp.glx.MakeCurrent(_sapp.x11.display, _sapp.glx.window, _sapp.glx.ctx);
+}
+
+_SOKOL_PRIVATE void _sapp_glx_swap_buffers(void) {
+    _sapp.glx.SwapBuffers(_sapp.x11.display, _sapp.glx.window);
+}
+
+_SOKOL_PRIVATE void _sapp_glx_swapinterval(int interval) {
+    _sapp_glx_make_current();
+    if (_sapp.glx.EXT_swap_control) {
+        _sapp.glx.SwapIntervalEXT(_sapp.x11.display, _sapp.glx.window, interval);
+    }
+    else if (_sapp.glx.MESA_swap_control) {
+        _sapp.glx.SwapIntervalMESA(interval);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_send_event(Atom type, int a, int b, int c, int d, int e) {
+    XEvent event;
+    memset(&event, 0, sizeof(event));
+
+    event.type = ClientMessage;
+    event.xclient.window = _sapp.x11.window;
+    event.xclient.format = 32;
+    event.xclient.message_type = type;
+    event.xclient.data.l[0] = a;
+    event.xclient.data.l[1] = b;
+    event.xclient.data.l[2] = c;
+    event.xclient.data.l[3] = d;
+    event.xclient.data.l[4] = e;
+
+    XSendEvent(_sapp.x11.display, _sapp.x11.root,
+               False,
+               SubstructureNotifyMask | SubstructureRedirectMask,
+               &event);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_query_window_size(void) {
+    XWindowAttributes attribs;
+    XGetWindowAttributes(_sapp.x11.display, _sapp.x11.window, &attribs);
+    _sapp.window_width = attribs.width;
+    _sapp.window_height = attribs.height;
+    _sapp.framebuffer_width = _sapp.window_width;
+    _sapp.framebuffer_height = _sapp.window_height;
+}
+
+_SOKOL_PRIVATE void _sapp_x11_set_fullscreen(bool enable) {
+    /* NOTE: this function must be called after XMapWindow (which happens in _sapp_x11_show_window()) */
+    if (_sapp.x11.NET_WM_STATE && _sapp.x11.NET_WM_STATE_FULLSCREEN) {
+        if (enable) {
+            const int _NET_WM_STATE_ADD = 1;
+            _sapp_x11_send_event(_sapp.x11.NET_WM_STATE,
+                                _NET_WM_STATE_ADD,
+                                _sapp.x11.NET_WM_STATE_FULLSCREEN,
+                                0, 1, 0);
+        }
+        else {
+            const int _NET_WM_STATE_REMOVE = 0;
+            _sapp_x11_send_event(_sapp.x11.NET_WM_STATE,
+                                _NET_WM_STATE_REMOVE,
+                                _sapp.x11.NET_WM_STATE_FULLSCREEN,
+                                0, 1, 0);
+        }
+    }
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_create_hidden_cursor(void) {
+    SOKOL_ASSERT(0 == _sapp.x11.hidden_cursor);
+    const int w = 16;
+    const int h = 16;
+    XcursorImage* img = XcursorImageCreate(w, h);
+    SOKOL_ASSERT(img && (img->width == 16) && (img->height == 16) && img->pixels);
+    img->xhot = 0;
+    img->yhot = 0;
+    const size_t num_bytes = (size_t)(w * h) * sizeof(XcursorPixel);
+    memset(img->pixels, 0, num_bytes);
+    _sapp.x11.hidden_cursor = XcursorImageLoadCursor(_sapp.x11.display, img);
+    XcursorImageDestroy(img);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_toggle_fullscreen(void) {
+    _sapp.fullscreen = !_sapp.fullscreen;
+    _sapp_x11_set_fullscreen(_sapp.fullscreen);
+    _sapp_x11_query_window_size();
+}
+
+_SOKOL_PRIVATE void _sapp_x11_show_mouse(bool show) {
+    if (show) {
+        XUndefineCursor(_sapp.x11.display, _sapp.x11.window);
+    }
+    else {
+        XDefineCursor(_sapp.x11.display, _sapp.x11.window, _sapp.x11.hidden_cursor);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_lock_mouse(bool lock) {
+    if (lock == _sapp.mouse.locked) {
+        return;
+    }
+    _sapp.mouse.dx = 0.0f;
+    _sapp.mouse.dy = 0.0f;
+    _sapp.mouse.locked = lock;
+    if (_sapp.mouse.locked) {
+        if (_sapp.x11.xi.available) {
+            XIEventMask em;
+            unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 }; // XIMaskLen is a macro
+            em.deviceid = XIAllMasterDevices;
+            em.mask_len = sizeof(mask);
+            em.mask = mask;
+            XISetMask(mask, XI_RawMotion);
+            XISelectEvents(_sapp.x11.display, _sapp.x11.root, &em, 1);
+        }
+        XGrabPointer(_sapp.x11.display, // display
+            _sapp.x11.window,           // grab_window
+            True,                       // owner_events
+            ButtonPressMask | ButtonReleaseMask | PointerMotionMask,    // event_mask
+            GrabModeAsync,              // pointer_mode
+            GrabModeAsync,              // keyboard_mode
+            _sapp.x11.window,           // confine_to
+            _sapp.x11.hidden_cursor,    // cursor
+            CurrentTime);               // time
+    }
+    else {
+        if (_sapp.x11.xi.available) {
+            XIEventMask em;
+            unsigned char mask[] = { 0 };
+            em.deviceid = XIAllMasterDevices;
+            em.mask_len = sizeof(mask);
+            em.mask = mask;
+            XISelectEvents(_sapp.x11.display, _sapp.x11.root, &em, 1);
+        }
+        XWarpPointer(_sapp.x11.display, None, _sapp.x11.window, 0, 0, 0, 0, (int) _sapp.mouse.x, _sapp.mouse.y);
+        XUngrabPointer(_sapp.x11.display, CurrentTime);
+    }
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_update_window_title(void) {
+    Xutf8SetWMProperties(_sapp.x11.display,
+        _sapp.x11.window,
+        _sapp.window_title, _sapp.window_title,
+        NULL, 0, NULL, NULL, NULL);
+    XChangeProperty(_sapp.x11.display, _sapp.x11.window,
+        _sapp.x11.NET_WM_NAME, _sapp.x11.UTF8_STRING, 8,
+        PropModeReplace,
+        (unsigned char*)_sapp.window_title,
+        strlen(_sapp.window_title));
+    XChangeProperty(_sapp.x11.display, _sapp.x11.window,
+        _sapp.x11.NET_WM_ICON_NAME, _sapp.x11.UTF8_STRING, 8,
+        PropModeReplace,
+        (unsigned char*)_sapp.window_title,
+        strlen(_sapp.window_title));
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_set_icon(const sapp_icon_desc* icon_desc, int num_images) {
+    SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES));
+    int long_count = 0;
+    for (int i = 0; i < num_images; i++) {
+        const sapp_image_desc* img_desc = &icon_desc->images[i];
+        long_count += 2 + (img_desc->width * img_desc->height);
+    }
+    long* icon_data = (long*) SOKOL_CALLOC((size_t)long_count, sizeof(long));
+    SOKOL_ASSERT(icon_data);
+    long* dst = icon_data;
+    for (int img_index = 0; img_index < num_images; img_index++) {
+        const sapp_image_desc* img_desc = &icon_desc->images[img_index];
+        const uint8_t* src = (const uint8_t*) img_desc->pixels.ptr;
+        *dst++ = img_desc->width;
+        *dst++ = img_desc->height;
+        const int num_pixels = img_desc->width * img_desc->height;
+        for (int pixel_index = 0; pixel_index < num_pixels; pixel_index++) {
+            *dst++ = ((long)(src[pixel_index * 4 + 0]) << 16) |
+                     ((long)(src[pixel_index * 4 + 1]) << 8) |
+                     ((long)(src[pixel_index * 4 + 2]) << 0) |
+                     ((long)(src[pixel_index * 4 + 3]) << 24);
+        }
+    }
+    XChangeProperty(_sapp.x11.display, _sapp.x11.window,
+        _sapp.x11.NET_WM_ICON,
+        XA_CARDINAL, 32,
+        PropModeReplace,
+        (unsigned char*)icon_data,
+        long_count);
+    SOKOL_FREE(icon_data);
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE void _sapp_x11_create_window(Visual* visual, int depth) {
+    _sapp.x11.colormap = XCreateColormap(_sapp.x11.display, _sapp.x11.root, visual, AllocNone);
+    XSetWindowAttributes wa;
+    memset(&wa, 0, sizeof(wa));
+    const uint32_t wamask = CWBorderPixel | CWColormap | CWEventMask;
+    wa.colormap = _sapp.x11.colormap;
+    wa.border_pixel = 0;
+    wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask |
+                    PointerMotionMask | ButtonPressMask | ButtonReleaseMask |
+                    ExposureMask | FocusChangeMask | VisibilityChangeMask |
+                    EnterWindowMask | LeaveWindowMask | PropertyChangeMask;
+    _sapp_x11_grab_error_handler();
+    _sapp.x11.window = XCreateWindow(_sapp.x11.display,
+                                     _sapp.x11.root,
+                                     0, 0,
+                                     (uint32_t)_sapp.window_width,
+                                     (uint32_t)_sapp.window_height,
+                                     0,     /* border width */
+                                     depth, /* color depth */
+                                     InputOutput,
+                                     visual,
+                                     wamask,
+                                     &wa);
+    _sapp_x11_release_error_handler();
+    if (!_sapp.x11.window) {
+        _sapp_fail("X11: Failed to create window");
+    }
+    Atom protocols[] = {
+        _sapp.x11.WM_DELETE_WINDOW
+    };
+    XSetWMProtocols(_sapp.x11.display, _sapp.x11.window, protocols, 1);
+
+    XSizeHints* hints = XAllocSizeHints();
+    hints->flags |= PWinGravity;
+    hints->win_gravity = StaticGravity;
+    XSetWMNormalHints(_sapp.x11.display, _sapp.x11.window, hints);
+    XFree(hints);
+
+    /* announce support for drag'n'drop */
+    if (_sapp.drop.enabled) {
+        const Atom version = _SAPP_X11_XDND_VERSION;
+        XChangeProperty(_sapp.x11.display, _sapp.x11.window, _sapp.x11.xdnd.XdndAware, XA_ATOM, 32, PropModeReplace, (unsigned char*) &version, 1);
+    }
+
+    _sapp_x11_update_window_title();
+}
+
+_SOKOL_PRIVATE void _sapp_x11_destroy_window(void) {
+    if (_sapp.x11.window) {
+        XUnmapWindow(_sapp.x11.display, _sapp.x11.window);
+        XDestroyWindow(_sapp.x11.display, _sapp.x11.window);
+        _sapp.x11.window = 0;
+    }
+    if (_sapp.x11.colormap) {
+        XFreeColormap(_sapp.x11.display, _sapp.x11.colormap);
+        _sapp.x11.colormap = 0;
+    }
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE bool _sapp_x11_window_visible(void) {
+    XWindowAttributes wa;
+    XGetWindowAttributes(_sapp.x11.display, _sapp.x11.window, &wa);
+    return wa.map_state == IsViewable;
+}
+
+_SOKOL_PRIVATE void _sapp_x11_show_window(void) {
+    if (!_sapp_x11_window_visible()) {
+        XMapWindow(_sapp.x11.display, _sapp.x11.window);
+        XRaiseWindow(_sapp.x11.display, _sapp.x11.window);
+        XFlush(_sapp.x11.display);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_hide_window(void) {
+    XUnmapWindow(_sapp.x11.display, _sapp.x11.window);
+    XFlush(_sapp.x11.display);
+}
+
+_SOKOL_PRIVATE unsigned long _sapp_x11_get_window_property(Window window, Atom property, Atom type, unsigned char** value) {
+    Atom actualType;
+    int actualFormat;
+    unsigned long itemCount, bytesAfter;
+    XGetWindowProperty(_sapp.x11.display,
+                       window,
+                       property,
+                       0,
+                       LONG_MAX,
+                       False,
+                       type,
+                       &actualType,
+                       &actualFormat,
+                       &itemCount,
+                       &bytesAfter,
+                       value);
+    return itemCount;
+}
+
+_SOKOL_PRIVATE int _sapp_x11_get_window_state(void) {
+    int result = WithdrawnState;
+    struct {
+        CARD32 state;
+        Window icon;
+    } *state = NULL;
+
+    if (_sapp_x11_get_window_property(_sapp.x11.window, _sapp.x11.WM_STATE, _sapp.x11.WM_STATE, (unsigned char**)&state) >= 2) {
+        result = (int)state->state;
+    }
+    if (state) {
+        XFree(state);
+    }
+    return result;
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_x11_key_modifier_bit(sapp_keycode key) {
+    switch (key) {
+        case SAPP_KEYCODE_LEFT_SHIFT:
+        case SAPP_KEYCODE_RIGHT_SHIFT:
+            return SAPP_MODIFIER_SHIFT;
+        case SAPP_KEYCODE_LEFT_CONTROL:
+        case SAPP_KEYCODE_RIGHT_CONTROL:
+            return SAPP_MODIFIER_CTRL;
+        case SAPP_KEYCODE_LEFT_ALT:
+        case SAPP_KEYCODE_RIGHT_ALT:
+            return SAPP_MODIFIER_ALT;
+        case SAPP_KEYCODE_LEFT_SUPER:
+        case SAPP_KEYCODE_RIGHT_SUPER:
+            return SAPP_MODIFIER_SUPER;
+        default:
+            return 0;
+    }
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_x11_button_modifier_bit(sapp_mousebutton btn) {
+    switch (btn) {
+        case SAPP_MOUSEBUTTON_LEFT:     return SAPP_MODIFIER_LMB;
+        case SAPP_MOUSEBUTTON_RIGHT:    return SAPP_MODIFIER_RMB;
+        case SAPP_MOUSEBUTTON_MIDDLE:   return SAPP_MODIFIER_MMB;
+        default: return 0;
+    }
+}
+
+_SOKOL_PRIVATE uint32_t _sapp_x11_mods(uint32_t x11_mods) {
+    uint32_t mods = 0;
+    if (x11_mods & ShiftMask) {
+        mods |= SAPP_MODIFIER_SHIFT;
+    }
+    if (x11_mods & ControlMask) {
+        mods |= SAPP_MODIFIER_CTRL;
+    }
+    if (x11_mods & Mod1Mask) {
+        mods |= SAPP_MODIFIER_ALT;
+    }
+    if (x11_mods & Mod4Mask) {
+        mods |= SAPP_MODIFIER_SUPER;
+    }
+    if (x11_mods & Button1Mask) {
+        mods |= SAPP_MODIFIER_LMB;
+    }
+    if (x11_mods & Button2Mask) {
+        mods |= SAPP_MODIFIER_MMB;
+    }
+    if (x11_mods & Button3Mask) {
+        mods |= SAPP_MODIFIER_RMB;
+    }
+    return mods;
+}
+
+_SOKOL_PRIVATE void _sapp_x11_app_event(sapp_event_type type) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE sapp_mousebutton _sapp_x11_translate_button(const XEvent* event) {
+    switch (event->xbutton.button) {
+        case Button1: return SAPP_MOUSEBUTTON_LEFT;
+        case Button2: return SAPP_MOUSEBUTTON_MIDDLE;
+        case Button3: return SAPP_MOUSEBUTTON_RIGHT;
+        default:      return SAPP_MOUSEBUTTON_INVALID;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_mouse_event(sapp_event_type type, sapp_mousebutton btn, uint32_t mods) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.mouse_button = btn;
+        _sapp.event.modifiers = mods;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_scroll_event(float x, float y, uint32_t mods) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL);
+        _sapp.event.modifiers = mods;
+        _sapp.event.scroll_x = x;
+        _sapp.event.scroll_y = y;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_key_event(sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mods) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(type);
+        _sapp.event.key_code = key;
+        _sapp.event.key_repeat = repeat;
+        _sapp.event.modifiers = mods;
+        _sapp_call_event(&_sapp.event);
+        /* check if a CLIPBOARD_PASTED event must be sent too */
+        if (_sapp.clipboard.enabled &&
+            (type == SAPP_EVENTTYPE_KEY_DOWN) &&
+            (_sapp.event.modifiers == SAPP_MODIFIER_CTRL) &&
+            (_sapp.event.key_code == SAPP_KEYCODE_V))
+        {
+            _sapp_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED);
+            _sapp_call_event(&_sapp.event);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_x11_char_event(uint32_t chr, bool repeat, uint32_t mods) {
+    if (_sapp_events_enabled()) {
+        _sapp_init_event(SAPP_EVENTTYPE_CHAR);
+        _sapp.event.char_code = chr;
+        _sapp.event.key_repeat = repeat;
+        _sapp.event.modifiers = mods;
+        _sapp_call_event(&_sapp.event);
+    }
+}
+
+_SOKOL_PRIVATE sapp_keycode _sapp_x11_translate_key(int scancode) {
+    int dummy;
+    KeySym* keysyms = XGetKeyboardMapping(_sapp.x11.display, scancode, 1, &dummy);
+    SOKOL_ASSERT(keysyms);
+    KeySym keysym = keysyms[0];
+    XFree(keysyms);
+    switch (keysym) {
+        case XK_Escape:         return SAPP_KEYCODE_ESCAPE;
+        case XK_Tab:            return SAPP_KEYCODE_TAB;
+        case XK_Shift_L:        return SAPP_KEYCODE_LEFT_SHIFT;
+        case XK_Shift_R:        return SAPP_KEYCODE_RIGHT_SHIFT;
+        case XK_Control_L:      return SAPP_KEYCODE_LEFT_CONTROL;
+        case XK_Control_R:      return SAPP_KEYCODE_RIGHT_CONTROL;
+        case XK_Meta_L:
+        case XK_Alt_L:          return SAPP_KEYCODE_LEFT_ALT;
+        case XK_Mode_switch:    /* Mapped to Alt_R on many keyboards */
+        case XK_ISO_Level3_Shift: /* AltGr on at least some machines */
+        case XK_Meta_R:
+        case XK_Alt_R:          return SAPP_KEYCODE_RIGHT_ALT;
+        case XK_Super_L:        return SAPP_KEYCODE_LEFT_SUPER;
+        case XK_Super_R:        return SAPP_KEYCODE_RIGHT_SUPER;
+        case XK_Menu:           return SAPP_KEYCODE_MENU;
+        case XK_Num_Lock:       return SAPP_KEYCODE_NUM_LOCK;
+        case XK_Caps_Lock:      return SAPP_KEYCODE_CAPS_LOCK;
+        case XK_Print:          return SAPP_KEYCODE_PRINT_SCREEN;
+        case XK_Scroll_Lock:    return SAPP_KEYCODE_SCROLL_LOCK;
+        case XK_Pause:          return SAPP_KEYCODE_PAUSE;
+        case XK_Delete:         return SAPP_KEYCODE_DELETE;
+        case XK_BackSpace:      return SAPP_KEYCODE_BACKSPACE;
+        case XK_Return:         return SAPP_KEYCODE_ENTER;
+        case XK_Home:           return SAPP_KEYCODE_HOME;
+        case XK_End:            return SAPP_KEYCODE_END;
+        case XK_Page_Up:        return SAPP_KEYCODE_PAGE_UP;
+        case XK_Page_Down:      return SAPP_KEYCODE_PAGE_DOWN;
+        case XK_Insert:         return SAPP_KEYCODE_INSERT;
+        case XK_Left:           return SAPP_KEYCODE_LEFT;
+        case XK_Right:          return SAPP_KEYCODE_RIGHT;
+        case XK_Down:           return SAPP_KEYCODE_DOWN;
+        case XK_Up:             return SAPP_KEYCODE_UP;
+        case XK_F1:             return SAPP_KEYCODE_F1;
+        case XK_F2:             return SAPP_KEYCODE_F2;
+        case XK_F3:             return SAPP_KEYCODE_F3;
+        case XK_F4:             return SAPP_KEYCODE_F4;
+        case XK_F5:             return SAPP_KEYCODE_F5;
+        case XK_F6:             return SAPP_KEYCODE_F6;
+        case XK_F7:             return SAPP_KEYCODE_F7;
+        case XK_F8:             return SAPP_KEYCODE_F8;
+        case XK_F9:             return SAPP_KEYCODE_F9;
+        case XK_F10:            return SAPP_KEYCODE_F10;
+        case XK_F11:            return SAPP_KEYCODE_F11;
+        case XK_F12:            return SAPP_KEYCODE_F12;
+        case XK_F13:            return SAPP_KEYCODE_F13;
+        case XK_F14:            return SAPP_KEYCODE_F14;
+        case XK_F15:            return SAPP_KEYCODE_F15;
+        case XK_F16:            return SAPP_KEYCODE_F16;
+        case XK_F17:            return SAPP_KEYCODE_F17;
+        case XK_F18:            return SAPP_KEYCODE_F18;
+        case XK_F19:            return SAPP_KEYCODE_F19;
+        case XK_F20:            return SAPP_KEYCODE_F20;
+        case XK_F21:            return SAPP_KEYCODE_F21;
+        case XK_F22:            return SAPP_KEYCODE_F22;
+        case XK_F23:            return SAPP_KEYCODE_F23;
+        case XK_F24:            return SAPP_KEYCODE_F24;
+        case XK_F25:            return SAPP_KEYCODE_F25;
+
+        case XK_KP_Divide:      return SAPP_KEYCODE_KP_DIVIDE;
+        case XK_KP_Multiply:    return SAPP_KEYCODE_KP_MULTIPLY;
+        case XK_KP_Subtract:    return SAPP_KEYCODE_KP_SUBTRACT;
+        case XK_KP_Add:         return SAPP_KEYCODE_KP_ADD;
+
+        case XK_KP_Insert:      return SAPP_KEYCODE_KP_0;
+        case XK_KP_End:         return SAPP_KEYCODE_KP_1;
+        case XK_KP_Down:        return SAPP_KEYCODE_KP_2;
+        case XK_KP_Page_Down:   return SAPP_KEYCODE_KP_3;
+        case XK_KP_Left:        return SAPP_KEYCODE_KP_4;
+        case XK_KP_Begin:       return SAPP_KEYCODE_KP_5;
+        case XK_KP_Right:       return SAPP_KEYCODE_KP_6;
+        case XK_KP_Home:        return SAPP_KEYCODE_KP_7;
+        case XK_KP_Up:          return SAPP_KEYCODE_KP_8;
+        case XK_KP_Page_Up:     return SAPP_KEYCODE_KP_9;
+        case XK_KP_Delete:      return SAPP_KEYCODE_KP_DECIMAL;
+        case XK_KP_Equal:       return SAPP_KEYCODE_KP_EQUAL;
+        case XK_KP_Enter:       return SAPP_KEYCODE_KP_ENTER;
+
+        case XK_a:              return SAPP_KEYCODE_A;
+        case XK_b:              return SAPP_KEYCODE_B;
+        case XK_c:              return SAPP_KEYCODE_C;
+        case XK_d:              return SAPP_KEYCODE_D;
+        case XK_e:              return SAPP_KEYCODE_E;
+        case XK_f:              return SAPP_KEYCODE_F;
+        case XK_g:              return SAPP_KEYCODE_G;
+        case XK_h:              return SAPP_KEYCODE_H;
+        case XK_i:              return SAPP_KEYCODE_I;
+        case XK_j:              return SAPP_KEYCODE_J;
+        case XK_k:              return SAPP_KEYCODE_K;
+        case XK_l:              return SAPP_KEYCODE_L;
+        case XK_m:              return SAPP_KEYCODE_M;
+        case XK_n:              return SAPP_KEYCODE_N;
+        case XK_o:              return SAPP_KEYCODE_O;
+        case XK_p:              return SAPP_KEYCODE_P;
+        case XK_q:              return SAPP_KEYCODE_Q;
+        case XK_r:              return SAPP_KEYCODE_R;
+        case XK_s:              return SAPP_KEYCODE_S;
+        case XK_t:              return SAPP_KEYCODE_T;
+        case XK_u:              return SAPP_KEYCODE_U;
+        case XK_v:              return SAPP_KEYCODE_V;
+        case XK_w:              return SAPP_KEYCODE_W;
+        case XK_x:              return SAPP_KEYCODE_X;
+        case XK_y:              return SAPP_KEYCODE_Y;
+        case XK_z:              return SAPP_KEYCODE_Z;
+        case XK_1:              return SAPP_KEYCODE_1;
+        case XK_2:              return SAPP_KEYCODE_2;
+        case XK_3:              return SAPP_KEYCODE_3;
+        case XK_4:              return SAPP_KEYCODE_4;
+        case XK_5:              return SAPP_KEYCODE_5;
+        case XK_6:              return SAPP_KEYCODE_6;
+        case XK_7:              return SAPP_KEYCODE_7;
+        case XK_8:              return SAPP_KEYCODE_8;
+        case XK_9:              return SAPP_KEYCODE_9;
+        case XK_0:              return SAPP_KEYCODE_0;
+        case XK_space:          return SAPP_KEYCODE_SPACE;
+        case XK_minus:          return SAPP_KEYCODE_MINUS;
+        case XK_equal:          return SAPP_KEYCODE_EQUAL;
+        case XK_bracketleft:    return SAPP_KEYCODE_LEFT_BRACKET;
+        case XK_bracketright:   return SAPP_KEYCODE_RIGHT_BRACKET;
+        case XK_backslash:      return SAPP_KEYCODE_BACKSLASH;
+        case XK_semicolon:      return SAPP_KEYCODE_SEMICOLON;
+        case XK_apostrophe:     return SAPP_KEYCODE_APOSTROPHE;
+        case XK_grave:          return SAPP_KEYCODE_GRAVE_ACCENT;
+        case XK_comma:          return SAPP_KEYCODE_COMMA;
+        case XK_period:         return SAPP_KEYCODE_PERIOD;
+        case XK_slash:          return SAPP_KEYCODE_SLASH;
+        case XK_less:           return SAPP_KEYCODE_WORLD_1; /* At least in some layouts... */
+        default:                return SAPP_KEYCODE_INVALID;
+    }
+}
+
+_SOKOL_PRIVATE int32_t _sapp_x11_keysym_to_unicode(KeySym keysym) {
+    int min = 0;
+    int max = sizeof(_sapp_x11_keysymtab) / sizeof(struct _sapp_x11_codepair) - 1;
+    int mid;
+
+    /* First check for Latin-1 characters (1:1 mapping) */
+    if ((keysym >= 0x0020 && keysym <= 0x007e) ||
+        (keysym >= 0x00a0 && keysym <= 0x00ff))
+    {
+        return keysym;
+    }
+
+    /* Also check for directly encoded 24-bit UCS characters */
+    if ((keysym & 0xff000000) == 0x01000000) {
+        return keysym & 0x00ffffff;
+    }
+
+    /* Binary search in table */
+    while (max >= min) {
+        mid = (min + max) / 2;
+        if (_sapp_x11_keysymtab[mid].keysym < keysym) {
+            min = mid + 1;
+        }
+        else if (_sapp_x11_keysymtab[mid].keysym > keysym) {
+            max = mid - 1;
+        }
+        else {
+            return _sapp_x11_keysymtab[mid].ucs;
+        }
+    }
+
+    /* No matching Unicode value found */
+    return -1;
+}
+
+_SOKOL_PRIVATE bool _sapp_x11_parse_dropped_files_list(const char* src) {
+    SOKOL_ASSERT(src);
+    SOKOL_ASSERT(_sapp.drop.buffer);
+
+    _sapp_clear_drop_buffer();
+    _sapp.drop.num_files = 0;
+
+    /*
+        src is (potentially percent-encoded) string made of one or multiple paths
+        separated by \r\n, each path starting with 'file://'
+    */
+    bool err = false;
+    int src_count = 0;
+    char src_chr = 0;
+    char* dst_ptr = _sapp.drop.buffer;
+    const char* dst_end_ptr = dst_ptr + (_sapp.drop.max_path_length - 1); // room for terminating 0
+    while (0 != (src_chr = *src++)) {
+        src_count++;
+        char dst_chr = 0;
+        /* check leading 'file://' */
+        if (src_count <= 7) {
+            if (((src_count == 1) && (src_chr != 'f')) ||
+                ((src_count == 2) && (src_chr != 'i')) ||
+                ((src_count == 3) && (src_chr != 'l')) ||
+                ((src_count == 4) && (src_chr != 'e')) ||
+                ((src_count == 5) && (src_chr != ':')) ||
+                ((src_count == 6) && (src_chr != '/')) ||
+                ((src_count == 7) && (src_chr != '/')))
+            {
+                SOKOL_LOG("sokol_app.h: dropped file URI doesn't start with file://");
+                err = true;
+                break;
+            }
+        }
+        else if (src_chr == '\r') {
+            // skip
+        }
+        else if (src_chr == '\n') {
+            src_chr = 0;
+            src_count = 0;
+            _sapp.drop.num_files++;
+            // too many files is not an error
+            if (_sapp.drop.num_files >= _sapp.drop.max_files) {
+                break;
+            }
+            dst_ptr = _sapp.drop.buffer + _sapp.drop.num_files * _sapp.drop.max_path_length;
+            dst_end_ptr = dst_ptr + (_sapp.drop.max_path_length - 1);
+        }
+        else if ((src_chr == '%') && src[0] && src[1]) {
+            // a percent-encoded byte (most like UTF-8 multibyte sequence)
+            const char digits[3] = { src[0], src[1], 0 };
+            src += 2;
+            dst_chr = (char) strtol(digits, 0, 16);
+        }
+        else {
+            dst_chr = src_chr;
+        }
+        if (dst_chr) {
+            // dst_end_ptr already has adjustment for terminating zero
+            if (dst_ptr < dst_end_ptr) {
+                *dst_ptr++ = dst_chr;
+            }
+            else {
+                SOKOL_LOG("sokol_app.h: dropped file path too long (sapp_desc.max_dropped_file_path_length)");
+                err = true;
+                break;
+            }
+        }
+    }
+    if (err) {
+        _sapp_clear_drop_buffer();
+        _sapp.drop.num_files = 0;
+        return false;
+    }
+    else {
+        return true;
+    }
+}
+
+// XLib manual says keycodes are in the range [8, 255] inclusive.
+// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html
+static bool _sapp_x11_keycodes[256];
+
+_SOKOL_PRIVATE void _sapp_x11_process_event(XEvent* event) {
+    Bool filtered = XFilterEvent(event, None);
+    switch (event->type) {
+        case GenericEvent:
+            if (_sapp.mouse.locked && _sapp.x11.xi.available) {
+                if (event->xcookie.extension == _sapp.x11.xi.major_opcode) {
+                    if (XGetEventData(_sapp.x11.display, &event->xcookie)) {
+                        if (event->xcookie.evtype == XI_RawMotion) {
+                            XIRawEvent* re = (XIRawEvent*) event->xcookie.data;
+                            if (re->valuators.mask_len) {
+                                const double* values = re->raw_values;
+                                if (XIMaskIsSet(re->valuators.mask, 0)) {
+                                    _sapp.mouse.dx = (float) *values;
+                                    values++;
+                                }
+                                if (XIMaskIsSet(re->valuators.mask, 1)) {
+                                    _sapp.mouse.dy = (float) *values;
+                                }
+                                _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xmotion.state));
+                            }
+                        }
+                        XFreeEventData(_sapp.x11.display, &event->xcookie);
+                    }
+                }
+            }
+            break;
+        case FocusOut:
+            /* if focus is lost for any reason, and we're in mouse locked mode, disable mouse lock */
+            if (_sapp.mouse.locked) {
+                _sapp_x11_lock_mouse(false);
+            }
+            break;
+        case KeyPress:
+            {
+                int keycode = (int)event->xkey.keycode;
+                const sapp_keycode key = _sapp_x11_translate_key(keycode);
+                bool repeat = _sapp_x11_keycodes[keycode & 0xFF];
+                _sapp_x11_keycodes[keycode & 0xFF] = true;
+                uint32_t mods = _sapp_x11_mods(event->xkey.state);
+                // X11 doesn't set modifier bit on key down, so emulate that
+                mods |= _sapp_x11_key_modifier_bit(key);
+                if (key != SAPP_KEYCODE_INVALID) {
+                    _sapp_x11_key_event(SAPP_EVENTTYPE_KEY_DOWN, key, repeat, mods);
+                }
+                KeySym keysym;
+                XLookupString(&event->xkey, NULL, 0, &keysym, NULL);
+                int32_t chr = _sapp_x11_keysym_to_unicode(keysym);
+                if (chr > 0) {
+                    _sapp_x11_char_event((uint32_t)chr, repeat, mods);
+                }
+            }
+            break;
+        case KeyRelease:
+            {
+                int keycode = (int)event->xkey.keycode;
+                const sapp_keycode key = _sapp_x11_translate_key(keycode);
+                _sapp_x11_keycodes[keycode & 0xFF] = false;
+                if (key != SAPP_KEYCODE_INVALID) {
+                    uint32_t mods = _sapp_x11_mods(event->xkey.state);
+                    // X11 doesn't clear modifier bit on key up, so emulate that
+                    mods &= ~_sapp_x11_key_modifier_bit(key);
+                    _sapp_x11_key_event(SAPP_EVENTTYPE_KEY_UP, key, false, mods);
+                }
+            }
+            break;
+        case ButtonPress:
+            {
+                const sapp_mousebutton btn = _sapp_x11_translate_button(event);
+                uint32_t mods = _sapp_x11_mods(event->xbutton.state);
+                // X11 doesn't set modifier bit on button down, so emulate that
+                mods |= _sapp_x11_button_modifier_bit(btn);
+                if (btn != SAPP_MOUSEBUTTON_INVALID) {
+                    _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_DOWN, btn, mods);
+                    _sapp.x11.mouse_buttons |= (1 << btn);
+                }
+                else {
+                    /* might be a scroll event */
+                    switch (event->xbutton.button) {
+                        case 4: _sapp_x11_scroll_event(0.0f, 1.0f, mods); break;
+                        case 5: _sapp_x11_scroll_event(0.0f, -1.0f, mods); break;
+                        case 6: _sapp_x11_scroll_event(1.0f, 0.0f, mods); break;
+                        case 7: _sapp_x11_scroll_event(-1.0f, 0.0f, mods); break;
+                    }
+                }
+            }
+            break;
+        case ButtonRelease:
+            {
+                const sapp_mousebutton btn = _sapp_x11_translate_button(event);
+                if (btn != SAPP_MOUSEBUTTON_INVALID) {
+                    uint32_t mods = _sapp_x11_mods(event->xbutton.state);
+                    // X11 doesn't clear modifier bit on button up, so emulate that
+                    mods &= ~_sapp_x11_button_modifier_bit(btn);
+                    _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_UP, btn, mods);
+                    _sapp.x11.mouse_buttons &= ~(1 << btn);
+                }
+            }
+            break;
+        case EnterNotify:
+            /* don't send enter/leave events while mouse button held down */
+            if (0 == _sapp.x11.mouse_buttons) {
+                _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xcrossing.state));
+            }
+            break;
+        case LeaveNotify:
+            if (0 == _sapp.x11.mouse_buttons) {
+                _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xcrossing.state));
+            }
+            break;
+        case MotionNotify:
+            if (!_sapp.mouse.locked) {
+                const float new_x = (float) event->xmotion.x;
+                const float new_y = (float) event->xmotion.y;
+                if (_sapp.mouse.pos_valid) {
+                    _sapp.mouse.dx = new_x - _sapp.mouse.x;
+                    _sapp.mouse.dy = new_y - _sapp.mouse.y;
+                }
+                _sapp.mouse.x = new_x;
+                _sapp.mouse.y = new_y;
+                _sapp.mouse.pos_valid = true;
+                _sapp_x11_mouse_event(SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID, _sapp_x11_mods(event->xmotion.state));
+            }
+            break;
+        case ConfigureNotify:
+            if ((event->xconfigure.width != _sapp.window_width) || (event->xconfigure.height != _sapp.window_height)) {
+                _sapp.window_width = event->xconfigure.width;
+                _sapp.window_height = event->xconfigure.height;
+                _sapp.framebuffer_width = _sapp.window_width;
+                _sapp.framebuffer_height = _sapp.window_height;
+                _sapp_x11_app_event(SAPP_EVENTTYPE_RESIZED);
+            }
+            break;
+        case PropertyNotify:
+            if (event->xproperty.state == PropertyNewValue) {
+                if (event->xproperty.atom == _sapp.x11.WM_STATE) {
+                    const int state = _sapp_x11_get_window_state();
+                    if (state != _sapp.x11.window_state) {
+                        _sapp.x11.window_state = state;
+                        if (state == IconicState) {
+                            _sapp_x11_app_event(SAPP_EVENTTYPE_ICONIFIED);
+                        }
+                        else if (state == NormalState) {
+                            _sapp_x11_app_event(SAPP_EVENTTYPE_RESTORED);
+                        }
+                    }
+                }
+            }
+            break;
+        case ClientMessage:
+            if (filtered) {
+                return;
+            }
+            if (event->xclient.message_type == _sapp.x11.WM_PROTOCOLS) {
+                const Atom protocol = (Atom)event->xclient.data.l[0];
+                if (protocol == _sapp.x11.WM_DELETE_WINDOW) {
+                    _sapp.quit_requested = true;
+                }
+            }
+            else if (event->xclient.message_type == _sapp.x11.xdnd.XdndEnter) {
+                const bool is_list = 0 != (event->xclient.data.l[1] & 1);
+                _sapp.x11.xdnd.source  = (Window)event->xclient.data.l[0];
+                _sapp.x11.xdnd.version = event->xclient.data.l[1] >> 24;
+                _sapp.x11.xdnd.format  = None;
+                if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) {
+                    return;
+                }
+                uint32_t count = 0;
+                Atom* formats = 0;
+                if (is_list) {
+                    count = _sapp_x11_get_window_property(_sapp.x11.xdnd.source, _sapp.x11.xdnd.XdndTypeList, XA_ATOM, (unsigned char**)&formats);
+                }
+                else {
+                    count = 3;
+                    formats = (Atom*) event->xclient.data.l + 2;
+                }
+                for (uint32_t i = 0; i < count; i++) {
+                    if (formats[i] == _sapp.x11.xdnd.text_uri_list) {
+                        _sapp.x11.xdnd.format = _sapp.x11.xdnd.text_uri_list;
+                        break;
+                    }
+                }
+                if (is_list && formats) {
+                    XFree(formats);
+                }
+            }
+            else if (event->xclient.message_type == _sapp.x11.xdnd.XdndDrop) {
+                if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) {
+                    return;
+                }
+                Time time = CurrentTime;
+                if (_sapp.x11.xdnd.format) {
+                    if (_sapp.x11.xdnd.version >= 1) {
+                        time = (Time)event->xclient.data.l[2];
+                    }
+                    XConvertSelection(_sapp.x11.display,
+                                      _sapp.x11.xdnd.XdndSelection,
+                                      _sapp.x11.xdnd.format,
+                                      _sapp.x11.xdnd.XdndSelection,
+                                      _sapp.x11.window,
+                                      time);
+                }
+                else if (_sapp.x11.xdnd.version >= 2) {
+                    XEvent reply;
+                    memset(&reply, 0, sizeof(reply));
+                    reply.type = ClientMessage;
+                    reply.xclient.window = _sapp.x11.window;
+                    reply.xclient.message_type = _sapp.x11.xdnd.XdndFinished;
+                    reply.xclient.format = 32;
+                    reply.xclient.data.l[0] = (long)_sapp.x11.window;
+                    reply.xclient.data.l[1] = 0;    // drag was rejected
+                    reply.xclient.data.l[2] = None;
+                    XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply);
+                    XFlush(_sapp.x11.display);
+                }
+            }
+            else if (event->xclient.message_type == _sapp.x11.xdnd.XdndPosition) {
+                /* drag operation has moved over the window
+                   FIXME: we could track the mouse position here, but
+                   this isn't implemented on other platforms either so far
+                */
+                if (_sapp.x11.xdnd.version > _SAPP_X11_XDND_VERSION) {
+                    return;
+                }
+                XEvent reply;
+                memset(&reply, 0, sizeof(reply));
+                reply.type = ClientMessage;
+                reply.xclient.window = _sapp.x11.xdnd.source;
+                reply.xclient.message_type = _sapp.x11.xdnd.XdndStatus;
+                reply.xclient.format = 32;
+                reply.xclient.data.l[0] = (long)_sapp.x11.window;
+                if (_sapp.x11.xdnd.format) {
+                    /* reply that we are ready to copy the dragged data */
+                    reply.xclient.data.l[1] = 1;    // accept with no rectangle
+                    if (_sapp.x11.xdnd.version >= 2) {
+                        reply.xclient.data.l[4] = (long)_sapp.x11.xdnd.XdndActionCopy;
+                    }
+                }
+                XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply);
+                XFlush(_sapp.x11.display);
+            }
+            break;
+        case SelectionNotify:
+            if (event->xselection.property == _sapp.x11.xdnd.XdndSelection) {
+                char* data = 0;
+                uint32_t result = _sapp_x11_get_window_property(event->xselection.requestor,
+                                                                event->xselection.property,
+                                                                event->xselection.target,
+                                                                (unsigned char**) &data);
+                if (_sapp.drop.enabled && result) {
+                    if (_sapp_x11_parse_dropped_files_list(data)) {
+                        if (_sapp_events_enabled()) {
+                            _sapp_init_event(SAPP_EVENTTYPE_FILES_DROPPED);
+                            _sapp_call_event(&_sapp.event);
+                        }
+                    }
+                }
+                if (_sapp.x11.xdnd.version >= 2) {
+                    XEvent reply;
+                    memset(&reply, 0, sizeof(reply));
+                    reply.type = ClientMessage;
+                    reply.xclient.window = _sapp.x11.window;
+                    reply.xclient.message_type = _sapp.x11.xdnd.XdndFinished;
+                    reply.xclient.format = 32;
+                    reply.xclient.data.l[0] = (long)_sapp.x11.window;
+                    reply.xclient.data.l[1] = result;
+                    reply.xclient.data.l[2] = (long)_sapp.x11.xdnd.XdndActionCopy;
+                    XSendEvent(_sapp.x11.display, _sapp.x11.xdnd.source, False, NoEventMask, &reply);
+                    XFlush(_sapp.x11.display);
+                }
+            }
+            break;
+        case DestroyNotify:
+            break;
+    }
+}
+
+_SOKOL_PRIVATE void _sapp_linux_run(const sapp_desc* desc) {
+    /* The following lines are here to trigger a linker error instead of an
+        obscure runtime error if the user has forgotten to add -pthread to
+        the compiler or linker options. They have no other purpose.
+    */
+    pthread_attr_t pthread_attr;
+    pthread_attr_init(&pthread_attr);
+    pthread_attr_destroy(&pthread_attr);
+
+    _sapp_init_state(desc);
+    _sapp.x11.window_state = NormalState;
+
+    XInitThreads();
+    XrmInitialize();
+    _sapp.x11.display = XOpenDisplay(NULL);
+    if (!_sapp.x11.display) {
+        _sapp_fail("XOpenDisplay() failed!\n");
+    }
+    _sapp.x11.screen = DefaultScreen(_sapp.x11.display);
+    _sapp.x11.root = DefaultRootWindow(_sapp.x11.display);
+    XkbSetDetectableAutoRepeat(_sapp.x11.display, true, NULL);
+    _sapp_x11_query_system_dpi();
+    _sapp.dpi_scale = _sapp.x11.dpi / 96.0f;
+    _sapp_x11_init_extensions();
+    _sapp_x11_create_hidden_cursor();
+    _sapp_glx_init();
+    Visual* visual = 0;
+    int depth = 0;
+    _sapp_glx_choose_visual(&visual, &depth);
+    _sapp_x11_create_window(visual, depth);
+    _sapp_glx_create_context();
+    sapp_set_icon(&desc->icon);
+    _sapp.valid = true;
+    _sapp_x11_show_window();
+    if (_sapp.fullscreen) {
+        _sapp_x11_set_fullscreen(true);
+    }
+    _sapp_x11_query_window_size();
+    _sapp_glx_swapinterval(_sapp.swap_interval);
+    XFlush(_sapp.x11.display);
+    while (!_sapp.quit_ordered) {
+        _sapp_glx_make_current();
+        int count = XPending(_sapp.x11.display);
+        while (count--) {
+            XEvent event;
+            XNextEvent(_sapp.x11.display, &event);
+            _sapp_x11_process_event(&event);
+        }
+        _sapp_frame();
+        _sapp_glx_swap_buffers();
+        XFlush(_sapp.x11.display);
+        /* handle quit-requested, either from window or from sapp_request_quit() */
+        if (_sapp.quit_requested && !_sapp.quit_ordered) {
+            /* give user code a chance to intervene */
+            _sapp_x11_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED);
+            /* if user code hasn't intervened, quit the app */
+            if (_sapp.quit_requested) {
+                _sapp.quit_ordered = true;
+            }
+        }
+    }
+    _sapp_call_cleanup();
+    _sapp_glx_destroy_context();
+    _sapp_x11_destroy_window();
+    XCloseDisplay(_sapp.x11.display);
+    _sapp_discard_state();
+}
+
+#if !defined(SOKOL_NO_ENTRY)
+int main(int argc, char* argv[]) {
+    sapp_desc desc = sokol_main(argc, argv);
+    _sapp_linux_run(&desc);
+    return 0;
+}
+#endif /* SOKOL_NO_ENTRY */
+#endif /* _SAPP_LINUX */
+
+/*== PUBLIC API FUNCTIONS ====================================================*/
+#if defined(SOKOL_NO_ENTRY)
+SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) {
+    SOKOL_ASSERT(desc);
+    #if defined(_SAPP_MACOS)
+        _sapp_macos_run(desc);
+    #elif defined(_SAPP_IOS)
+        _sapp_ios_run(desc);
+    #elif defined(_SAPP_EMSCRIPTEN)
+        _sapp_emsc_run(desc);
+    #elif defined(_SAPP_WIN32)
+        _sapp_win32_run(desc);
+    #elif defined(_SAPP_UWP)
+        _sapp_uwp_run(desc);
+    #elif defined(_SAPP_LINUX)
+        _sapp_linux_run(desc);
+    #else
+        // calling sapp_run() directly is not supported on Android)
+        _sapp_fail("sapp_run() not supported on this platform!");
+    #endif
+}
+
+/* this is just a stub so the linker doesn't complain */
+sapp_desc sokol_main(int argc, char* argv[]) {
+    _SOKOL_UNUSED(argc);
+    _SOKOL_UNUSED(argv);
+    sapp_desc desc;
+    memset(&desc, 0, sizeof(desc));
+    return desc;
+}
+#else
+/* likewise, in normal mode, sapp_run() is just an empty stub */
+SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) {
+    _SOKOL_UNUSED(desc);
+}
+#endif
+
+SOKOL_API_IMPL bool sapp_isvalid(void) {
+    return _sapp.valid;
+}
+
+SOKOL_API_IMPL void* sapp_userdata(void) {
+    return _sapp.desc.user_data;
+}
+
+SOKOL_API_IMPL sapp_desc sapp_query_desc(void) {
+    return _sapp.desc;
+}
+
+SOKOL_API_IMPL uint64_t sapp_frame_count(void) {
+    return _sapp.frame_count;
+}
+
+SOKOL_API_IMPL int sapp_width(void) {
+    return (_sapp.framebuffer_width > 0) ? _sapp.framebuffer_width : 1;
+}
+
+SOKOL_API_IMPL float sapp_widthf(void) {
+    return (float)sapp_width();
+}
+
+SOKOL_API_IMPL int sapp_height(void) {
+    return (_sapp.framebuffer_height > 0) ? _sapp.framebuffer_height : 1;
+}
+
+SOKOL_API_IMPL float sapp_heightf(void) {
+    return (float)sapp_height();
+}
+
+SOKOL_API_IMPL int sapp_color_format(void) {
+    #if defined(_SAPP_EMSCRIPTEN) && defined(SOKOL_WGPU)
+        switch (_sapp.emsc.wgpu.render_format) {
+            case WGPUTextureFormat_RGBA8Unorm:
+                return _SAPP_PIXELFORMAT_RGBA8;
+            case WGPUTextureFormat_BGRA8Unorm:
+                return _SAPP_PIXELFORMAT_BGRA8;
+            default:
+                SOKOL_UNREACHABLE;
+                return 0;
+        }
+    #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11)
+        return _SAPP_PIXELFORMAT_BGRA8;
+    #else
+        return _SAPP_PIXELFORMAT_RGBA8;
+    #endif
+}
+
+SOKOL_API_IMPL int sapp_depth_format(void) {
+    return _SAPP_PIXELFORMAT_DEPTH_STENCIL;
+}
+
+SOKOL_API_IMPL int sapp_sample_count(void) {
+    return _sapp.sample_count;
+}
+
+SOKOL_API_IMPL bool sapp_high_dpi(void) {
+    return _sapp.desc.high_dpi && (_sapp.dpi_scale >= 1.5f);
+}
+
+SOKOL_API_IMPL float sapp_dpi_scale(void) {
+    return _sapp.dpi_scale;
+}
+
+SOKOL_API_IMPL bool sapp_gles2(void) {
+    return _sapp.gles2_fallback;
+}
+
+SOKOL_API_IMPL void sapp_show_keyboard(bool show) {
+    #if defined(_SAPP_IOS)
+    _sapp_ios_show_keyboard(show);
+    #elif defined(_SAPP_EMSCRIPTEN)
+    _sapp_emsc_show_keyboard(show);
+    #elif defined(_SAPP_ANDROID)
+    _sapp_android_show_keyboard(show);
+    #else
+    _SOKOL_UNUSED(show);
+    #endif
+}
+
+SOKOL_API_IMPL bool sapp_keyboard_shown(void) {
+    return _sapp.onscreen_keyboard_shown;
+}
+
+SOKOL_API_IMPL bool sapp_is_fullscreen(void) {
+    return _sapp.fullscreen;
+}
+
+SOKOL_API_IMPL void sapp_toggle_fullscreen(void) {
+    #if defined(_SAPP_MACOS)
+    _sapp_macos_toggle_fullscreen();
+    #elif defined(_SAPP_WIN32)
+    _sapp_win32_toggle_fullscreen();
+    #elif defined(_SAPP_UWP)
+    _sapp_uwp_toggle_fullscreen();
+    #elif defined(_SAPP_LINUX)
+    _sapp_x11_toggle_fullscreen();
+    #endif
+}
+
+/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */
+SOKOL_API_IMPL void sapp_show_mouse(bool show) {
+    if (_sapp.mouse.shown != show) {
+        #if defined(_SAPP_MACOS)
+        _sapp_macos_show_mouse(show);
+        #elif defined(_SAPP_WIN32)
+        _sapp_win32_show_mouse(show);
+        #elif defined(_SAPP_LINUX)
+        _sapp_x11_show_mouse(show);
+        #elif defined(_SAPP_UWP)
+        _sapp_uwp_show_mouse(show);
+        #endif
+        _sapp.mouse.shown = show;
+    }
+}
+
+SOKOL_API_IMPL bool sapp_mouse_shown(void) {
+    return _sapp.mouse.shown;
+}
+
+SOKOL_API_IMPL void sapp_lock_mouse(bool lock) {
+    #if defined(_SAPP_MACOS)
+    _sapp_macos_lock_mouse(lock);
+    #elif defined(_SAPP_EMSCRIPTEN)
+    _sapp_emsc_lock_mouse(lock);
+    #elif defined(_SAPP_WIN32)
+    _sapp_win32_lock_mouse(lock);
+    #elif defined(_SAPP_LINUX)
+    _sapp_x11_lock_mouse(lock);
+    #else
+    _sapp.mouse.locked = lock;
+    #endif
+}
+
+SOKOL_API_IMPL bool sapp_mouse_locked(void) {
+    return _sapp.mouse.locked;
+}
+
+SOKOL_API_IMPL void sapp_request_quit(void) {
+    _sapp.quit_requested = true;
+}
+
+SOKOL_API_IMPL void sapp_cancel_quit(void) {
+    _sapp.quit_requested = false;
+}
+
+SOKOL_API_IMPL void sapp_quit(void) {
+    _sapp.quit_ordered = true;
+}
+
+SOKOL_API_IMPL void sapp_consume_event(void) {
+    _sapp.event_consumed = true;
+}
+
+/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */
+SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) {
+    SOKOL_ASSERT(_sapp.clipboard.enabled);
+    if (!_sapp.clipboard.enabled) {
+        return;
+    }
+    SOKOL_ASSERT(str);
+    #if defined(_SAPP_MACOS)
+        _sapp_macos_set_clipboard_string(str);
+    #elif defined(_SAPP_EMSCRIPTEN)
+        _sapp_emsc_set_clipboard_string(str);
+    #elif defined(_SAPP_WIN32)
+        _sapp_win32_set_clipboard_string(str);
+    #else
+        /* not implemented */
+    #endif
+    _sapp_strcpy(str, _sapp.clipboard.buffer, _sapp.clipboard.buf_size);
+}
+
+SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) {
+    SOKOL_ASSERT(_sapp.clipboard.enabled);
+    if (!_sapp.clipboard.enabled) {
+        return "";
+    }
+    #if defined(_SAPP_MACOS)
+        return _sapp_macos_get_clipboard_string();
+    #elif defined(_SAPP_EMSCRIPTEN)
+        return _sapp.clipboard.buffer;
+    #elif defined(_SAPP_WIN32)
+        return _sapp_win32_get_clipboard_string();
+    #else
+        /* not implemented */
+        return _sapp.clipboard.buffer;
+    #endif
+}
+
+SOKOL_API_IMPL void sapp_set_window_title(const char* title) {
+    SOKOL_ASSERT(title);
+    _sapp_strcpy(title, _sapp.window_title, sizeof(_sapp.window_title));
+    #if defined(_SAPP_MACOS)
+        _sapp_macos_update_window_title();
+    #elif defined(_SAPP_WIN32)
+        _sapp_win32_update_window_title();
+    #elif defined(_SAPP_LINUX)
+        _sapp_x11_update_window_title();
+    #endif
+}
+
+SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) {
+    SOKOL_ASSERT(desc);
+    if (desc->sokol_default) {
+        if (0 == _sapp.default_icon_pixels) {
+            _sapp_setup_default_icon();
+        }
+        SOKOL_ASSERT(0 != _sapp.default_icon_pixels);
+        desc = &_sapp.default_icon_desc;
+    }
+    const int num_images = _sapp_icon_num_images(desc);
+    if (num_images == 0) {
+        return;
+    }
+    SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES));
+    if (!_sapp_validate_icon_desc(desc, num_images)) {
+        return;
+    }
+    #if defined(_SAPP_MACOS)
+        _sapp_macos_set_icon(desc, num_images);
+    #elif defined(_SAPP_WIN32)
+        _sapp_win32_set_icon(desc, num_images);
+    #elif defined(_SAPP_LINUX)
+        _sapp_x11_set_icon(desc, num_images);
+    #elif defined(_SAPP_EMSCRIPTEN)
+        _sapp_emsc_set_icon(desc, num_images);
+    #endif
+}
+
+SOKOL_API_IMPL int sapp_get_num_dropped_files(void) {
+    SOKOL_ASSERT(_sapp.drop.enabled);
+    return _sapp.drop.num_files;
+}
+
+SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) {
+    SOKOL_ASSERT(_sapp.drop.enabled);
+    SOKOL_ASSERT((index >= 0) && (index < _sapp.drop.num_files));
+    SOKOL_ASSERT(_sapp.drop.buffer);
+    if (!_sapp.drop.enabled) {
+        return "";
+    }
+    if ((index < 0) || (index >= _sapp.drop.max_files)) {
+        return "";
+    }
+    return (const char*) _sapp_dropped_file_path_ptr(index);
+}
+
+SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) {
+    SOKOL_ASSERT(_sapp.drop.enabled);
+    SOKOL_ASSERT((index >= 0) && (index < _sapp.drop.num_files));
+    #if defined(_SAPP_EMSCRIPTEN)
+        if (!_sapp.drop.enabled) {
+            return 0;
+        }
+        return sapp_js_dropped_file_size(index);
+    #else
+        (void)index;
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) {
+    SOKOL_ASSERT(_sapp.drop.enabled);
+    SOKOL_ASSERT(request);
+    SOKOL_ASSERT(request->callback);
+    SOKOL_ASSERT(request->buffer_ptr);
+    SOKOL_ASSERT(request->buffer_size > 0);
+    #if defined(_SAPP_EMSCRIPTEN)
+        const int index = request->dropped_file_index;
+        sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR;
+        if ((index < 0) || (index >= _sapp.drop.num_files)) {
+            error_code = SAPP_HTML5_FETCH_ERROR_OTHER;
+        }
+        if (sapp_html5_get_dropped_file_size(index) > request->buffer_size) {
+            error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL;
+        }
+        if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) {
+            _sapp_emsc_invoke_fetch_cb(index,
+                false, // success
+                (int)error_code,
+                request->callback,
+                0, // fetched_size
+                request->buffer_ptr,
+                request->buffer_size,
+                request->user_data);
+        }
+        else {
+            sapp_js_fetch_dropped_file(index,
+                request->callback,
+                request->buffer_ptr,
+                request->buffer_size,
+                request->user_data);
+        }
+    #else
+        (void)request;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_metal_get_device(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_METAL)
+        #if defined(_SAPP_MACOS)
+            const void* obj = (__bridge const void*) _sapp.macos.mtl_device;
+        #else
+            const void* obj = (__bridge const void*) _sapp.ios.mtl_device;
+        #endif
+        SOKOL_ASSERT(obj);
+        return obj;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_metal_get_renderpass_descriptor(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_METAL)
+        #if defined(_SAPP_MACOS)
+            const void* obj = (__bridge const void*) [_sapp.macos.view currentRenderPassDescriptor];
+        #else
+            const void* obj = (__bridge const void*) [_sapp.ios.view currentRenderPassDescriptor];
+        #endif
+        SOKOL_ASSERT(obj);
+        return obj;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_metal_get_drawable(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_METAL)
+        #if defined(_SAPP_MACOS)
+            const void* obj = (__bridge const void*) [_sapp.macos.view currentDrawable];
+        #else
+            const void* obj = (__bridge const void*) [_sapp.ios.view currentDrawable];
+        #endif
+        SOKOL_ASSERT(obj);
+        return obj;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_macos_get_window(void) {
+    #if defined(_SAPP_MACOS)
+        const void* obj = (__bridge const void*) _sapp.macos.window;
+        SOKOL_ASSERT(obj);
+        return obj;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_ios_get_window(void) {
+    #if defined(_SAPP_IOS)
+        const void* obj = (__bridge const void*) _sapp.ios.window;
+        SOKOL_ASSERT(obj);
+        return obj;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_d3d11_get_device(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_D3D11)
+        return _sapp.d3d11.device;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_d3d11_get_device_context(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_D3D11)
+        return _sapp.d3d11.device_context;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) {
+    SOKOL_ASSERT(_sapp.valid);
+#if defined(SOKOL_D3D11)
+    return _sapp.d3d11.swap_chain;
+#else
+    return 0;
+#endif
+}
+
+SOKOL_API_IMPL const void* sapp_d3d11_get_render_target_view(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_D3D11)
+        if (_sapp.d3d11.msaa_rtv) {
+            return _sapp.d3d11.msaa_rtv;
+        }
+        else {
+            return _sapp.d3d11.rtv;
+        }
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_d3d11_get_depth_stencil_view(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(SOKOL_D3D11)
+        return _sapp.d3d11.dsv;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_WIN32)
+        return _sapp.win32.hwnd;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_wgpu_get_device(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_EMSCRIPTEN) && defined(SOKOL_WGPU)
+        return (const void*) _sapp.emsc.wgpu.device;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_wgpu_get_render_view(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_EMSCRIPTEN) && defined(SOKOL_WGPU)
+        if (_sapp.sample_count > 1) {
+            return (const void*) _sapp.emsc.wgpu.msaa_view;
+        }
+        else {
+            return (const void*) _sapp.emsc.wgpu.swapchain_view;
+        }
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_wgpu_get_resolve_view(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_EMSCRIPTEN) && defined(SOKOL_WGPU)
+        if (_sapp.sample_count > 1) {
+            return (const void*) _sapp.emsc.wgpu.swapchain_view;
+        }
+        else {
+            return 0;
+        }
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_wgpu_get_depth_stencil_view(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_EMSCRIPTEN) && defined(SOKOL_WGPU)
+        return (const void*) _sapp.emsc.wgpu.depth_stencil_view;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) {
+    SOKOL_ASSERT(_sapp.valid);
+    #if defined(_SAPP_ANDROID)
+        return (void*)_sapp.android.activity;
+    #else
+        return 0;
+    #endif
+}
+
+SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) {
+    _sapp.html5_ask_leave_site = ask;
+}
+
+#endif /* SOKOL_APP_IMPL */
--- /dev/null
+++ b/src/libs/sokol_audio.h
@@ -1,0 +1,2596 @@
+#if defined(SOKOL_IMPL) && !defined(SOKOL_AUDIO_IMPL)
+#define SOKOL_AUDIO_IMPL
+#endif
+#ifndef SOKOL_AUDIO_INCLUDED
+/*
+    sokol_audio.h -- cross-platform audio-streaming API
+
+    Project URL: https://github.com/floooh/sokol
+
+    Do this:
+        #define SOKOL_IMPL or
+        #define SOKOL_AUDIO_IMPL
+    before you include this file in *one* C or C++ file to create the
+    implementation.
+
+    Optionally provide the following defines with your own implementations:
+
+    SOKOL_DUMMY_BACKEND - use a dummy backend
+    SOKOL_ASSERT(c)     - your own assert macro (default: assert(c))
+    SOKOL_AUDIO_API_DECL- public function declaration prefix (default: extern)
+    SOKOL_API_DECL      - same as SOKOL_AUDIO_API_DECL
+    SOKOL_API_IMPL      - public function implementation prefix (default: -)
+
+    SAUDIO_RING_MAX_SLOTS           - max number of slots in the push-audio ring buffer (default 1024)
+    SAUDIO_OSX_USE_SYSTEM_HEADERS   - define this to force inclusion of system headers on
+                                      macOS instead of using embedded CoreAudio declarations
+    SAUDIO_ANDROID_AAUDIO           - on Android, select the AAudio backend (default)
+    SAUDIO_ANDROID_SLES             - on Android, select the OpenSLES backend
+
+    If sokol_audio.h is compiled as a DLL, define the following before
+    including the declaration or implementation:
+
+    SOKOL_DLL
+
+    On Windows, SOKOL_DLL will define SOKOL_AUDIO_API_DECL as __declspec(dllexport)
+    or __declspec(dllimport) as needed.
+
+    Link with the following libraries:
+
+    - on macOS: AudioToolbox
+    - on iOS: AudioToolbox, AVFoundation
+    - on Linux: asound
+    - on Android: link with OpenSLES or aaudio
+    - on Windows with MSVC or Clang toolchain: no action needed, libs are defined in-source via pragma-comment-lib
+    - on Windows with MINGW/MSYS2 gcc: compile with '-mwin32' and link with -lole32
+
+    FEATURE OVERVIEW
+    ================
+    You provide a mono- or stereo-stream of 32-bit float samples, which
+    Sokol Audio feeds into platform-specific audio backends:
+
+    - Windows: WASAPI
+    - Linux: ALSA
+    - macOS: CoreAudio
+    - iOS: CoreAudio+AVAudioSession
+    - emscripten: WebAudio with ScriptProcessorNode
+    - Android: AAudio (default) or OpenSLES, select at build time
+
+    Sokol Audio will not do any buffer mixing or volume control, if you have
+    multiple independent input streams of sample data you need to perform the
+    mixing yourself before forwarding the data to Sokol Audio.
+
+    There are two mutually exclusive ways to provide the sample data:
+
+    1. Callback model: You provide a callback function, which will be called
+       when Sokol Audio needs new samples. On all platforms except emscripten,
+       this function is called from a separate thread.
+    2. Push model: Your code pushes small blocks of sample data from your
+       main loop or a thread you created. The pushed data is stored in
+       a ring buffer where it is pulled by the backend code when
+       needed.
+
+    The callback model is preferred because it is the most direct way to
+    feed sample data into the audio backends and also has less moving parts
+    (there is no ring buffer between your code and the audio backend).
+
+    Sometimes it is not possible to generate the audio stream directly in a
+    callback function running in a separate thread, for such cases Sokol Audio
+    provides the push-model as a convenience.
+
+    SOKOL AUDIO, SOLOUD AND MINIAUDIO
+    =================================
+    The WASAPI, ALSA, OpenSLES and CoreAudio backend code has been taken from the
+    SoLoud library (with some modifications, so any bugs in there are most
+    likely my fault). If you need a more fully-featured audio solution, check
+    out SoLoud, it's excellent:
+
+        https://github.com/jarikomppa/soloud
+
+    Another alternative which feature-wise is somewhere inbetween SoLoud and
+    sokol-audio might be MiniAudio:
+
+        https://github.com/mackron/miniaudio
+
+    GLOSSARY
+    ========
+    - stream buffer:
+        The internal audio data buffer, usually provided by the backend API. The
+        size of the stream buffer defines the base latency, smaller buffers have
+        lower latency but may cause audio glitches. Bigger buffers reduce or
+        eliminate glitches, but have a higher base latency.
+
+    - stream callback:
+        Optional callback function which is called by Sokol Audio when it
+        needs new samples. On Windows, macOS/iOS and Linux, this is called in
+        a separate thread, on WebAudio, this is called per-frame in the
+        browser thread.
+
+    - channel:
+        A discrete track of audio data, currently 1-channel (mono) and
+        2-channel (stereo) is supported and tested.
+
+    - sample:
+        The magnitude of an audio signal on one channel at a given time. In
+        Sokol Audio, samples are 32-bit float numbers in the range -1.0 to
+        +1.0.
+
+    - frame:
+        The tightly packed set of samples for all channels at a given time.
+        For mono 1 frame is 1 sample. For stereo, 1 frame is 2 samples.
+
+    - packet:
+        In Sokol Audio, a small chunk of audio data that is moved from the
+        main thread to the audio streaming thread in order to decouple the
+        rate at which the main thread provides new audio data, and the
+        streaming thread consuming audio data.
+
+    WORKING WITH SOKOL AUDIO
+    ========================
+    First call saudio_setup() with your preferred audio playback options.
+    In most cases you can stick with the default values, these provide
+    a good balance between low-latency and glitch-free playback
+    on all audio backends.
+
+    You should always provide a logging callback to be aware of any
+    warnings and errors. The easiest way is to use sokol_log.h for this:
+
+        #include "sokol_log.h"
+        // ...
+        saudio_setup(&(saudio_desc){
+            .logger = {
+                .func = slog_func,
+            }
+        });
+
+    If you want to use the callback-model, you need to provide a stream
+    callback function either in saudio_desc.stream_cb or saudio_desc.stream_userdata_cb,
+    otherwise keep both function pointers zero-initialized.
+
+    Use push model and default playback parameters:
+
+        saudio_setup(&(saudio_desc){ .logger.func = slog_func });
+
+    Use stream callback model and default playback parameters:
+
+        saudio_setup(&(saudio_desc){
+            .stream_cb = my_stream_callback
+            .logger.func = slog_func,
+        });
+
+    The standard stream callback doesn't have a user data argument, if you want
+    that, use the alternative stream_userdata_cb and also set the user_data pointer:
+
+        saudio_setup(&(saudio_desc){
+            .stream_userdata_cb = my_stream_callback,
+            .user_data = &my_data
+            .logger.func = slog_func,
+        });
+
+    The following playback parameters can be provided through the
+    saudio_desc struct:
+
+    General parameters (both for stream-callback and push-model):
+
+        int sample_rate     -- the sample rate in Hz, default: 44100
+        int num_channels    -- number of channels, default: 1 (mono)
+        int buffer_frames   -- number of frames in streaming buffer, default: 2048
+
+    The stream callback prototype (either with or without userdata):
+
+        void (*stream_cb)(float* buffer, int num_frames, int num_channels)
+        void (*stream_userdata_cb)(float* buffer, int num_frames, int num_channels, void* user_data)
+            Function pointer to the user-provide stream callback.
+
+    Push-model parameters:
+
+        int packet_frames   -- number of frames in a packet, default: 128
+        int num_packets     -- number of packets in ring buffer, default: 64
+
+    The sample_rate and num_channels parameters are only hints for the audio
+    backend, it isn't guaranteed that those are the values used for actual
+    playback.
+
+    To get the actual parameters, call the following functions after
+    saudio_setup():
+
+        int saudio_sample_rate(void)
+        int saudio_channels(void);
+
+    It's unlikely that the number of channels will be different than requested,
+    but a different sample rate isn't uncommon.
+
+    (NOTE: there's an yet unsolved issue when an audio backend might switch
+    to a different sample rate when switching output devices, for instance
+    plugging in a bluetooth headset, this case is currently not handled in
+    Sokol Audio).
+
+    You can check if audio initialization was successful with
+    saudio_isvalid(). If backend initialization failed for some reason
+    (for instance when there's no audio device in the machine), this
+    will return false. Not checking for success won't do any harm, all
+    Sokol Audio function will silently fail when called after initialization
+    has failed, so apart from missing audio output, nothing bad will happen.
+
+    Before your application exits, you should call
+
+        saudio_shutdown();
+
+    This stops the audio thread (on Linux, Windows and macOS/iOS) and
+    properly shuts down the audio backend.
+
+    THE STREAM CALLBACK MODEL
+    =========================
+    To use Sokol Audio in stream-callback-mode, provide a callback function
+    like this in the saudio_desc struct when calling saudio_setup():
+
+    void stream_cb(float* buffer, int num_frames, int num_channels) {
+        ...
+    }
+
+    Or the alternative version with a user-data argument:
+
+    void stream_userdata_cb(float* buffer, int num_frames, int num_channels, void* user_data) {
+        my_data_t* my_data = (my_data_t*) user_data;
+        ...
+    }
+
+    The job of the callback function is to fill the *buffer* with 32-bit
+    float sample values.
+
+    To output silence, fill the buffer with zeros:
+
+        void stream_cb(float* buffer, int num_frames, int num_channels) {
+            const int num_samples = num_frames * num_channels;
+            for (int i = 0; i < num_samples; i++) {
+                buffer[i] = 0.0f;
+            }
+        }
+
+    For stereo output (num_channels == 2), the samples for the left
+    and right channel are interleaved:
+
+        void stream_cb(float* buffer, int num_frames, int num_channels) {
+            assert(2 == num_channels);
+            for (int i = 0; i < num_frames; i++) {
+                buffer[2*i + 0] = ...;  // left channel
+                buffer[2*i + 1] = ...;  // right channel
+            }
+        }
+
+    Please keep in mind that the stream callback function is running in a
+    separate thread, if you need to share data with the main thread you need
+    to take care yourself to make the access to the shared data thread-safe!
+
+    THE PUSH MODEL
+    ==============
+    To use the push-model for providing audio data, simply don't set (keep
+    zero-initialized) the stream_cb field in the saudio_desc struct when
+    calling saudio_setup().
+
+    To provide sample data with the push model, call the saudio_push()
+    function at regular intervals (for instance once per frame). You can
+    call the saudio_expect() function to ask Sokol Audio how much room is
+    in the ring buffer, but if you provide a continuous stream of data
+    at the right sample rate, saudio_expect() isn't required (it's a simple
+    way to sync/throttle your sample generation code with the playback
+    rate though).
+
+    With saudio_push() you may need to maintain your own intermediate sample
+    buffer, since pushing individual sample values isn't very efficient.
+    The following example is from the MOD player sample in
+    sokol-samples (https://github.com/floooh/sokol-samples):
+
+        const int num_frames = saudio_expect();
+        if (num_frames > 0) {
+            const int num_samples = num_frames * saudio_channels();
+            read_samples(flt_buf, num_samples);
+            saudio_push(flt_buf, num_frames);
+        }
+
+    Another option is to ignore saudio_expect(), and just push samples as they
+    are generated in small batches. In this case you *need* to generate the
+    samples at the right sample rate:
+
+    The following example is taken from the Tiny Emulators project
+    (https://github.com/floooh/chips-test), this is for mono playback,
+    so (num_samples == num_frames):
+
+        // tick the sound generator
+        if (ay38910_tick(&sys->psg)) {
+            // new sample is ready
+            sys->sample_buffer[sys->sample_pos++] = sys->psg.sample;
+            if (sys->sample_pos == sys->num_samples) {
+                // new sample packet is ready
+                saudio_push(sys->sample_buffer, sys->num_samples);
+                sys->sample_pos = 0;
+            }
+        }
+
+    THE WEBAUDIO BACKEND
+    ====================
+    The WebAudio backend is currently using a ScriptProcessorNode callback to
+    feed the sample data into WebAudio. ScriptProcessorNode has been
+    deprecated for a while because it is running from the main thread, with
+    the default initialization parameters it works 'pretty well' though.
+    Ultimately Sokol Audio will use Audio Worklets, but this requires a few
+    more things to fall into place (Audio Worklets implemented everywhere,
+    SharedArrayBuffers enabled again, and I need to figure out a 'low-cost'
+    solution in terms of implementation effort, since Audio Worklets are
+    a lot more complex than ScriptProcessorNode if the audio data needs to come
+    from the main thread).
+
+    The WebAudio backend is automatically selected when compiling for
+    emscripten (__EMSCRIPTEN__ define exists).
+
+    https://developers.google.com/web/updates/2017/12/audio-worklet
+    https://developers.google.com/web/updates/2018/06/audio-worklet-design-pattern
+
+    "Blob URLs": https://www.html5rocks.com/en/tutorials/workers/basics/
+
+    Also see: https://blog.paul.cx/post/a-wait-free-spsc-ringbuffer-for-the-web/
+
+    THE COREAUDIO BACKEND
+    =====================
+    The CoreAudio backend is selected on macOS and iOS (__APPLE__ is defined).
+    Since the CoreAudio API is implemented in C (not Objective-C) on macOS the
+    implementation part of Sokol Audio can be included into a C source file.
+
+    However on iOS, Sokol Audio must be compiled as Objective-C due to it's
+    reliance on the AVAudioSession object. The iOS code path support both
+    being compiled with or without ARC (Automatic Reference Counting).
+
+    For thread synchronisation, the CoreAudio backend will use the
+    pthread_mutex_* functions.
+
+    The incoming floating point samples will be directly forwarded to
+    CoreAudio without further conversion.
+
+    macOS and iOS applications that use Sokol Audio need to link with
+    the AudioToolbox framework.
+
+    THE WASAPI BACKEND
+    ==================
+    The WASAPI backend is automatically selected when compiling on Windows
+    (_WIN32 is defined).
+
+    For thread synchronisation a Win32 critical section is used.
+
+    WASAPI may use a different size for its own streaming buffer then requested,
+    so the base latency may be slightly bigger. The current backend implementation
+    converts the incoming floating point sample values to signed 16-bit
+    integers.
+
+    The required Windows system DLLs are linked with #pragma comment(lib, ...),
+    so you shouldn't need to add additional linker libs in the build process
+    (otherwise this is a bug which should be fixed in sokol_audio.h).
+
+    THE ALSA BACKEND
+    ================
+    The ALSA backend is automatically selected when compiling on Linux
+    ('linux' is defined).
+
+    For thread synchronisation, the pthread_mutex_* functions are used.
+
+    Samples are directly forwarded to ALSA in 32-bit float format, no
+    further conversion is taking place.
+
+    You need to link with the 'asound' library, and the <alsa/asoundlib.h>
+    header must be present (usually both are installed with some sort
+    of ALSA development package).
+
+
+    MEMORY ALLOCATION OVERRIDE
+    ==========================
+    You can override the memory allocation functions at initialization time
+    like this:
+
+        void* my_alloc(size_t size, void* user_data) {
+            return malloc(size);
+        }
+
+        void my_free(void* ptr, void* user_data) {
+            free(ptr);
+        }
+
+        ...
+            saudio_setup(&(saudio_desc){
+                // ...
+                .allocator = {
+                    .alloc = my_alloc,
+                    .free = my_free,
+                    .user_data = ...,
+                }
+            });
+        ...
+
+    If no overrides are provided, malloc and free will be used.
+
+    This only affects memory allocation calls done by sokol_audio.h
+    itself though, not any allocations in OS libraries.
+
+    Memory allocation will only happen on the same thread where saudio_setup()
+    was called, so you don't need to worry about thread-safety.
+
+
+    ERROR REPORTING AND LOGGING
+    ===========================
+    To get any logging information at all you need to provide a logging callback in the setup call
+    the easiest way is to use sokol_log.h:
+
+        #include "sokol_log.h"
+
+        saudio_setup(&(saudio_desc){ .logger.func = slog_func });
+
+    To override logging with your own callback, first write a logging function like this:
+
+        void my_log(const char* tag,                // e.g. 'saudio'
+                    uint32_t log_level,             // 0=panic, 1=error, 2=warn, 3=info
+                    uint32_t log_item_id,           // SAUDIO_LOGITEM_*
+                    const char* message_or_null,    // a message string, may be nullptr in release mode
+                    uint32_t line_nr,               // line number in sokol_audio.h
+                    const char* filename_or_null,   // source filename, may be nullptr in release mode
+                    void* user_data)
+        {
+            ...
+        }
+
+    ...and then setup sokol-audio like this:
+
+        saudio_setup(&(saudio_desc){
+            .logger = {
+                .func = my_log,
+                .user_data = my_user_data,
+            }
+        });
+
+    The provided logging function must be reentrant (e.g. be callable from
+    different threads).
+
+    If you don't want to provide your own custom logger it is highly recommended to use
+    the standard logger in sokol_log.h instead, otherwise you won't see any warnings or
+    errors.
+
+    LICENSE
+    =======
+
+    zlib/libpng license
+
+    Copyright (c) 2018 Andre Weissflog
+
+    This software is provided 'as-is', without any express or implied warranty.
+    In no event will the authors be held liable for any damages arising from the
+    use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+        1. The origin of this software must not be misrepresented; you must not
+        claim that you wrote the original software. If you use this software in a
+        product, an acknowledgment in the product documentation would be
+        appreciated but is not required.
+
+        2. Altered source versions must be plainly marked as such, and must not
+        be misrepresented as being the original software.
+
+        3. This notice may not be removed or altered from any source
+        distribution.
+*/
+#define SOKOL_AUDIO_INCLUDED (1)
+#include <stddef.h> // size_t
+#include <stdint.h>
+#include <stdbool.h>
+
+#if defined(SOKOL_API_DECL) && !defined(SOKOL_AUDIO_API_DECL)
+#define SOKOL_AUDIO_API_DECL SOKOL_API_DECL
+#endif
+#ifndef SOKOL_AUDIO_API_DECL
+#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_AUDIO_IMPL)
+#define SOKOL_AUDIO_API_DECL __declspec(dllexport)
+#elif defined(_WIN32) && defined(SOKOL_DLL)
+#define SOKOL_AUDIO_API_DECL __declspec(dllimport)
+#else
+#define SOKOL_AUDIO_API_DECL extern
+#endif
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+    saudio_log_item
+
+    Log items are defined via X-Macros, and expanded to an
+    enum 'saudio_log_item', and in debug mode only,
+    corresponding strings.
+
+    Used as parameter in the logging callback.
+*/
+#define _SAUDIO_LOG_ITEMS \
+    _SAUDIO_LOGITEM_XMACRO(OK, "Ok") \
+    _SAUDIO_LOGITEM_XMACRO(MALLOC_FAILED, "memory allocation failed") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_SND_PCM_OPEN_FAILED, "snd_pcm_open() failed") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_FLOAT_SAMPLES_NOT_SUPPORTED, "floating point sample format not supported") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_REQUESTED_BUFFER_SIZE_NOT_SUPPORTED, "requested buffer size not supported") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_REQUESTED_CHANNEL_COUNT_NOT_SUPPORTED, "requested channel count not supported") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_SND_PCM_HW_PARAMS_SET_RATE_NEAR_FAILED, "snd_pcm_hw_params_set_rate_near() failed") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_SND_PCM_HW_PARAMS_FAILED, "snd_pcm_hw_params() failed") \
+    _SAUDIO_LOGITEM_XMACRO(ALSA_PTHREAD_CREATE_FAILED, "pthread_create() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_CREATE_EVENT_FAILED, "CreateEvent() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_CREATE_DEVICE_ENUMERATOR_FAILED, "CoCreateInstance() for IMMDeviceEnumerator failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_GET_DEFAULT_AUDIO_ENDPOINT_FAILED, "IMMDeviceEnumerator.GetDefaultAudioEndpoint() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_DEVICE_ACTIVATE_FAILED, "IMMDevice.Activate() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_AUDIO_CLIENT_INITIALIZE_FAILED, "IAudioClient.Initialize() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_AUDIO_CLIENT_GET_BUFFER_SIZE_FAILED, "IAudioClient.GetBufferSize() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_AUDIO_CLIENT_GET_SERVICE_FAILED, "IAudioClient.GetService() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_AUDIO_CLIENT_SET_EVENT_HANDLE_FAILED, "IAudioClient.SetEventHandle() failed") \
+    _SAUDIO_LOGITEM_XMACRO(WASAPI_CREATE_THREAD_FAILED, "CreateThread() failed") \
+    _SAUDIO_LOGITEM_XMACRO(AAUDIO_STREAMBUILDER_OPEN_STREAM_FAILED, "AAudioStreamBuilder_openStream() failed") \
+    _SAUDIO_LOGITEM_XMACRO(AAUDIO_PTHREAD_CREATE_FAILED, "pthread_create() failed after AAUDIO_ERROR_DISCONNECTED") \
+    _SAUDIO_LOGITEM_XMACRO(AAUDIO_RESTARTING_STREAM_AFTER_ERROR, "restarting AAudio stream after error") \
+    _SAUDIO_LOGITEM_XMACRO(USING_AAUDIO_BACKEND, "using AAudio backend") \
+    _SAUDIO_LOGITEM_XMACRO(AAUDIO_CREATE_STREAMBUILDER_FAILED, "AAudio_createStreamBuilder() failed") \
+    _SAUDIO_LOGITEM_XMACRO(USING_SLES_BACKEND, "using OpenSLES backend") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_CREATE_ENGINE_FAILED, "slCreateEngine() failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_ENGINE_GET_ENGINE_INTERFACE_FAILED, "GetInterface() for SL_IID_ENGINE failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_CREATE_OUTPUT_MIX_FAILED, "CreateOutputMix() failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_MIXER_GET_VOLUME_INTERFACE_FAILED, "GetInterface() for SL_IID_VOLUME failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_ENGINE_CREATE_AUDIO_PLAYER_FAILED, "CreateAudioPlayer() failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_PLAYER_GET_PLAY_INTERFACE_FAILED, "GetInterface() for SL_IID_PLAY failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_PLAYER_GET_VOLUME_INTERFACE_FAILED, "GetInterface() for SL_IID_VOLUME failed") \
+    _SAUDIO_LOGITEM_XMACRO(SLES_PLAYER_GET_BUFFERQUEUE_INTERFACE_FAILED, "GetInterface() for SL_IID_ANDROIDSIMPLEBUFFERQUEUE failed") \
+    _SAUDIO_LOGITEM_XMACRO(COREAUDIO_NEW_OUTPUT_FAILED, "AudioQueueNewOutput() failed") \
+    _SAUDIO_LOGITEM_XMACRO(COREAUDIO_ALLOCATE_BUFFER_FAILED, "AudioQueueAllocateBuffer() failed") \
+    _SAUDIO_LOGITEM_XMACRO(COREAUDIO_START_FAILED, "AudioQueueStart() failed") \
+    _SAUDIO_LOGITEM_XMACRO(BACKEND_BUFFER_SIZE_ISNT_MULTIPLE_OF_PACKET_SIZE, "backend buffer size isn't multiple of packet size") \
+
+#define _SAUDIO_LOGITEM_XMACRO(item,msg) SAUDIO_LOGITEM_##item,
+typedef enum saudio_log_item {
+    _SAUDIO_LOG_ITEMS
+} saudio_log_item;
+#undef _SAUDIO_LOGITEM_XMACRO
+
+/*
+    saudio_logger
+
+    Used in saudio_desc to provide a custom logging and error reporting
+    callback to sokol-audio.
+*/
+typedef struct saudio_logger {
+    void (*func)(
+        const char* tag,                // always "saudio"
+        uint32_t log_level,             // 0=panic, 1=error, 2=warning, 3=info
+        uint32_t log_item_id,           // SAUDIO_LOGITEM_*
+        const char* message_or_null,    // a message string, may be nullptr in release mode
+        uint32_t line_nr,               // line number in sokol_audio.h
+        const char* filename_or_null,   // source filename, may be nullptr in release mode
+        void* user_data);
+    void* user_data;
+} saudio_logger;
+
+/*
+    saudio_allocator
+
+    Used in saudio_desc to provide custom memory-alloc and -free functions
+    to sokol_audio.h. If memory management should be overridden, both the
+    alloc and free function must be provided (e.g. it's not valid to
+    override one function but not the other).
+*/
+typedef struct saudio_allocator {
+    void* (*alloc)(size_t size, void* user_data);
+    void (*free)(void* ptr, void* user_data);
+    void* user_data;
+} saudio_allocator;
+
+typedef struct saudio_desc {
+    int sample_rate;        // requested sample rate
+    int num_channels;       // number of channels, default: 1 (mono)
+    int buffer_frames;      // number of frames in streaming buffer
+    int packet_frames;      // number of frames in a packet
+    int num_packets;        // number of packets in packet queue
+    void (*stream_cb)(float* buffer, int num_frames, int num_channels);  // optional streaming callback (no user data)
+    void (*stream_userdata_cb)(float* buffer, int num_frames, int num_channels, void* user_data); //... and with user data
+    void* user_data;        // optional user data argument for stream_userdata_cb
+    saudio_allocator allocator;     // optional allocation override functions
+    saudio_logger logger;           // optional logging function (default: NO LOGGING!)
+} saudio_desc;
+
+/* setup sokol-audio */
+SOKOL_AUDIO_API_DECL void saudio_setup(const saudio_desc* desc);
+/* shutdown sokol-audio */
+SOKOL_AUDIO_API_DECL void saudio_shutdown(void);
+/* true after setup if audio backend was successfully initialized */
+SOKOL_AUDIO_API_DECL bool saudio_isvalid(void);
+/* return the saudio_desc.user_data pointer */
+SOKOL_AUDIO_API_DECL void* saudio_userdata(void);
+/* return a copy of the original saudio_desc struct */
+SOKOL_AUDIO_API_DECL saudio_desc saudio_query_desc(void);
+/* actual sample rate */
+SOKOL_AUDIO_API_DECL int saudio_sample_rate(void);
+/* return actual backend buffer size in number of frames */
+SOKOL_AUDIO_API_DECL int saudio_buffer_frames(void);
+/* actual number of channels */
+SOKOL_AUDIO_API_DECL int saudio_channels(void);
+/* return true if audio context is currently suspended (only in WebAudio backend, all other backends return false) */
+SOKOL_AUDIO_API_DECL bool saudio_suspended(void);
+/* get current number of frames to fill packet queue */
+SOKOL_AUDIO_API_DECL int saudio_expect(void);
+/* push sample frames from main thread, returns number of frames actually pushed */
+SOKOL_AUDIO_API_DECL int saudio_push(const float* frames, int num_frames);
+
+#ifdef __cplusplus
+} /* extern "C" */
+
+/* reference-based equivalents for c++ */
+inline void saudio_setup(const saudio_desc& desc) { return saudio_setup(&desc); }
+
+#endif
+#endif // SOKOL_AUDIO_INCLUDED
+
+// ██ ███    ███ ██████  ██      ███████ ███    ███ ███████ ███    ██ ████████  █████  ████████ ██  ██████  ███    ██
+// ██ ████  ████ ██   ██ ██      ██      ████  ████ ██      ████   ██    ██    ██   ██    ██    ██ ██    ██ ████   ██
+// ██ ██ ████ ██ ██████  ██      █████   ██ ████ ██ █████   ██ ██  ██    ██    ███████    ██    ██ ██    ██ ██ ██  ██
+// ██ ██  ██  ██ ██      ██      ██      ██  ██  ██ ██      ██  ██ ██    ██    ██   ██    ██    ██ ██    ██ ██  ██ ██
+// ██ ██      ██ ██      ███████ ███████ ██      ██ ███████ ██   ████    ██    ██   ██    ██    ██  ██████  ██   ████
+//
+// >>implementation
+#ifdef SOKOL_AUDIO_IMPL
+#define SOKOL_AUDIO_IMPL_INCLUDED (1)
+
+#if defined(SOKOL_MALLOC) || defined(SOKOL_CALLOC) || defined(SOKOL_FREE)
+#error "SOKOL_MALLOC/CALLOC/FREE macros are no longer supported, please use saudio_desc.allocator to override memory allocation functions"
+#endif
+
+#include <stdlib.h> // alloc, free
+#include <string.h> // memset, memcpy
+#include <stddef.h> // size_t
+
+#ifndef SOKOL_API_IMPL
+    #define SOKOL_API_IMPL
+#endif
+#ifndef SOKOL_DEBUG
+    #ifndef NDEBUG
+        #define SOKOL_DEBUG
+    #endif
+#endif
+#ifndef SOKOL_ASSERT
+    #include <assert.h>
+    #define SOKOL_ASSERT(c) assert(c)
+#endif
+
+#ifndef _SOKOL_PRIVATE
+    #if defined(__GNUC__) || defined(__clang__)
+        #define _SOKOL_PRIVATE __attribute__((unused)) static
+    #else
+        #define _SOKOL_PRIVATE static
+    #endif
+#endif
+
+#ifndef _SOKOL_UNUSED
+    #define _SOKOL_UNUSED(x) (void)(x)
+#endif
+
+// platform detection defines
+#if defined(SOKOL_DUMMY_BACKEND)
+    // nothing
+#elif defined(__APPLE__)
+    #define _SAUDIO_APPLE (1)
+    #include <TargetConditionals.h>
+    #if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
+        #define _SAUDIO_IOS (1)
+    #else
+        #define _SAUDIO_MACOS (1)
+    #endif
+#elif defined(__EMSCRIPTEN__)
+    #define _SAUDIO_EMSCRIPTEN (1)
+#elif defined(_WIN32)
+    #define _SAUDIO_WINDOWS (1)
+    #include <winapifamily.h>
+    #if (defined(WINAPI_FAMILY_PARTITION) && !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP))
+        #error "sokol_audio.h no longer supports UWP"
+    #endif
+#elif defined(__ANDROID__)
+    #define _SAUDIO_ANDROID (1)
+    #if !defined(SAUDIO_ANDROID_SLES) && !defined(SAUDIO_ANDROID_AAUDIO)
+        #define SAUDIO_ANDROID_AAUDIO (1)
+    #endif
+#elif defined(__linux__) || defined(__unix__)
+    #define _SAUDIO_LINUX (1)
+#else
+#error "sokol_audio.h: Unknown platform"
+#endif
+
+// platform-specific headers and definitions
+#if defined(SOKOL_DUMMY_BACKEND)
+    #define _SAUDIO_NOTHREADS (1)
+#elif defined(_SAUDIO_WINDOWS)
+    #define _SAUDIO_WINTHREADS (1)
+    #ifndef WIN32_LEAN_AND_MEAN
+    #define WIN32_LEAN_AND_MEAN
+    #endif
+    #ifndef NOMINMAX
+    #define NOMINMAX
+    #endif
+    #include <windows.h>
+    #include <synchapi.h>
+    #pragma comment (lib, "kernel32")
+    #pragma comment (lib, "ole32")
+    #ifndef CINTERFACE
+    #define CINTERFACE
+    #endif
+    #ifndef COBJMACROS
+    #define COBJMACROS
+    #endif
+    #ifndef CONST_VTABLE
+    #define CONST_VTABLE
+    #endif
+    #include <mmdeviceapi.h>
+    #include <audioclient.h>
+    static const IID _saudio_IID_IAudioClient                               = { 0x1cb9ad4c, 0xdbfa, 0x4c32, {0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2} };
+    static const IID _saudio_IID_IMMDeviceEnumerator                        = { 0xa95664d2, 0x9614, 0x4f35, {0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6} };
+    static const CLSID _saudio_CLSID_IMMDeviceEnumerator                    = { 0xbcde0395, 0xe52f, 0x467c, {0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e} };
+    static const IID _saudio_IID_IAudioRenderClient                         = { 0xf294acfc, 0x3146, 0x4483, {0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2} };
+    static const IID _saudio_IID_Devinterface_Audio_Render                  = { 0xe6327cad, 0xdcec, 0x4949, {0xae, 0x8a, 0x99, 0x1e, 0x97, 0x6a, 0x79, 0xd2} };
+    static const IID _saudio_IID_IActivateAudioInterface_Completion_Handler = { 0x94ea2b94, 0xe9cc, 0x49e0, {0xc0, 0xff, 0xee, 0x64, 0xca, 0x8f, 0x5b, 0x90} };
+    static const GUID _saudio_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT               = { 0x00000003, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71} };
+    #if defined(__cplusplus)
+    #define _SOKOL_AUDIO_WIN32COM_ID(x) (x)
+    #else
+    #define _SOKOL_AUDIO_WIN32COM_ID(x) (&x)
+    #endif
+    /* fix for Visual Studio 2015 SDKs */
+    #ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
+    #define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000
+    #endif
+    #ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY
+    #define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 0x08000000
+    #endif
+    #ifdef _MSC_VER
+        #pragma warning(push)
+        #pragma warning(disable:4505)   /* unreferenced local function has been removed */
+    #endif
+#elif defined(_SAUDIO_APPLE)
+    #define _SAUDIO_PTHREADS (1)
+    #include <pthread.h>
+    #if defined(_SAUDIO_IOS)
+        // always use system headers on iOS (for now at least)
+        #if !defined(SAUDIO_OSX_USE_SYSTEM_HEADERS)
+            #define SAUDIO_OSX_USE_SYSTEM_HEADERS (1)
+        #endif
+        #if !defined(__cplusplus)
+            #if __has_feature(objc_arc) && !__has_feature(objc_arc_fields)
+                #error "sokol_audio.h on iOS requires __has_feature(objc_arc_field) if ARC is enabled (use a more recent compiler version)"
+            #endif
+        #endif
+        #include <AudioToolbox/AudioToolbox.h>
+        #include <AVFoundation/AVFoundation.h>
+    #else
+        #if defined(SAUDIO_OSX_USE_SYSTEM_HEADERS)
+            #include <AudioToolbox/AudioToolbox.h>
+        #endif
+    #endif
+#elif defined(_SAUDIO_ANDROID)
+    #define _SAUDIO_PTHREADS (1)
+    #include <pthread.h>
+    #if defined(SAUDIO_ANDROID_SLES)
+        #include "SLES/OpenSLES_Android.h"
+    #elif defined(SAUDIO_ANDROID_AAUDIO)
+        #include "aaudio/AAudio.h"
+    #endif
+#elif defined(_SAUDIO_LINUX)
+    #include <alloca.h>
+    #define _SAUDIO_PTHREADS (1)
+    #include <pthread.h>
+    #define ALSA_PCM_NEW_HW_PARAMS_API
+    #include <alsa/asoundlib.h>
+#elif defined(__EMSCRIPTEN__)
+    #define _SAUDIO_NOTHREADS (1)
+    #include <emscripten/emscripten.h>
+#endif
+
+#define _saudio_def(val, def) (((val) == 0) ? (def) : (val))
+#define _saudio_def_flt(val, def) (((val) == 0.0f) ? (def) : (val))
+
+#define _SAUDIO_DEFAULT_SAMPLE_RATE (44100)
+#define _SAUDIO_DEFAULT_BUFFER_FRAMES (2048)
+#define _SAUDIO_DEFAULT_PACKET_FRAMES (128)
+#define _SAUDIO_DEFAULT_NUM_PACKETS ((_SAUDIO_DEFAULT_BUFFER_FRAMES/_SAUDIO_DEFAULT_PACKET_FRAMES)*4)
+
+#ifndef SAUDIO_RING_MAX_SLOTS
+#define SAUDIO_RING_MAX_SLOTS (1024)
+#endif
+
+// ███████ ████████ ██████  ██    ██  ██████ ████████ ███████
+// ██         ██    ██   ██ ██    ██ ██         ██    ██
+// ███████    ██    ██████  ██    ██ ██         ██    ███████
+//      ██    ██    ██   ██ ██    ██ ██         ██         ██
+// ███████    ██    ██   ██  ██████   ██████    ██    ███████
+//
+// >>structs
+#if defined(_SAUDIO_PTHREADS)
+
+typedef struct {
+    pthread_mutex_t mutex;
+} _saudio_mutex_t;
+
+#elif defined(_SAUDIO_WINTHREADS)
+
+typedef struct {
+    CRITICAL_SECTION critsec;
+} _saudio_mutex_t;
+
+#elif defined(_SAUDIO_NOTHREADS)
+
+typedef struct {
+    int dummy_mutex;
+} _saudio_mutex_t;
+
+#endif
+
+#if defined(SOKOL_DUMMY_BACKEND)
+
+typedef struct {
+    int dummy;
+} _saudio_dummy_backend_t;
+
+#elif defined(_SAUDIO_APPLE)
+
+#if defined(SAUDIO_OSX_USE_SYSTEM_HEADERS)
+
+typedef AudioQueueRef _saudio_AudioQueueRef;
+typedef AudioQueueBufferRef _saudio_AudioQueueBufferRef;
+typedef AudioStreamBasicDescription _saudio_AudioStreamBasicDescription;
+typedef OSStatus _saudio_OSStatus;
+
+#define _saudio_kAudioFormatLinearPCM (kAudioFormatLinearPCM)
+#define _saudio_kLinearPCMFormatFlagIsFloat (kLinearPCMFormatFlagIsFloat)
+#define _saudio_kAudioFormatFlagIsPacked (kAudioFormatFlagIsPacked)
+
+#else
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// embedded AudioToolbox declarations
+typedef uint32_t _saudio_AudioFormatID;
+typedef uint32_t _saudio_AudioFormatFlags;
+typedef int32_t _saudio_OSStatus;
+typedef uint32_t _saudio_SMPTETimeType;
+typedef uint32_t _saudio_SMPTETimeFlags;
+typedef uint32_t _saudio_AudioTimeStampFlags;
+typedef void* _saudio_CFRunLoopRef;
+typedef void* _saudio_CFStringRef;
+typedef void* _saudio_AudioQueueRef;
+
+#define _saudio_kAudioFormatLinearPCM ('lpcm')
+#define _saudio_kLinearPCMFormatFlagIsFloat (1U << 0)
+#define _saudio_kAudioFormatFlagIsPacked (1U << 3)
+
+typedef struct _saudio_AudioStreamBasicDescription {
+    double mSampleRate;
+    _saudio_AudioFormatID mFormatID;
+    _saudio_AudioFormatFlags mFormatFlags;
+    uint32_t mBytesPerPacket;
+    uint32_t mFramesPerPacket;
+    uint32_t mBytesPerFrame;
+    uint32_t mChannelsPerFrame;
+    uint32_t mBitsPerChannel;
+    uint32_t mReserved;
+} _saudio_AudioStreamBasicDescription;
+
+typedef struct _saudio_AudioStreamPacketDescription {
+    int64_t mStartOffset;
+    uint32_t mVariableFramesInPacket;
+    uint32_t mDataByteSize;
+} _saudio_AudioStreamPacketDescription;
+
+typedef struct _saudio_SMPTETime {
+    int16_t mSubframes;
+    int16_t mSubframeDivisor;
+    uint32_t mCounter;
+    _saudio_SMPTETimeType mType;
+    _saudio_SMPTETimeFlags mFlags;
+    int16_t mHours;
+    int16_t mMinutes;
+    int16_t mSeconds;
+    int16_t mFrames;
+} _saudio_SMPTETime;
+
+typedef struct _saudio_AudioTimeStamp {
+    double mSampleTime;
+    uint64_t mHostTime;
+    double mRateScalar;
+    uint64_t mWordClockTime;
+    _saudio_SMPTETime mSMPTETime;
+    _saudio_AudioTimeStampFlags mFlags;
+    uint32_t mReserved;
+} _saudio_AudioTimeStamp;
+
+typedef struct _saudio_AudioQueueBuffer {
+    const uint32_t mAudioDataBytesCapacity;
+    void* const mAudioData;
+    uint32_t mAudioDataByteSize;
+    void * mUserData;
+    const uint32_t mPacketDescriptionCapacity;
+    _saudio_AudioStreamPacketDescription* const mPacketDescriptions;
+    uint32_t mPacketDescriptionCount;
+} _saudio_AudioQueueBuffer;
+typedef _saudio_AudioQueueBuffer* _saudio_AudioQueueBufferRef;
+
+typedef void (*_saudio_AudioQueueOutputCallback)(void* user_data, _saudio_AudioQueueRef inAQ, _saudio_AudioQueueBufferRef inBuffer);
+
+extern _saudio_OSStatus AudioQueueNewOutput(const _saudio_AudioStreamBasicDescription* inFormat, _saudio_AudioQueueOutputCallback inCallbackProc, void* inUserData, _saudio_CFRunLoopRef inCallbackRunLoop, _saudio_CFStringRef inCallbackRunLoopMode, uint32_t inFlags, _saudio_AudioQueueRef* outAQ);
+extern _saudio_OSStatus AudioQueueDispose(_saudio_AudioQueueRef inAQ, bool inImmediate);
+extern _saudio_OSStatus AudioQueueAllocateBuffer(_saudio_AudioQueueRef inAQ, uint32_t inBufferByteSize, _saudio_AudioQueueBufferRef* outBuffer);
+extern _saudio_OSStatus AudioQueueEnqueueBuffer(_saudio_AudioQueueRef inAQ, _saudio_AudioQueueBufferRef inBuffer, uint32_t inNumPacketDescs, const _saudio_AudioStreamPacketDescription* inPacketDescs);
+extern _saudio_OSStatus AudioQueueStart(_saudio_AudioQueueRef inAQ, const _saudio_AudioTimeStamp * inStartTime);
+extern _saudio_OSStatus AudioQueueStop(_saudio_AudioQueueRef inAQ, bool inImmediate);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // SAUDIO_OSX_USE_SYSTEM_HEADERS
+
+typedef struct {
+    _saudio_AudioQueueRef ca_audio_queue;
+    #if defined(_SAUDIO_IOS)
+    id ca_interruption_handler;
+    #endif
+} _saudio_apple_backend_t;
+
+#elif defined(_SAUDIO_LINUX)
+
+typedef struct {
+    snd_pcm_t* device;
+    float* buffer;
+    int buffer_byte_size;
+    int buffer_frames;
+    pthread_t thread;
+    bool thread_stop;
+} _saudio_alsa_backend_t;
+
+#elif defined(SAUDIO_ANDROID_SLES)
+
+#define SAUDIO_SLES_NUM_BUFFERS (2)
+
+typedef struct {
+    pthread_mutex_t mutex;
+    pthread_cond_t cond;
+    int count;
+} _saudio_sles_semaphore_t;
+
+typedef struct {
+    SLObjectItf engine_obj;
+    SLEngineItf engine;
+    SLObjectItf output_mix_obj;
+    SLVolumeItf output_mix_vol;
+    SLDataLocator_OutputMix out_locator;
+    SLDataSink dst_data_sink;
+    SLObjectItf player_obj;
+    SLPlayItf player;
+    SLVolumeItf player_vol;
+    SLAndroidSimpleBufferQueueItf player_buffer_queue;
+
+    int16_t* output_buffers[SAUDIO_SLES_NUM_BUFFERS];
+    float* src_buffer;
+    int active_buffer;
+    _saudio_sles_semaphore_t buffer_sem;
+    pthread_t thread;
+    volatile int thread_stop;
+    SLDataLocator_AndroidSimpleBufferQueue in_locator;
+} _saudio_sles_backend_t;
+
+#elif defined(SAUDIO_ANDROID_AAUDIO)
+
+typedef struct {
+    AAudioStreamBuilder* builder;
+    AAudioStream* stream;
+    pthread_t thread;
+    pthread_mutex_t mutex;
+} _saudio_aaudio_backend_t;
+
+#elif defined(_SAUDIO_WINDOWS)
+
+typedef struct {
+    HANDLE thread_handle;
+    HANDLE buffer_end_event;
+    bool stop;
+    UINT32 dst_buffer_frames;
+    int src_buffer_frames;
+    int src_buffer_byte_size;
+    int src_buffer_pos;
+    float* src_buffer;
+} _saudio_wasapi_thread_data_t;
+
+typedef struct {
+    IMMDeviceEnumerator* device_enumerator;
+    IMMDevice* device;
+    IAudioClient* audio_client;
+    IAudioRenderClient* render_client;
+    _saudio_wasapi_thread_data_t thread;
+} _saudio_wasapi_backend_t;
+
+#elif defined(_SAUDIO_EMSCRIPTEN)
+
+typedef struct {
+    uint8_t* buffer;
+} _saudio_web_backend_t;
+
+#else
+#error "unknown platform"
+#endif
+
+#if defined(SOKOL_DUMMY_BACKEND)
+typedef _saudio_dummy_backend_t _saudio_backend_t;
+#elif defined(_SAUDIO_APPLE)
+typedef _saudio_apple_backend_t _saudio_backend_t;
+#elif defined(_SAUDIO_EMSCRIPTEN)
+typedef _saudio_web_backend_t _saudio_backend_t;
+#elif defined(_SAUDIO_WINDOWS)
+typedef _saudio_wasapi_backend_t _saudio_backend_t;
+#elif defined(SAUDIO_ANDROID_SLES)
+typedef _saudio_sles_backend_t _saudio_backend_t;
+#elif defined(SAUDIO_ANDROID_AAUDIO)
+typedef _saudio_aaudio_backend_t _saudio_backend_t;
+#elif defined(_SAUDIO_LINUX)
+typedef _saudio_alsa_backend_t _saudio_backend_t;
+#endif
+
+/* a ringbuffer structure */
+typedef struct {
+    int head;  // next slot to write to
+    int tail;  // next slot to read from
+    int num;   // number of slots in queue
+    int queue[SAUDIO_RING_MAX_SLOTS];
+} _saudio_ring_t;
+
+/* a packet FIFO structure */
+typedef struct {
+    bool valid;
+    int packet_size;            /* size of a single packets in bytes(!) */
+    int num_packets;            /* number of packet in fifo */
+    uint8_t* base_ptr;          /* packet memory chunk base pointer (dynamically allocated) */
+    int cur_packet;             /* current write-packet */
+    int cur_offset;             /* current byte-offset into current write packet */
+    _saudio_mutex_t mutex;      /* mutex for thread-safe access */
+    _saudio_ring_t read_queue;  /* buffers with data, ready to be streamed */
+    _saudio_ring_t write_queue; /* empty buffers, ready to be pushed to */
+} _saudio_fifo_t;
+
+/* sokol-audio state */
+typedef struct {
+    bool valid;
+    void (*stream_cb)(float* buffer, int num_frames, int num_channels);
+    void (*stream_userdata_cb)(float* buffer, int num_frames, int num_channels, void* user_data);
+    void* user_data;
+    int sample_rate;            /* sample rate */
+    int buffer_frames;          /* number of frames in streaming buffer */
+    int bytes_per_frame;        /* filled by backend */
+    int packet_frames;          /* number of frames in a packet */
+    int num_packets;            /* number of packets in packet queue */
+    int num_channels;           /* actual number of channels */
+    saudio_desc desc;
+    _saudio_fifo_t fifo;
+    _saudio_backend_t backend;
+} _saudio_state_t;
+
+_SOKOL_PRIVATE _saudio_state_t _saudio;
+
+_SOKOL_PRIVATE bool _saudio_has_callback(void) {
+    return (_saudio.stream_cb || _saudio.stream_userdata_cb);
+}
+
+_SOKOL_PRIVATE void _saudio_stream_callback(float* buffer, int num_frames, int num_channels) {
+    if (_saudio.stream_cb) {
+        _saudio.stream_cb(buffer, num_frames, num_channels);
+    }
+    else if (_saudio.stream_userdata_cb) {
+        _saudio.stream_userdata_cb(buffer, num_frames, num_channels, _saudio.user_data);
+    }
+}
+
+// ██       ██████   ██████   ██████  ██ ███    ██  ██████
+// ██      ██    ██ ██       ██       ██ ████   ██ ██
+// ██      ██    ██ ██   ███ ██   ███ ██ ██ ██  ██ ██   ███
+// ██      ██    ██ ██    ██ ██    ██ ██ ██  ██ ██ ██    ██
+// ███████  ██████   ██████   ██████  ██ ██   ████  ██████
+//
+// >>logging
+#if defined(SOKOL_DEBUG)
+#define _SAUDIO_LOGITEM_XMACRO(item,msg) #item ": " msg,
+static const char* _saudio_log_messages[] = {
+    _SAUDIO_LOG_ITEMS
+};
+#undef _SAUDIO_LOGITEM_XMACRO
+#endif // SOKOL_DEBUG
+
+#define _SAUDIO_PANIC(code) _saudio_log(SAUDIO_LOGITEM_ ##code, 0, __LINE__)
+#define _SAUDIO_ERROR(code) _saudio_log(SAUDIO_LOGITEM_ ##code, 1, __LINE__)
+#define _SAUDIO_WARN(code) _saudio_log(SAUDIO_LOGITEM_ ##code, 2, __LINE__)
+#define _SAUDIO_INFO(code) _saudio_log(SAUDIO_LOGITEM_ ##code, 3, __LINE__)
+
+static void _saudio_log(saudio_log_item log_item, uint32_t log_level, uint32_t line_nr) {
+    if (_saudio.desc.logger.func) {
+        #if defined(SOKOL_DEBUG)
+            const char* filename = __FILE__;
+            const char* message = _saudio_log_messages[log_item];
+        #else
+            const char* filename = 0;
+            const char* message = 0;
+        #endif
+        _saudio.desc.logger.func("saudio", log_level, log_item, message, line_nr, filename, _saudio.desc.logger.user_data);
+    }
+    else {
+        // for log level PANIC it would be 'undefined behaviour' to continue
+        if (log_level == 0) {
+            abort();
+        }
+    }
+}
+
+// ███    ███ ███████ ███    ███  ██████  ██████  ██    ██
+// ████  ████ ██      ████  ████ ██    ██ ██   ██  ██  ██
+// ██ ████ ██ █████   ██ ████ ██ ██    ██ ██████    ████
+// ██  ██  ██ ██      ██  ██  ██ ██    ██ ██   ██    ██
+// ██      ██ ███████ ██      ██  ██████  ██   ██    ██
+//
+// >>memory
+_SOKOL_PRIVATE void _saudio_clear(void* ptr, size_t size) {
+    SOKOL_ASSERT(ptr && (size > 0));
+    memset(ptr, 0, size);
+}
+
+_SOKOL_PRIVATE void* _saudio_malloc(size_t size) {
+    SOKOL_ASSERT(size > 0);
+    void* ptr;
+    if (_saudio.desc.allocator.alloc) {
+        ptr = _saudio.desc.allocator.alloc(size, _saudio.desc.allocator.user_data);
+    }
+    else {
+        ptr = malloc(size);
+    }
+    if (0 == ptr) {
+        _SAUDIO_PANIC(MALLOC_FAILED);
+    }
+    return ptr;
+}
+
+_SOKOL_PRIVATE void* _saudio_malloc_clear(size_t size) {
+    void* ptr = _saudio_malloc(size);
+    _saudio_clear(ptr, size);
+    return ptr;
+}
+
+_SOKOL_PRIVATE void _saudio_free(void* ptr) {
+    if (_saudio.desc.allocator.free) {
+        _saudio.desc.allocator.free(ptr, _saudio.desc.allocator.user_data);
+    }
+    else {
+        free(ptr);
+    }
+}
+
+// ███    ███ ██    ██ ████████ ███████ ██   ██
+// ████  ████ ██    ██    ██    ██       ██ ██
+// ██ ████ ██ ██    ██    ██    █████     ███
+// ██  ██  ██ ██    ██    ██    ██       ██ ██
+// ██      ██  ██████     ██    ███████ ██   ██
+//
+// >>mutex
+#if defined(_SAUDIO_NOTHREADS)
+
+_SOKOL_PRIVATE void _saudio_mutex_init(_saudio_mutex_t* m) { (void)m; }
+_SOKOL_PRIVATE void _saudio_mutex_destroy(_saudio_mutex_t* m) { (void)m; }
+_SOKOL_PRIVATE void _saudio_mutex_lock(_saudio_mutex_t* m) { (void)m; }
+_SOKOL_PRIVATE void _saudio_mutex_unlock(_saudio_mutex_t* m) { (void)m; }
+
+#elif defined(_SAUDIO_PTHREADS)
+
+_SOKOL_PRIVATE void _saudio_mutex_init(_saudio_mutex_t* m) {
+    pthread_mutexattr_t attr;
+    pthread_mutexattr_init(&attr);
+    pthread_mutex_init(&m->mutex, &attr);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_destroy(_saudio_mutex_t* m) {
+    pthread_mutex_destroy(&m->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_lock(_saudio_mutex_t* m) {
+    pthread_mutex_lock(&m->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_unlock(_saudio_mutex_t* m) {
+    pthread_mutex_unlock(&m->mutex);
+}
+
+#elif defined(_SAUDIO_WINTHREADS)
+
+_SOKOL_PRIVATE void _saudio_mutex_init(_saudio_mutex_t* m) {
+    InitializeCriticalSection(&m->critsec);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_destroy(_saudio_mutex_t* m) {
+    DeleteCriticalSection(&m->critsec);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_lock(_saudio_mutex_t* m) {
+    EnterCriticalSection(&m->critsec);
+}
+
+_SOKOL_PRIVATE void _saudio_mutex_unlock(_saudio_mutex_t* m) {
+    LeaveCriticalSection(&m->critsec);
+}
+#else
+#error "sokol_audio.h: unknown platform!"
+#endif
+
+// ██████  ██ ███    ██  ██████  ██████  ██    ██ ███████ ███████ ███████ ██████
+// ██   ██ ██ ████   ██ ██       ██   ██ ██    ██ ██      ██      ██      ██   ██
+// ██████  ██ ██ ██  ██ ██   ███ ██████  ██    ██ █████   █████   █████   ██████
+// ██   ██ ██ ██  ██ ██ ██    ██ ██   ██ ██    ██ ██      ██      ██      ██   ██
+// ██   ██ ██ ██   ████  ██████  ██████   ██████  ██      ██      ███████ ██   ██
+//
+// >>ringbuffer
+_SOKOL_PRIVATE int _saudio_ring_idx(_saudio_ring_t* ring, int i) {
+    return (i % ring->num);
+}
+
+_SOKOL_PRIVATE void _saudio_ring_init(_saudio_ring_t* ring, int num_slots) {
+    SOKOL_ASSERT((num_slots + 1) <= SAUDIO_RING_MAX_SLOTS);
+    ring->head = 0;
+    ring->tail = 0;
+    /* one slot reserved to detect 'full' vs 'empty' */
+    ring->num = num_slots + 1;
+}
+
+_SOKOL_PRIVATE bool _saudio_ring_full(_saudio_ring_t* ring) {
+    return _saudio_ring_idx(ring, ring->head + 1) == ring->tail;
+}
+
+_SOKOL_PRIVATE bool _saudio_ring_empty(_saudio_ring_t* ring) {
+    return ring->head == ring->tail;
+}
+
+_SOKOL_PRIVATE int _saudio_ring_count(_saudio_ring_t* ring) {
+    int count;
+    if (ring->head >= ring->tail) {
+        count = ring->head - ring->tail;
+    }
+    else {
+        count = (ring->head + ring->num) - ring->tail;
+    }
+    SOKOL_ASSERT(count < ring->num);
+    return count;
+}
+
+_SOKOL_PRIVATE void _saudio_ring_enqueue(_saudio_ring_t* ring, int val) {
+    SOKOL_ASSERT(!_saudio_ring_full(ring));
+    ring->queue[ring->head] = val;
+    ring->head = _saudio_ring_idx(ring, ring->head + 1);
+}
+
+_SOKOL_PRIVATE int _saudio_ring_dequeue(_saudio_ring_t* ring) {
+    SOKOL_ASSERT(!_saudio_ring_empty(ring));
+    int val = ring->queue[ring->tail];
+    ring->tail = _saudio_ring_idx(ring, ring->tail + 1);
+    return val;
+}
+
+// ███████ ██ ███████  ██████
+// ██      ██ ██      ██    ██
+// █████   ██ █████   ██    ██
+// ██      ██ ██      ██    ██
+// ██      ██ ██       ██████
+//
+// >>fifo
+_SOKOL_PRIVATE void _saudio_fifo_init_mutex(_saudio_fifo_t* fifo) {
+    /* this must be called before initializing both the backend and the fifo itself! */
+    _saudio_mutex_init(&fifo->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_fifo_destroy_mutex(_saudio_fifo_t* fifo) {
+    _saudio_mutex_destroy(&fifo->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_fifo_init(_saudio_fifo_t* fifo, int packet_size, int num_packets) {
+    /* NOTE: there's a chicken-egg situation during the init phase where the
+        streaming thread must be started before the fifo is actually initialized,
+        thus the fifo init must already be protected from access by the fifo_read() func.
+    */
+    _saudio_mutex_lock(&fifo->mutex);
+    SOKOL_ASSERT((packet_size > 0) && (num_packets > 0));
+    fifo->packet_size = packet_size;
+    fifo->num_packets = num_packets;
+    fifo->base_ptr = (uint8_t*) _saudio_malloc((size_t)(packet_size * num_packets));
+    fifo->cur_packet = -1;
+    fifo->cur_offset = 0;
+    _saudio_ring_init(&fifo->read_queue, num_packets);
+    _saudio_ring_init(&fifo->write_queue, num_packets);
+    for (int i = 0; i < num_packets; i++) {
+        _saudio_ring_enqueue(&fifo->write_queue, i);
+    }
+    SOKOL_ASSERT(_saudio_ring_full(&fifo->write_queue));
+    SOKOL_ASSERT(_saudio_ring_count(&fifo->write_queue) == num_packets);
+    SOKOL_ASSERT(_saudio_ring_empty(&fifo->read_queue));
+    SOKOL_ASSERT(_saudio_ring_count(&fifo->read_queue) == 0);
+    fifo->valid = true;
+    _saudio_mutex_unlock(&fifo->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_fifo_shutdown(_saudio_fifo_t* fifo) {
+    SOKOL_ASSERT(fifo->base_ptr);
+    _saudio_free(fifo->base_ptr);
+    fifo->base_ptr = 0;
+    fifo->valid = false;
+}
+
+_SOKOL_PRIVATE int _saudio_fifo_writable_bytes(_saudio_fifo_t* fifo) {
+    _saudio_mutex_lock(&fifo->mutex);
+    int num_bytes = (_saudio_ring_count(&fifo->write_queue) * fifo->packet_size);
+    if (fifo->cur_packet != -1) {
+        num_bytes += fifo->packet_size - fifo->cur_offset;
+    }
+    _saudio_mutex_unlock(&fifo->mutex);
+    SOKOL_ASSERT((num_bytes >= 0) && (num_bytes <= (fifo->num_packets * fifo->packet_size)));
+    return num_bytes;
+}
+
+/* write new data to the write queue, this is called from main thread */
+_SOKOL_PRIVATE int _saudio_fifo_write(_saudio_fifo_t* fifo, const uint8_t* ptr, int num_bytes) {
+    /* returns the number of bytes written, this will be smaller then requested
+        if the write queue runs full
+    */
+    int all_to_copy = num_bytes;
+    while (all_to_copy > 0) {
+        /* need to grab a new packet? */
+        if (fifo->cur_packet == -1) {
+            _saudio_mutex_lock(&fifo->mutex);
+            if (!_saudio_ring_empty(&fifo->write_queue)) {
+                fifo->cur_packet = _saudio_ring_dequeue(&fifo->write_queue);
+            }
+            _saudio_mutex_unlock(&fifo->mutex);
+            SOKOL_ASSERT(fifo->cur_offset == 0);
+        }
+        /* append data to current write packet */
+        if (fifo->cur_packet != -1) {
+            int to_copy = all_to_copy;
+            const int max_copy = fifo->packet_size - fifo->cur_offset;
+            if (to_copy > max_copy) {
+                to_copy = max_copy;
+            }
+            uint8_t* dst = fifo->base_ptr + fifo->cur_packet * fifo->packet_size + fifo->cur_offset;
+            memcpy(dst, ptr, (size_t)to_copy);
+            ptr += to_copy;
+            fifo->cur_offset += to_copy;
+            all_to_copy -= to_copy;
+            SOKOL_ASSERT(fifo->cur_offset <= fifo->packet_size);
+            SOKOL_ASSERT(all_to_copy >= 0);
+        }
+        else {
+            /* early out if we're starving */
+            int bytes_copied = num_bytes - all_to_copy;
+            SOKOL_ASSERT((bytes_copied >= 0) && (bytes_copied < num_bytes));
+            return bytes_copied;
+        }
+        /* if write packet is full, push to read queue */
+        if (fifo->cur_offset == fifo->packet_size) {
+            _saudio_mutex_lock(&fifo->mutex);
+            _saudio_ring_enqueue(&fifo->read_queue, fifo->cur_packet);
+            _saudio_mutex_unlock(&fifo->mutex);
+            fifo->cur_packet = -1;
+            fifo->cur_offset = 0;
+        }
+    }
+    SOKOL_ASSERT(all_to_copy == 0);
+    return num_bytes;
+}
+
+/* read queued data, this is called form the stream callback (maybe separate thread) */
+_SOKOL_PRIVATE int _saudio_fifo_read(_saudio_fifo_t* fifo, uint8_t* ptr, int num_bytes) {
+    /* NOTE: fifo_read might be called before the fifo is properly initialized */
+    _saudio_mutex_lock(&fifo->mutex);
+    int num_bytes_copied = 0;
+    if (fifo->valid) {
+        SOKOL_ASSERT(0 == (num_bytes % fifo->packet_size));
+        SOKOL_ASSERT(num_bytes <= (fifo->packet_size * fifo->num_packets));
+        const int num_packets_needed = num_bytes / fifo->packet_size;
+        uint8_t* dst = ptr;
+        /* either pull a full buffer worth of data, or nothing */
+        if (_saudio_ring_count(&fifo->read_queue) >= num_packets_needed) {
+            for (int i = 0; i < num_packets_needed; i++) {
+                int packet_index = _saudio_ring_dequeue(&fifo->read_queue);
+                _saudio_ring_enqueue(&fifo->write_queue, packet_index);
+                const uint8_t* src = fifo->base_ptr + packet_index * fifo->packet_size;
+                memcpy(dst, src, (size_t)fifo->packet_size);
+                dst += fifo->packet_size;
+                num_bytes_copied += fifo->packet_size;
+            }
+            SOKOL_ASSERT(num_bytes == num_bytes_copied);
+        }
+    }
+    _saudio_mutex_unlock(&fifo->mutex);
+    return num_bytes_copied;
+}
+
+// ██████  ██    ██ ███    ███ ███    ███ ██    ██
+// ██   ██ ██    ██ ████  ████ ████  ████  ██  ██
+// ██   ██ ██    ██ ██ ████ ██ ██ ████ ██   ████
+// ██   ██ ██    ██ ██  ██  ██ ██  ██  ██    ██
+// ██████   ██████  ██      ██ ██      ██    ██
+//
+// >>dummy
+#if defined(SOKOL_DUMMY_BACKEND)
+_SOKOL_PRIVATE bool _saudio_dummy_backend_init(void) {
+    _saudio.bytes_per_frame = _saudio.num_channels * (int)sizeof(float);
+    return true;
+};
+_SOKOL_PRIVATE void _saudio_dummy_backend_shutdown(void) { };
+
+//  █████  ██      ███████  █████
+// ██   ██ ██      ██      ██   ██
+// ███████ ██      ███████ ███████
+// ██   ██ ██           ██ ██   ██
+// ██   ██ ███████ ███████ ██   ██
+//
+// >>alsa
+#elif defined(_SAUDIO_LINUX)
+
+/* the streaming callback runs in a separate thread */
+_SOKOL_PRIVATE void* _saudio_alsa_cb(void* param) {
+    _SOKOL_UNUSED(param);
+    while (!_saudio.backend.thread_stop) {
+        /* snd_pcm_writei() will be blocking until it needs data */
+        int write_res = snd_pcm_writei(_saudio.backend.device, _saudio.backend.buffer, (snd_pcm_uframes_t)_saudio.backend.buffer_frames);
+        if (write_res < 0) {
+            /* underrun occurred */
+            snd_pcm_prepare(_saudio.backend.device);
+        }
+        else {
+            /* fill the streaming buffer with new data */
+            if (_saudio_has_callback()) {
+                _saudio_stream_callback(_saudio.backend.buffer, _saudio.backend.buffer_frames, _saudio.num_channels);
+            }
+            else {
+                if (0 == _saudio_fifo_read(&_saudio.fifo, (uint8_t*)_saudio.backend.buffer, _saudio.backend.buffer_byte_size)) {
+                    /* not enough read data available, fill the entire buffer with silence */
+                    _saudio_clear(_saudio.backend.buffer, (size_t)_saudio.backend.buffer_byte_size);
+                }
+            }
+        }
+    }
+    return 0;
+}
+
+_SOKOL_PRIVATE bool _saudio_alsa_backend_init(void) {
+    int dir; uint32_t rate;
+    int rc = snd_pcm_open(&_saudio.backend.device, "default", SND_PCM_STREAM_PLAYBACK, 0);
+    if (rc < 0) {
+        _SAUDIO_ERROR(ALSA_SND_PCM_OPEN_FAILED);
+        return false;
+    }
+
+    /* configuration works by restricting the 'configuration space' step
+       by step, we require all parameters except the sample rate to
+       match perfectly
+    */
+    snd_pcm_hw_params_t* params = 0;
+    snd_pcm_hw_params_alloca(&params);
+    snd_pcm_hw_params_any(_saudio.backend.device, params);
+    snd_pcm_hw_params_set_access(_saudio.backend.device, params, SND_PCM_ACCESS_RW_INTERLEAVED);
+    if (0 > snd_pcm_hw_params_set_format(_saudio.backend.device, params, SND_PCM_FORMAT_FLOAT_LE)) {
+        _SAUDIO_ERROR(ALSA_FLOAT_SAMPLES_NOT_SUPPORTED);
+        goto error;
+    }
+    if (0 > snd_pcm_hw_params_set_buffer_size(_saudio.backend.device, params, (snd_pcm_uframes_t)_saudio.buffer_frames)) {
+        _SAUDIO_ERROR(ALSA_REQUESTED_BUFFER_SIZE_NOT_SUPPORTED);
+        goto error;
+    }
+    if (0 > snd_pcm_hw_params_set_channels(_saudio.backend.device, params, (uint32_t)_saudio.num_channels)) {
+        _SAUDIO_ERROR(ALSA_REQUESTED_CHANNEL_COUNT_NOT_SUPPORTED);
+        goto error;
+    }
+    /* let ALSA pick a nearby sampling rate */
+    rate = (uint32_t) _saudio.sample_rate;
+    dir = 0;
+    if (0 > snd_pcm_hw_params_set_rate_near(_saudio.backend.device, params, &rate, &dir)) {
+        _SAUDIO_ERROR(ALSA_SND_PCM_HW_PARAMS_SET_RATE_NEAR_FAILED);
+        goto error;
+    }
+    if (0 > snd_pcm_hw_params(_saudio.backend.device, params)) {
+        _SAUDIO_ERROR(ALSA_SND_PCM_HW_PARAMS_FAILED);
+        goto error;
+    }
+
+    /* read back actual sample rate and channels */
+    _saudio.sample_rate = (int)rate;
+    _saudio.bytes_per_frame = _saudio.num_channels * (int)sizeof(float);
+
+    /* allocate the streaming buffer */
+    _saudio.backend.buffer_byte_size = _saudio.buffer_frames * _saudio.bytes_per_frame;
+    _saudio.backend.buffer_frames = _saudio.buffer_frames;
+    _saudio.backend.buffer = (float*) _saudio_malloc_clear((size_t)_saudio.backend.buffer_byte_size);
+
+    /* create the buffer-streaming start thread */
+    if (0 != pthread_create(&_saudio.backend.thread, 0, _saudio_alsa_cb, 0)) {
+        _SAUDIO_ERROR(ALSA_PTHREAD_CREATE_FAILED);
+        goto error;
+    }
+
+    return true;
+error:
+    if (_saudio.backend.device) {
+        snd_pcm_close(_saudio.backend.device);
+        _saudio.backend.device = 0;
+    }
+    return false;
+};
+
+_SOKOL_PRIVATE void _saudio_alsa_backend_shutdown(void) {
+    SOKOL_ASSERT(_saudio.backend.device);
+    _saudio.backend.thread_stop = true;
+    pthread_join(_saudio.backend.thread, 0);
+    snd_pcm_drain(_saudio.backend.device);
+    snd_pcm_close(_saudio.backend.device);
+    _saudio_free(_saudio.backend.buffer);
+};
+
+// ██     ██  █████  ███████  █████  ██████  ██
+// ██     ██ ██   ██ ██      ██   ██ ██   ██ ██
+// ██  █  ██ ███████ ███████ ███████ ██████  ██
+// ██ ███ ██ ██   ██      ██ ██   ██ ██      ██
+//  ███ ███  ██   ██ ███████ ██   ██ ██      ██
+//
+// >>wasapi
+#elif defined(_SAUDIO_WINDOWS)
+
+/* fill intermediate buffer with new data and reset buffer_pos */
+_SOKOL_PRIVATE void _saudio_wasapi_fill_buffer(void) {
+    if (_saudio_has_callback()) {
+        _saudio_stream_callback(_saudio.backend.thread.src_buffer, _saudio.backend.thread.src_buffer_frames, _saudio.num_channels);
+    }
+    else {
+        if (0 == _saudio_fifo_read(&_saudio.fifo, (uint8_t*)_saudio.backend.thread.src_buffer, _saudio.backend.thread.src_buffer_byte_size)) {
+            /* not enough read data available, fill the entire buffer with silence */
+            _saudio_clear(_saudio.backend.thread.src_buffer, (size_t)_saudio.backend.thread.src_buffer_byte_size);
+        }
+    }
+}
+
+_SOKOL_PRIVATE int _saudio_wasapi_min(int a, int b) {
+    return (a < b) ? a : b;
+}
+
+_SOKOL_PRIVATE void _saudio_wasapi_submit_buffer(int num_frames) {
+    BYTE* wasapi_buffer = 0;
+    if (FAILED(IAudioRenderClient_GetBuffer(_saudio.backend.render_client, num_frames, &wasapi_buffer))) {
+        return;
+    }
+    SOKOL_ASSERT(wasapi_buffer);
+
+    /* copy samples to WASAPI buffer, refill source buffer if needed */
+    int num_remaining_samples = num_frames * _saudio.num_channels;
+    int buffer_pos = _saudio.backend.thread.src_buffer_pos;
+    const int buffer_size_in_samples = _saudio.backend.thread.src_buffer_byte_size / (int)sizeof(float);
+    float* dst = (float*)wasapi_buffer;
+    const float* dst_end = dst + num_remaining_samples;
+    _SOKOL_UNUSED(dst_end); // suppress unused warning in release mode
+    const float* src = _saudio.backend.thread.src_buffer;
+
+    while (num_remaining_samples > 0) {
+        if (0 == buffer_pos) {
+            _saudio_wasapi_fill_buffer();
+        }
+        const int samples_to_copy = _saudio_wasapi_min(num_remaining_samples, buffer_size_in_samples - buffer_pos);
+        SOKOL_ASSERT((buffer_pos + samples_to_copy) <= buffer_size_in_samples);
+        SOKOL_ASSERT((dst + samples_to_copy) <= dst_end);
+        memcpy(dst, &src[buffer_pos], (size_t)samples_to_copy * sizeof(float));
+        num_remaining_samples -= samples_to_copy;
+        SOKOL_ASSERT(num_remaining_samples >= 0);
+        buffer_pos += samples_to_copy;
+        dst += samples_to_copy;
+
+        SOKOL_ASSERT(buffer_pos <= buffer_size_in_samples);
+        if (buffer_pos == buffer_size_in_samples) {
+            buffer_pos = 0;
+        }
+    }
+    _saudio.backend.thread.src_buffer_pos = buffer_pos;
+    IAudioRenderClient_ReleaseBuffer(_saudio.backend.render_client, num_frames, 0);
+}
+
+_SOKOL_PRIVATE DWORD WINAPI _saudio_wasapi_thread_fn(LPVOID param) {
+    (void)param;
+    _saudio_wasapi_submit_buffer(_saudio.backend.thread.src_buffer_frames);
+    IAudioClient_Start(_saudio.backend.audio_client);
+    while (!_saudio.backend.thread.stop) {
+        WaitForSingleObject(_saudio.backend.thread.buffer_end_event, INFINITE);
+        UINT32 padding = 0;
+        if (FAILED(IAudioClient_GetCurrentPadding(_saudio.backend.audio_client, &padding))) {
+            continue;
+        }
+        SOKOL_ASSERT(_saudio.backend.thread.dst_buffer_frames >= padding);
+        int num_frames = (int)_saudio.backend.thread.dst_buffer_frames - (int)padding;
+        if (num_frames > 0) {
+            _saudio_wasapi_submit_buffer(num_frames);
+        }
+    }
+    return 0;
+}
+
+_SOKOL_PRIVATE void _saudio_wasapi_release(void) {
+    if (_saudio.backend.thread.src_buffer) {
+        _saudio_free(_saudio.backend.thread.src_buffer);
+        _saudio.backend.thread.src_buffer = 0;
+    }
+    if (_saudio.backend.render_client) {
+        IAudioRenderClient_Release(_saudio.backend.render_client);
+        _saudio.backend.render_client = 0;
+    }
+    if (_saudio.backend.audio_client) {
+        IAudioClient_Release(_saudio.backend.audio_client);
+        _saudio.backend.audio_client = 0;
+    }
+    if (_saudio.backend.device) {
+        IMMDevice_Release(_saudio.backend.device);
+        _saudio.backend.device = 0;
+    }
+    if (_saudio.backend.device_enumerator) {
+        IMMDeviceEnumerator_Release(_saudio.backend.device_enumerator);
+        _saudio.backend.device_enumerator = 0;
+    }
+    if (0 != _saudio.backend.thread.buffer_end_event) {
+        CloseHandle(_saudio.backend.thread.buffer_end_event);
+        _saudio.backend.thread.buffer_end_event = 0;
+    }
+}
+
+_SOKOL_PRIVATE bool _saudio_wasapi_backend_init(void) {
+    REFERENCE_TIME dur;
+    /* CoInitializeEx could have been called elsewhere already, in which
+        case the function returns with S_FALSE (thus it does not make much
+        sense to check the result)
+    */
+    HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
+    _SOKOL_UNUSED(hr);
+    _saudio.backend.thread.buffer_end_event = CreateEvent(0, FALSE, FALSE, 0);
+    if (0 == _saudio.backend.thread.buffer_end_event) {
+        _SAUDIO_ERROR(WASAPI_CREATE_EVENT_FAILED);
+        goto error;
+    }
+    if (FAILED(CoCreateInstance(_SOKOL_AUDIO_WIN32COM_ID(_saudio_CLSID_IMMDeviceEnumerator),
+        0, CLSCTX_ALL,
+        _SOKOL_AUDIO_WIN32COM_ID(_saudio_IID_IMMDeviceEnumerator),
+        (void**)&_saudio.backend.device_enumerator)))
+    {
+        _SAUDIO_ERROR(WASAPI_CREATE_DEVICE_ENUMERATOR_FAILED);
+        goto error;
+    }
+    if (FAILED(IMMDeviceEnumerator_GetDefaultAudioEndpoint(_saudio.backend.device_enumerator,
+        eRender, eConsole,
+        &_saudio.backend.device)))
+    {
+        _SAUDIO_ERROR(WASAPI_GET_DEFAULT_AUDIO_ENDPOINT_FAILED);
+        goto error;
+    }
+    if (FAILED(IMMDevice_Activate(_saudio.backend.device,
+        _SOKOL_AUDIO_WIN32COM_ID(_saudio_IID_IAudioClient),
+        CLSCTX_ALL, 0,
+        (void**)&_saudio.backend.audio_client)))
+    {
+        _SAUDIO_ERROR(WASAPI_DEVICE_ACTIVATE_FAILED);
+        goto error;
+    }
+
+    WAVEFORMATEXTENSIBLE fmtex;
+    _saudio_clear(&fmtex, sizeof(fmtex));
+    fmtex.Format.nChannels = (WORD)_saudio.num_channels;
+    fmtex.Format.nSamplesPerSec = (DWORD)_saudio.sample_rate;
+    fmtex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
+    fmtex.Format.wBitsPerSample = 32;
+    fmtex.Format.nBlockAlign = (fmtex.Format.nChannels * fmtex.Format.wBitsPerSample) / 8;
+    fmtex.Format.nAvgBytesPerSec = fmtex.Format.nSamplesPerSec * fmtex.Format.nBlockAlign;
+    fmtex.Format.cbSize = 22;   /* WORD + DWORD + GUID */
+    fmtex.Samples.wValidBitsPerSample = 32;
+    if (_saudio.num_channels == 1) {
+        fmtex.dwChannelMask = SPEAKER_FRONT_CENTER;
+    }
+    else {
+        fmtex.dwChannelMask = SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT;
+    }
+    fmtex.SubFormat = _saudio_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
+    dur = (REFERENCE_TIME)
+        (((double)_saudio.buffer_frames) / (((double)_saudio.sample_rate) * (1.0/10000000.0)));
+    if (FAILED(IAudioClient_Initialize(_saudio.backend.audio_client,
+        AUDCLNT_SHAREMODE_SHARED,
+        AUDCLNT_STREAMFLAGS_EVENTCALLBACK|AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM|AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,
+        dur, 0, (WAVEFORMATEX*)&fmtex, 0)))
+    {
+        _SAUDIO_ERROR(WASAPI_AUDIO_CLIENT_INITIALIZE_FAILED);
+        goto error;
+    }
+    if (FAILED(IAudioClient_GetBufferSize(_saudio.backend.audio_client, &_saudio.backend.thread.dst_buffer_frames))) {
+        _SAUDIO_ERROR(WASAPI_AUDIO_CLIENT_GET_BUFFER_SIZE_FAILED);
+        goto error;
+    }
+    if (FAILED(IAudioClient_GetService(_saudio.backend.audio_client,
+        _SOKOL_AUDIO_WIN32COM_ID(_saudio_IID_IAudioRenderClient),
+        (void**)&_saudio.backend.render_client)))
+    {
+        _SAUDIO_ERROR(WASAPI_AUDIO_CLIENT_GET_SERVICE_FAILED);
+        goto error;
+    }
+    if (FAILED(IAudioClient_SetEventHandle(_saudio.backend.audio_client, _saudio.backend.thread.buffer_end_event))) {
+        _SAUDIO_ERROR(WASAPI_AUDIO_CLIENT_SET_EVENT_HANDLE_FAILED);
+        goto error;
+    }
+    _saudio.bytes_per_frame = _saudio.num_channels * (int)sizeof(float);
+    _saudio.backend.thread.src_buffer_frames = _saudio.buffer_frames;
+    _saudio.backend.thread.src_buffer_byte_size = _saudio.backend.thread.src_buffer_frames * _saudio.bytes_per_frame;
+
+    /* allocate an intermediate buffer for sample format conversion */
+    _saudio.backend.thread.src_buffer = (float*) _saudio_malloc((size_t)_saudio.backend.thread.src_buffer_byte_size);
+
+    /* create streaming thread */
+    _saudio.backend.thread.thread_handle = CreateThread(NULL, 0, _saudio_wasapi_thread_fn, 0, 0, 0);
+    if (0 == _saudio.backend.thread.thread_handle) {
+        _SAUDIO_ERROR(WASAPI_CREATE_THREAD_FAILED);
+        goto error;
+    }
+    return true;
+error:
+    _saudio_wasapi_release();
+    return false;
+}
+
+_SOKOL_PRIVATE void _saudio_wasapi_backend_shutdown(void) {
+    if (_saudio.backend.thread.thread_handle) {
+        _saudio.backend.thread.stop = true;
+        SetEvent(_saudio.backend.thread.buffer_end_event);
+        WaitForSingleObject(_saudio.backend.thread.thread_handle, INFINITE);
+        CloseHandle(_saudio.backend.thread.thread_handle);
+        _saudio.backend.thread.thread_handle = 0;
+    }
+    if (_saudio.backend.audio_client) {
+        IAudioClient_Stop(_saudio.backend.audio_client);
+    }
+    _saudio_wasapi_release();
+    CoUninitialize();
+}
+
+// ██     ██ ███████ ██████   █████  ██    ██ ██████  ██  ██████
+// ██     ██ ██      ██   ██ ██   ██ ██    ██ ██   ██ ██ ██    ██
+// ██  █  ██ █████   ██████  ███████ ██    ██ ██   ██ ██ ██    ██
+// ██ ███ ██ ██      ██   ██ ██   ██ ██    ██ ██   ██ ██ ██    ██
+//  ███ ███  ███████ ██████  ██   ██  ██████  ██████  ██  ██████
+//
+// >>webaudio
+#elif defined(_SAUDIO_EMSCRIPTEN)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+EMSCRIPTEN_KEEPALIVE int _saudio_emsc_pull(int num_frames) {
+    SOKOL_ASSERT(_saudio.backend.buffer);
+    if (num_frames == _saudio.buffer_frames) {
+        if (_saudio_has_callback()) {
+            _saudio_stream_callback((float*)_saudio.backend.buffer, num_frames, _saudio.num_channels);
+        }
+        else {
+            const int num_bytes = num_frames * _saudio.bytes_per_frame;
+            if (0 == _saudio_fifo_read(&_saudio.fifo, _saudio.backend.buffer, num_bytes)) {
+                /* not enough read data available, fill the entire buffer with silence */
+                _saudio_clear(_saudio.backend.buffer, (size_t)num_bytes);
+            }
+        }
+        int res = (int) _saudio.backend.buffer;
+        return res;
+    }
+    else {
+        return 0;
+    }
+}
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+/* setup the WebAudio context and attach a ScriptProcessorNode */
+EM_JS(int, saudio_js_init, (int sample_rate, int num_channels, int buffer_size), {
+    Module._saudio_context = null;
+    Module._saudio_node = null;
+    if (typeof AudioContext !== 'undefined') {
+        Module._saudio_context = new AudioContext({
+            sampleRate: sample_rate,
+            latencyHint: 'interactive',
+        });
+    }
+    else {
+        Module._saudio_context = null;
+        console.log('sokol_audio.h: no WebAudio support');
+    }
+    if (Module._saudio_context) {
+        console.log('sokol_audio.h: sample rate ', Module._saudio_context.sampleRate);
+        Module._saudio_node = Module._saudio_context.createScriptProcessor(buffer_size, 0, num_channels);
+        Module._saudio_node.onaudioprocess = (event) => {
+            const num_frames = event.outputBuffer.length;
+            const ptr = __saudio_emsc_pull(num_frames);
+            if (ptr) {
+                const num_channels = event.outputBuffer.numberOfChannels;
+                for (let chn = 0; chn < num_channels; chn++) {
+                    const chan = event.outputBuffer.getChannelData(chn);
+                    for (let i = 0; i < num_frames; i++) {
+                        chan[i] = HEAPF32[(ptr>>2) + ((num_channels*i)+chn)]
+                    }
+                }
+            }
+        };
+        Module._saudio_node.connect(Module._saudio_context.destination);
+
+        // in some browsers, WebAudio needs to be activated on a user action
+        const resume_webaudio = () => {
+            if (Module._saudio_context) {
+                if (Module._saudio_context.state === 'suspended') {
+                    Module._saudio_context.resume();
+                }
+            }
+        };
+        document.addEventListener('click', resume_webaudio, {once:true});
+        document.addEventListener('touchend', resume_webaudio, {once:true});
+        document.addEventListener('keydown', resume_webaudio, {once:true});
+        return 1;
+    }
+    else {
+        return 0;
+    }
+});
+
+/* shutdown the WebAudioContext and ScriptProcessorNode */
+EM_JS(void, saudio_js_shutdown, (void), {
+    \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F
+    const ctx = Module._saudio_context;
+    if (ctx !== null) {
+        if (Module._saudio_node) {
+            Module._saudio_node.disconnect();
+        }
+        ctx.close();
+        Module._saudio_context = null;
+        Module._saudio_node = null;
+    }
+});
+
+/* get the actual sample rate back from the WebAudio context */
+EM_JS(int, saudio_js_sample_rate, (void), {
+    if (Module._saudio_context) {
+        return Module._saudio_context.sampleRate;
+    }
+    else {
+        return 0;
+    }
+});
+
+/* get the actual buffer size in number of frames */
+EM_JS(int, saudio_js_buffer_frames, (void), {
+    if (Module._saudio_node) {
+        return Module._saudio_node.bufferSize;
+    }
+    else {
+        return 0;
+    }
+});
+
+/* return 1 if the WebAudio context is currently suspended, else 0 */
+EM_JS(int, saudio_js_suspended, (void), {
+    if (Module._saudio_context) {
+        if (Module._saudio_context.state === 'suspended') {
+            return 1;
+        }
+        else {
+            return 0;
+        }
+    }
+});
+
+_SOKOL_PRIVATE bool _saudio_webaudio_backend_init(void) {
+    if (saudio_js_init(_saudio.sample_rate, _saudio.num_channels, _saudio.buffer_frames)) {
+        _saudio.bytes_per_frame = (int)sizeof(float) * _saudio.num_channels;
+        _saudio.sample_rate = saudio_js_sample_rate();
+        _saudio.buffer_frames = saudio_js_buffer_frames();
+        const size_t buf_size = (size_t) (_saudio.buffer_frames * _saudio.bytes_per_frame);
+        _saudio.backend.buffer = (uint8_t*) _saudio_malloc(buf_size);
+        return true;
+    }
+    else {
+        return false;
+    }
+}
+
+_SOKOL_PRIVATE void _saudio_webaudio_backend_shutdown(void) {
+    saudio_js_shutdown();
+    if (_saudio.backend.buffer) {
+        _saudio_free(_saudio.backend.buffer);
+        _saudio.backend.buffer = 0;
+    }
+}
+
+//  █████   █████  ██    ██ ██████  ██  ██████
+// ██   ██ ██   ██ ██    ██ ██   ██ ██ ██    ██
+// ███████ ███████ ██    ██ ██   ██ ██ ██    ██
+// ██   ██ ██   ██ ██    ██ ██   ██ ██ ██    ██
+// ██   ██ ██   ██  ██████  ██████  ██  ██████
+//
+// >>aaudio
+#elif defined(SAUDIO_ANDROID_AAUDIO)
+
+_SOKOL_PRIVATE aaudio_data_callback_result_t _saudio_aaudio_data_callback(AAudioStream* stream, void* user_data, void* audio_data, int32_t num_frames) {
+    _SOKOL_UNUSED(user_data);
+    _SOKOL_UNUSED(stream);
+    if (_saudio_has_callback()) {
+        _saudio_stream_callback((float*)audio_data, (int)num_frames, _saudio.num_channels);
+    }
+    else {
+        uint8_t* ptr = (uint8_t*)audio_data;
+        int num_bytes = _saudio.bytes_per_frame * num_frames;
+        if (0 == _saudio_fifo_read(&_saudio.fifo, ptr, num_bytes)) {
+            // not enough read data available, fill the entire buffer with silence
+            memset(ptr, 0, (size_t)num_bytes);
+        }
+    }
+    return AAUDIO_CALLBACK_RESULT_CONTINUE;
+}
+
+_SOKOL_PRIVATE bool _saudio_aaudio_start_stream(void) {
+    if (AAudioStreamBuilder_openStream(_saudio.backend.builder, &_saudio.backend.stream) != AAUDIO_OK) {
+        _SAUDIO_ERROR(AAUDIO_STREAMBUILDER_OPEN_STREAM_FAILED);
+        return false;
+    }
+    AAudioStream_requestStart(_saudio.backend.stream);
+    return true;
+}
+
+_SOKOL_PRIVATE void _saudio_aaudio_stop_stream(void) {
+    if (_saudio.backend.stream) {
+        AAudioStream_requestStop(_saudio.backend.stream);
+        AAudioStream_close(_saudio.backend.stream);
+        _saudio.backend.stream = 0;
+    }
+}
+
+_SOKOL_PRIVATE void* _saudio_aaudio_restart_stream_thread_fn(void* param) {
+    _SOKOL_UNUSED(param);
+    _SAUDIO_WARN(AAUDIO_RESTARTING_STREAM_AFTER_ERROR);
+    pthread_mutex_lock(&_saudio.backend.mutex);
+    _saudio_aaudio_stop_stream();
+    _saudio_aaudio_start_stream();
+    pthread_mutex_unlock(&_saudio.backend.mutex);
+    return 0;
+}
+
+_SOKOL_PRIVATE void _saudio_aaudio_error_callback(AAudioStream* stream, void* user_data, aaudio_result_t error) {
+    _SOKOL_UNUSED(stream);
+    _SOKOL_UNUSED(user_data);
+    if (error == AAUDIO_ERROR_DISCONNECTED) {
+        if (0 != pthread_create(&_saudio.backend.thread, 0, _saudio_aaudio_restart_stream_thread_fn, 0)) {
+            _SAUDIO_ERROR(AAUDIO_PTHREAD_CREATE_FAILED);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void _saudio_aaudio_backend_shutdown(void) {
+    pthread_mutex_lock(&_saudio.backend.mutex);
+    _saudio_aaudio_stop_stream();
+    pthread_mutex_unlock(&_saudio.backend.mutex);
+    if (_saudio.backend.builder) {
+        AAudioStreamBuilder_delete(_saudio.backend.builder);
+        _saudio.backend.builder = 0;
+    }
+    pthread_mutex_destroy(&_saudio.backend.mutex);
+}
+
+_SOKOL_PRIVATE bool _saudio_aaudio_backend_init(void) {
+    _SAUDIO_INFO(USING_AAUDIO_BACKEND);
+
+    _saudio.bytes_per_frame = _saudio.num_channels * (int)sizeof(float);
+
+    pthread_mutexattr_t attr;
+    pthread_mutexattr_init(&attr);
+    pthread_mutex_init(&_saudio.backend.mutex, &attr);
+
+    if (AAudio_createStreamBuilder(&_saudio.backend.builder) != AAUDIO_OK) {
+        _SAUDIO_ERROR(AAUDIO_CREATE_STREAMBUILDER_FAILED);
+        _saudio_aaudio_backend_shutdown();
+        return false;
+    }
+
+    AAudioStreamBuilder_setFormat(_saudio.backend.builder, AAUDIO_FORMAT_PCM_FLOAT);
+    AAudioStreamBuilder_setSampleRate(_saudio.backend.builder, _saudio.sample_rate);
+    AAudioStreamBuilder_setChannelCount(_saudio.backend.builder, _saudio.num_channels);
+    AAudioStreamBuilder_setBufferCapacityInFrames(_saudio.backend.builder, _saudio.buffer_frames * 2);
+    AAudioStreamBuilder_setFramesPerDataCallback(_saudio.backend.builder, _saudio.buffer_frames);
+    AAudioStreamBuilder_setDataCallback(_saudio.backend.builder, _saudio_aaudio_data_callback, 0);
+    AAudioStreamBuilder_setErrorCallback(_saudio.backend.builder, _saudio_aaudio_error_callback, 0);
+
+    if (!_saudio_aaudio_start_stream()) {
+        _saudio_aaudio_backend_shutdown();
+        return false;
+    }
+
+    return true;
+}
+
+//  ██████  ██████  ███████ ███    ██ ███████ ██      ███████ ███████
+// ██    ██ ██   ██ ██      ████   ██ ██      ██      ██      ██
+// ██    ██ ██████  █████   ██ ██  ██ ███████ ██      █████   ███████
+// ██    ██ ██      ██      ██  ██ ██      ██ ██      ██           ██
+//  ██████  ██      ███████ ██   ████ ███████ ███████ ███████ ███████
+//
+//  >>opensles
+//  >>sles
+#elif defined(SAUDIO_ANDROID_SLES)
+
+_SOKOL_PRIVATE void _saudio_sles_semaphore_init(_saudio_sles_semaphore_t* sem) {
+    sem->count = 0;
+    int r = pthread_mutex_init(&sem->mutex, NULL);
+    SOKOL_ASSERT(r == 0);
+    r = pthread_cond_init(&sem->cond, NULL);
+    SOKOL_ASSERT(r == 0);
+    (void)(r);
+}
+
+_SOKOL_PRIVATE void _saudio_sles_semaphore_destroy(_saudio_sles_semaphore_t* sem) {
+    pthread_cond_destroy(&sem->cond);
+    pthread_mutex_destroy(&sem->mutex);
+}
+
+_SOKOL_PRIVATE void _saudio_sles_semaphore_post(_saudio_sles_semaphore_t* sem, int count) {
+    int r = pthread_mutex_lock(&sem->mutex);
+    SOKOL_ASSERT(r == 0);
+    for (int ii = 0; ii < count; ii++) {
+        r = pthread_cond_signal(&sem->cond);
+        SOKOL_ASSERT(r == 0);
+    }
+    sem->count += count;
+    r = pthread_mutex_unlock(&sem->mutex);
+    SOKOL_ASSERT(r == 0);
+    (void)(r);
+}
+
+_SOKOL_PRIVATE bool _saudio_sles_semaphore_wait(_saudio_sles_semaphore_t* sem) {
+    int r = pthread_mutex_lock(&sem->mutex);
+    SOKOL_ASSERT(r == 0);
+    while (r == 0 && sem->count <= 0) {
+        r = pthread_cond_wait(&sem->cond, &sem->mutex);
+    }
+    bool ok = (r == 0);
+    if (ok) {
+        --sem->count;
+    }
+    r = pthread_mutex_unlock(&sem->mutex);
+    (void)(r);
+    return ok;
+}
+
+/* fill intermediate buffer with new data and reset buffer_pos */
+_SOKOL_PRIVATE void _saudio_sles_fill_buffer(void) {
+    int src_buffer_frames = _saudio.buffer_frames;
+    if (_saudio_has_callback()) {
+        _saudio_stream_callback(_saudio.backend.src_buffer, src_buffer_frames, _saudio.num_channels);
+    }
+    else {
+        const int src_buffer_byte_size = src_buffer_frames * _saudio.num_channels * (int)sizeof(float);
+        if (0 == _saudio_fifo_read(&_saudio.fifo, (uint8_t*)_saudio.backend.src_buffer, src_buffer_byte_size)) {
+            /* not enough read data available, fill the entire buffer with silence */
+            _saudio_clear(_saudio.backend.src_buffer, (size_t)src_buffer_byte_size);
+        }
+    }
+}
+
+_SOKOL_PRIVATE void SLAPIENTRY _saudio_sles_play_cb(SLPlayItf player, void *context, SLuint32 event) {
+    _SOKOL_UNUSED(context);
+    _SOKOL_UNUSED(player);
+    if (event & SL_PLAYEVENT_HEADATEND) {
+        _saudio_sles_semaphore_post(&_saudio.backend.buffer_sem, 1);
+    }
+}
+
+_SOKOL_PRIVATE void* _saudio_sles_thread_fn(void* param) {
+    _SOKOL_UNUSED(param);
+    while (!_saudio.backend.thread_stop)  {
+        /* get next output buffer, advance, next buffer. */
+        int16_t* out_buffer = _saudio.backend.output_buffers[_saudio.backend.active_buffer];
+        _saudio.backend.active_buffer = (_saudio.backend.active_buffer + 1) % SAUDIO_SLES_NUM_BUFFERS;
+        int16_t* next_buffer = _saudio.backend.output_buffers[_saudio.backend.active_buffer];
+
+        /* queue this buffer */
+        const int buffer_size_bytes = _saudio.buffer_frames * _saudio.num_channels * (int)sizeof(short);
+        (*_saudio.backend.player_buffer_queue)->Enqueue(_saudio.backend.player_buffer_queue, out_buffer, (SLuint32)buffer_size_bytes);
+
+        /* fill the next buffer */
+        _saudio_sles_fill_buffer();
+        const int num_samples = _saudio.num_channels * _saudio.buffer_frames;
+        for (int i = 0; i < num_samples; ++i) {
+            next_buffer[i] = (int16_t) (_saudio.backend.src_buffer[i] * 0x7FFF);
+        }
+
+        _saudio_sles_semaphore_wait(&_saudio.backend.buffer_sem);
+    }
+
+    return 0;
+}
+
+_SOKOL_PRIVATE void _saudio_sles_backend_shutdown(void) {
+    _saudio.backend.thread_stop = 1;
+    pthread_join(_saudio.backend.thread, 0);
+
+    if (_saudio.backend.player_obj) {
+        (*_saudio.backend.player_obj)->Destroy(_saudio.backend.player_obj);
+    }
+
+    if (_saudio.backend.output_mix_obj) {
+        (*_saudio.backend.output_mix_obj)->Destroy(_saudio.backend.output_mix_obj);
+    }
+
+    if (_saudio.backend.engine_obj) {
+        (*_saudio.backend.engine_obj)->Destroy(_saudio.backend.engine_obj);
+    }
+
+    for (int i = 0; i < SAUDIO_SLES_NUM_BUFFERS; i++) {
+        _saudio_free(_saudio.backend.output_buffers[i]);
+    }
+    _saudio_free(_saudio.backend.src_buffer);
+}
+
+_SOKOL_PRIVATE bool _saudio_sles_backend_init(void) {
+    _SAUDIO_INFO(USING_SLES_BACKEND);
+
+    _saudio.bytes_per_frame = (int)sizeof(float) * _saudio.num_channels;
+
+    for (int i = 0; i < SAUDIO_SLES_NUM_BUFFERS; ++i) {
+        const int buffer_size_bytes = (int)sizeof(int16_t) * _saudio.num_channels * _saudio.buffer_frames;
+        _saudio.backend.output_buffers[i] = (int16_t*) _saudio_malloc_clear((size_t)buffer_size_bytes);
+    }
+
+    {
+        const int buffer_size_bytes = _saudio.bytes_per_frame * _saudio.buffer_frames;
+        _saudio.backend.src_buffer = (float*) _saudio_malloc_clear((size_t)buffer_size_bytes);
+    }
+
+    /* Create engine */
+    const SLEngineOption opts[] = { { SL_ENGINEOPTION_THREADSAFE, SL_BOOLEAN_TRUE } };
+    if (slCreateEngine(&_saudio.backend.engine_obj, 1, opts, 0, NULL, NULL ) != SL_RESULT_SUCCESS) {
+        _SAUDIO_ERROR(SLES_CREATE_ENGINE_FAILED);
+        _saudio_sles_backend_shutdown();
+        return false;
+    }
+
+    (*_saudio.backend.engine_obj)->Realize(_saudio.backend.engine_obj, SL_BOOLEAN_FALSE);
+    if ((*_saudio.backend.engine_obj)->GetInterface(_saudio.backend.engine_obj, SL_IID_ENGINE, &_saudio.backend.engine) != SL_RESULT_SUCCESS) {
+        _SAUDIO_ERROR(SLES_ENGINE_GET_ENGINE_INTERFACE_FAILED);
+        _saudio_sles_backend_shutdown();
+        return false;
+    }
+
+    /* Create output mix. */
+    {
+        const SLInterfaceID ids[] = { SL_IID_VOLUME };
+        const SLboolean req[] = { SL_BOOLEAN_FALSE };
+
+        if ((*_saudio.backend.engine)->CreateOutputMix(_saudio.backend.engine, &_saudio.backend.output_mix_obj, 1, ids, req) != SL_RESULT_SUCCESS) {
+            _SAUDIO_ERROR(SLES_CREATE_OUTPUT_MIX_FAILED);
+            _saudio_sles_backend_shutdown();
+            return false;
+        }
+        (*_saudio.backend.output_mix_obj)->Realize(_saudio.backend.output_mix_obj, SL_BOOLEAN_FALSE);
+
+        if ((*_saudio.backend.output_mix_obj)->GetInterface(_saudio.backend.output_mix_obj, SL_IID_VOLUME, &_saudio.backend.output_mix_vol) != SL_RESULT_SUCCESS) {
+            _SAUDIO_WARN(SLES_MIXER_GET_VOLUME_INTERFACE_FAILED);
+        }
+    }
+
+    /* android buffer queue */
+    _saudio.backend.in_locator.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
+    _saudio.backend.in_locator.numBuffers = SAUDIO_SLES_NUM_BUFFERS;
+
+    /* data format */
+    SLDataFormat_PCM format;
+    format.formatType = SL_DATAFORMAT_PCM;
+    format.numChannels = (SLuint32)_saudio.num_channels;
+    format.samplesPerSec = (SLuint32) (_saudio.sample_rate * 1000);
+    format.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
+    format.containerSize = 16;
+    format.endianness = SL_BYTEORDER_LITTLEENDIAN;
+
+    if (_saudio.num_channels == 2) {
+        format.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+    } else {
+        format.channelMask = SL_SPEAKER_FRONT_CENTER;
+    }
+
+    SLDataSource src;
+    src.pLocator = &_saudio.backend.in_locator;
+    src.pFormat = &format;
+
+    /* Output mix. */
+    _saudio.backend.out_locator.locatorType = SL_DATALOCATOR_OUTPUTMIX;
+    _saudio.backend.out_locator.outputMix = _saudio.backend.output_mix_obj;
+
+    _saudio.backend.dst_data_sink.pLocator = &_saudio.backend.out_locator;
+    _saudio.backend.dst_data_sink.pFormat = NULL;
+
+    /* setup player */
+    {
+        const SLInterfaceID ids[] = { SL_IID_VOLUME, SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
+        const SLboolean req[] = { SL_BOOLEAN_FALSE, SL_BOOLEAN_TRUE };
+
+        if ((*_saudio.backend.engine)->CreateAudioPlayer(_saudio.backend.engine, &_saudio.backend.player_obj, &src, &_saudio.backend.dst_data_sink, sizeof(ids) / sizeof(ids[0]), ids, req) != SL_RESULT_SUCCESS)
+        {
+            _SAUDIO_ERROR(SLES_ENGINE_CREATE_AUDIO_PLAYER_FAILED);
+            _saudio_sles_backend_shutdown();
+            return false;
+        }
+        (*_saudio.backend.player_obj)->Realize(_saudio.backend.player_obj, SL_BOOLEAN_FALSE);
+
+        if ((*_saudio.backend.player_obj)->GetInterface(_saudio.backend.player_obj, SL_IID_PLAY, &_saudio.backend.player) != SL_RESULT_SUCCESS) {
+            _SAUDIO_ERROR(SLES_PLAYER_GET_PLAY_INTERFACE_FAILED);
+            _saudio_sles_backend_shutdown();
+            return false;
+        }
+        if ((*_saudio.backend.player_obj)->GetInterface(_saudio.backend.player_obj, SL_IID_VOLUME, &_saudio.backend.player_vol) != SL_RESULT_SUCCESS) {
+            _SAUDIO_ERROR(SLES_PLAYER_GET_VOLUME_INTERFACE_FAILED);
+        }
+        if ((*_saudio.backend.player_obj)->GetInterface(_saudio.backend.player_obj, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &_saudio.backend.player_buffer_queue) != SL_RESULT_SUCCESS) {
+            _SAUDIO_ERROR(SLES_PLAYER_GET_BUFFERQUEUE_INTERFACE_FAILED);
+            _saudio_sles_backend_shutdown();
+            return false;
+        }
+    }
+
+    /* begin */
+    {
+        const int buffer_size_bytes = (int)sizeof(int16_t) * _saudio.num_channels * _saudio.buffer_frames;
+        (*_saudio.backend.player_buffer_queue)->Enqueue(_saudio.backend.player_buffer_queue, _saudio.backend.output_buffers[0], (SLuint32)buffer_size_bytes);
+        _saudio.backend.active_buffer = (_saudio.backend.active_buffer + 1) % SAUDIO_SLES_NUM_BUFFERS;
+
+        (*_saudio.backend.player)->RegisterCallback(_saudio.backend.player, _saudio_sles_play_cb, NULL);
+        (*_saudio.backend.player)->SetCallbackEventsMask(_saudio.backend.player, SL_PLAYEVENT_HEADATEND);
+        (*_saudio.backend.player)->SetPlayState(_saudio.backend.player, SL_PLAYSTATE_PLAYING);
+    }
+
+    /* create the buffer-streaming start thread */
+    if (0 != pthread_create(&_saudio.backend.thread, 0, _saudio_sles_thread_fn, 0)) {
+        _saudio_sles_backend_shutdown();
+        return false;
+    }
+
+    return true;
+}
+
+//  ██████  ██████  ██████  ███████  █████  ██    ██ ██████  ██  ██████
+// ██      ██    ██ ██   ██ ██      ██   ██ ██    ██ ██   ██ ██ ██    ██
+// ██      ██    ██ ██████  █████   ███████ ██    ██ ██   ██ ██ ██    ██
+// ██      ██    ██ ██   ██ ██      ██   ██ ██    ██ ██   ██ ██ ██    ██
+//  ██████  ██████  ██   ██ ███████ ██   ██  ██████  ██████  ██  ██████
+//
+// >>coreaudio
+#elif defined(_SAUDIO_APPLE)
+
+#if defined(_SAUDIO_IOS)
+#if __has_feature(objc_arc)
+#define _SAUDIO_OBJC_RELEASE(obj) { obj = nil; }
+#else
+#define _SAUDIO_OBJC_RELEASE(obj) { [obj release]; obj = nil; }
+#endif
+
+@interface _saudio_interruption_handler : NSObject { }
+@end
+
+@implementation _saudio_interruption_handler
+-(id)init {
+    self = [super init];
+    AVAudioSession* session = [AVAudioSession sharedInstance];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handle_interruption:) name:AVAudioSessionInterruptionNotification object:session];
+    return self;
+}
+
+-(void)dealloc {
+    [self remove_handler];
+    #if !__has_feature(objc_arc)
+    [super dealloc];
+    #endif
+}
+
+-(void)remove_handler {
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"AVAudioSessionInterruptionNotification" object:nil];
+}
+
+-(void)handle_interruption:(NSNotification*)notification {
+    AVAudioSession* session = [AVAudioSession sharedInstance];
+    SOKOL_ASSERT(session);
+    NSDictionary* dict = notification.userInfo;
+    SOKOL_ASSERT(dict);
+    NSInteger type = [[dict valueForKey:AVAudioSessionInterruptionTypeKey] integerValue];
+    switch (type) {
+        case AVAudioSessionInterruptionTypeBegan:
+            if (_saudio.backend.ca_audio_queue) {
+                AudioQueuePause(_saudio.backend.ca_audio_queue);
+            }
+            [session setActive:false error:nil];
+            break;
+        case AVAudioSessionInterruptionTypeEnded:
+            [session setActive:true error:nil];
+            if (_saudio.backend.ca_audio_queue) {
+                AudioQueueStart(_saudio.backend.ca_audio_queue, NULL);
+            }
+            break;
+        default:
+            break;
+    }
+}
+@end
+#endif // _SAUDIO_IOS
+
+/* NOTE: the buffer data callback is called on a separate thread! */
+_SOKOL_PRIVATE void _saudio_coreaudio_callback(void* user_data, _saudio_AudioQueueRef queue, _saudio_AudioQueueBufferRef buffer) {
+    _SOKOL_UNUSED(user_data);
+    if (_saudio_has_callback()) {
+        const int num_frames = (int)buffer->mAudioDataByteSize / _saudio.bytes_per_frame;
+        const int num_channels = _saudio.num_channels;
+        _saudio_stream_callback((float*)buffer->mAudioData, num_frames, num_channels);
+    }
+    else {
+        uint8_t* ptr = (uint8_t*)buffer->mAudioData;
+        int num_bytes = (int) buffer->mAudioDataByteSize;
+        if (0 == _saudio_fifo_read(&_saudio.fifo, ptr, num_bytes)) {
+            /* not enough read data available, fill the entire buffer with silence */
+            _saudio_clear(ptr, (size_t)num_bytes);
+        }
+    }
+    AudioQueueEnqueueBuffer(queue, buffer, 0, NULL);
+}
+
+_SOKOL_PRIVATE void _saudio_coreaudio_backend_shutdown(void) {
+    if (_saudio.backend.ca_audio_queue) {
+        AudioQueueStop(_saudio.backend.ca_audio_queue, true);
+        AudioQueueDispose(_saudio.backend.ca_audio_queue, false);
+        _saudio.backend.ca_audio_queue = 0;
+    }
+    #if defined(_SAUDIO_IOS)
+        /* remove interruption handler */
+        if (_saudio.backend.ca_interruption_handler != nil) {
+            [_saudio.backend.ca_interruption_handler remove_handler];
+            _SAUDIO_OBJC_RELEASE(_saudio.backend.ca_interruption_handler);
+        }
+        /* deactivate audio session */
+        AVAudioSession* session = [AVAudioSession sharedInstance];
+        SOKOL_ASSERT(session);
+        [session setActive:false error:nil];;
+    #endif // _SAUDIO_IOS
+}
+
+_SOKOL_PRIVATE bool _saudio_coreaudio_backend_init(void) {
+    SOKOL_ASSERT(0 == _saudio.backend.ca_audio_queue);
+
+    #if defined(_SAUDIO_IOS)
+        /* activate audio session */
+        AVAudioSession* session = [AVAudioSession sharedInstance];
+        SOKOL_ASSERT(session != nil);
+        [session setCategory: AVAudioSessionCategoryPlayback error:nil];
+        [session setActive:true error:nil];
+
+        /* create interruption handler */
+        _saudio.backend.ca_interruption_handler = [[_saudio_interruption_handler alloc] init];
+    #endif
+
+    /* create an audio queue with fp32 samples */
+    _saudio_AudioStreamBasicDescription fmt;
+    _saudio_clear(&fmt, sizeof(fmt));
+    fmt.mSampleRate = (double) _saudio.sample_rate;
+    fmt.mFormatID = _saudio_kAudioFormatLinearPCM;
+    fmt.mFormatFlags = _saudio_kLinearPCMFormatFlagIsFloat | _saudio_kAudioFormatFlagIsPacked;
+    fmt.mFramesPerPacket = 1;
+    fmt.mChannelsPerFrame = (uint32_t) _saudio.num_channels;
+    fmt.mBytesPerFrame = (uint32_t)sizeof(float) * (uint32_t)_saudio.num_channels;
+    fmt.mBytesPerPacket = fmt.mBytesPerFrame;
+    fmt.mBitsPerChannel = 32;
+    _saudio_OSStatus res = AudioQueueNewOutput(&fmt, _saudio_coreaudio_callback, 0, NULL, NULL, 0, &_saudio.backend.ca_audio_queue);
+    if (0 != res) {
+        _SAUDIO_ERROR(COREAUDIO_NEW_OUTPUT_FAILED);
+        return false;
+    }
+    SOKOL_ASSERT(_saudio.backend.ca_audio_queue);
+
+    /* create 2 audio buffers */
+    for (int i = 0; i < 2; i++) {
+        _saudio_AudioQueueBufferRef buf = NULL;
+        const uint32_t buf_byte_size = (uint32_t)_saudio.buffer_frames * fmt.mBytesPerFrame;
+        res = AudioQueueAllocateBuffer(_saudio.backend.ca_audio_queue, buf_byte_size, &buf);
+        if (0 != res) {
+            _SAUDIO_ERROR(COREAUDIO_ALLOCATE_BUFFER_FAILED);
+            _saudio_coreaudio_backend_shutdown();
+            return false;
+        }
+        buf->mAudioDataByteSize = buf_byte_size;
+        _saudio_clear(buf->mAudioData, buf->mAudioDataByteSize);
+        AudioQueueEnqueueBuffer(_saudio.backend.ca_audio_queue, buf, 0, NULL);
+    }
+
+    /* init or modify actual playback parameters */
+    _saudio.bytes_per_frame = (int)fmt.mBytesPerFrame;
+
+    /* ...and start playback */
+    res = AudioQueueStart(_saudio.backend.ca_audio_queue, NULL);
+    if (0 != res) {
+        _SAUDIO_ERROR(COREAUDIO_START_FAILED);
+        _saudio_coreaudio_backend_shutdown();
+        return false;
+    }
+    return true;
+}
+
+#else
+#error "unsupported platform"
+#endif
+
+bool _saudio_backend_init(void) {
+    #if defined(SOKOL_DUMMY_BACKEND)
+        return _saudio_dummy_backend_init();
+    #elif defined(_SAUDIO_LINUX)
+        return _saudio_alsa_backend_init();
+    #elif defined(_SAUDIO_WINDOWS)
+        return _saudio_wasapi_backend_init();
+    #elif defined(_SAUDIO_EMSCRIPTEN)
+        return _saudio_webaudio_backend_init();
+    #elif defined(SAUDIO_ANDROID_AAUDIO)
+        return _saudio_aaudio_backend_init();
+    #elif defined(SAUDIO_ANDROID_SLES)
+        return _saudio_sles_backend_init();
+    #elif defined(_SAUDIO_APPLE)
+        return _saudio_coreaudio_backend_init();
+    #else
+    #error "unknown platform"
+    #endif
+}
+
+void _saudio_backend_shutdown(void) {
+    #if defined(SOKOL_DUMMY_BACKEND)
+        _saudio_dummy_backend_shutdown();
+    #elif defined(_SAUDIO_LINUX)
+        _saudio_alsa_backend_shutdown();
+    #elif defined(_SAUDIO_WINDOWS)
+        _saudio_wasapi_backend_shutdown();
+    #elif defined(_SAUDIO_EMSCRIPTEN)
+        _saudio_webaudio_backend_shutdown();
+    #elif defined(SAUDIO_ANDROID_AAUDIO)
+        _saudio_aaudio_backend_shutdown();
+    #elif defined(SAUDIO_ANDROID_SLES)
+        _saudio_sles_backend_shutdown();
+    #elif defined(_SAUDIO_APPLE)
+        return _saudio_coreaudio_backend_shutdown();
+    #else
+    #error "unknown platform"
+    #endif
+}
+
+// ██████  ██    ██ ██████  ██      ██  ██████
+// ██   ██ ██    ██ ██   ██ ██      ██ ██
+// ██████  ██    ██ ██████  ██      ██ ██
+// ██      ██    ██ ██   ██ ██      ██ ██
+// ██       ██████  ██████  ███████ ██  ██████
+//
+// >>public
+SOKOL_API_IMPL void saudio_setup(const saudio_desc* desc) {
+    SOKOL_ASSERT(!_saudio.valid);
+    SOKOL_ASSERT(desc);
+    SOKOL_ASSERT((desc->allocator.alloc && desc->allocator.free) || (!desc->allocator.alloc && !desc->allocator.free));
+    _saudio_clear(&_saudio, sizeof(_saudio));
+    _saudio.desc = *desc;
+    _saudio.stream_cb = desc->stream_cb;
+    _saudio.stream_userdata_cb = desc->stream_userdata_cb;
+    _saudio.user_data = desc->user_data;
+    _saudio.sample_rate = _saudio_def(_saudio.desc.sample_rate, _SAUDIO_DEFAULT_SAMPLE_RATE);
+    _saudio.buffer_frames = _saudio_def(_saudio.desc.buffer_frames, _SAUDIO_DEFAULT_BUFFER_FRAMES);
+    _saudio.packet_frames = _saudio_def(_saudio.desc.packet_frames, _SAUDIO_DEFAULT_PACKET_FRAMES);
+    _saudio.num_packets = _saudio_def(_saudio.desc.num_packets, _SAUDIO_DEFAULT_NUM_PACKETS);
+    _saudio.num_channels = _saudio_def(_saudio.desc.num_channels, 1);
+    _saudio_fifo_init_mutex(&_saudio.fifo);
+    if (_saudio_backend_init()) {
+        /* the backend might not support the requested exact buffer size,
+           make sure the actual buffer size is still a multiple of
+           the requested packet size
+        */
+        if (0 != (_saudio.buffer_frames % _saudio.packet_frames)) {
+            _SAUDIO_ERROR(BACKEND_BUFFER_SIZE_ISNT_MULTIPLE_OF_PACKET_SIZE);
+            _saudio_backend_shutdown();
+            return;
+        }
+        SOKOL_ASSERT(_saudio.bytes_per_frame > 0);
+        _saudio_fifo_init(&_saudio.fifo, _saudio.packet_frames * _saudio.bytes_per_frame, _saudio.num_packets);
+        _saudio.valid = true;
+    }
+    else {
+        _saudio_fifo_destroy_mutex(&_saudio.fifo);
+    }
+}
+
+SOKOL_API_IMPL void saudio_shutdown(void) {
+    if (_saudio.valid) {
+        _saudio_backend_shutdown();
+        _saudio_fifo_shutdown(&_saudio.fifo);
+        _saudio_fifo_destroy_mutex(&_saudio.fifo);
+        _saudio.valid = false;
+    }
+}
+
+SOKOL_API_IMPL bool saudio_isvalid(void) {
+    return _saudio.valid;
+}
+
+SOKOL_API_IMPL void* saudio_userdata(void) {
+    return _saudio.desc.user_data;
+}
+
+SOKOL_API_IMPL saudio_desc saudio_query_desc(void) {
+    return _saudio.desc;
+}
+
+SOKOL_API_IMPL int saudio_sample_rate(void) {
+    return _saudio.sample_rate;
+}
+
+SOKOL_API_IMPL int saudio_buffer_frames(void) {
+    return _saudio.buffer_frames;
+}
+
+SOKOL_API_IMPL int saudio_channels(void) {
+    return _saudio.num_channels;
+}
+
+SOKOL_API_IMPL bool saudio_suspended(void) {
+    #if defined(_SAUDIO_EMSCRIPTEN)
+        if (_saudio.valid) {
+            return 1 == saudio_js_suspended();
+        }
+        else {
+            return false;
+        }
+    #else
+        return false;
+    #endif
+}
+
+SOKOL_API_IMPL int saudio_expect(void) {
+    if (_saudio.valid) {
+        const int num_frames = _saudio_fifo_writable_bytes(&_saudio.fifo) / _saudio.bytes_per_frame;
+        return num_frames;
+    }
+    else {
+        return 0;
+    }
+}
+
+SOKOL_API_IMPL int saudio_push(const float* frames, int num_frames) {
+    SOKOL_ASSERT(frames && (num_frames > 0));
+    if (_saudio.valid) {
+        const int num_bytes = num_frames * _saudio.bytes_per_frame;
+        const int num_written = _saudio_fifo_write(&_saudio.fifo, (const uint8_t*)frames, num_bytes);
+        return num_written / _saudio.bytes_per_frame;
+    }
+    else {
+        return 0;
+    }
+}
+
+#undef _saudio_def
+#undef _saudio_def_flt
+
+#if defined(_SAUDIO_WINDOWS)
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+#endif
+
+#endif /* SOKOL_AUDIO_IMPL */
--- /dev/null
+++ b/src/libs/sokol_time.h
@@ -1,0 +1,324 @@
+#if defined(SOKOL_IMPL) && !defined(SOKOL_TIME_IMPL)
+#define SOKOL_TIME_IMPL
+#endif
+#ifndef SOKOL_TIME_INCLUDED
+/*
+    sokol_time.h    -- simple cross-platform time measurement
+
+    Project URL: https://github.com/floooh/sokol
+
+    Do this:
+        #define SOKOL_IMPL or
+        #define SOKOL_TIME_IMPL
+    before you include this file in *one* C or C++ file to create the
+    implementation.
+
+    Optionally provide the following defines with your own implementations:
+    SOKOL_ASSERT(c)     - your own assert macro (default: assert(c))
+    SOKOL_TIME_API_DECL - public function declaration prefix (default: extern)
+    SOKOL_API_DECL      - same as SOKOL_TIME_API_DECL
+    SOKOL_API_IMPL      - public function implementation prefix (default: -)
+
+    If sokol_time.h is compiled as a DLL, define the following before
+    including the declaration or implementation:
+
+    SOKOL_DLL
+
+    On Windows, SOKOL_DLL will define SOKOL_TIME_API_DECL as __declspec(dllexport)
+    or __declspec(dllimport) as needed.
+
+    void stm_setup();
+        Call once before any other functions to initialize sokol_time
+        (this calls for instance QueryPerformanceFrequency on Windows)
+
+    uint64_t stm_now();
+        Get current point in time in unspecified 'ticks'. The value that
+        is returned has no relation to the 'wall-clock' time and is
+        not in a specific time unit, it is only useful to compute
+        time differences.
+
+    uint64_t stm_diff(uint64_t new, uint64_t old);
+        Computes the time difference between new and old. This will always
+        return a positive, non-zero value.
+
+    uint64_t stm_since(uint64_t start);
+        Takes the current time, and returns the elapsed time since start
+        (this is a shortcut for "stm_diff(stm_now(), start)")
+
+    uint64_t stm_laptime(uint64_t* last_time);
+        This is useful for measuring frame time and other recurring
+        events. It takes the current time, returns the time difference
+        to the value in last_time, and stores the current time in
+        last_time for the next call. If the value in last_time is 0,
+        the return value will be zero (this usually happens on the
+        very first call).
+
+    uint64_t stm_round_to_common_refresh_rate(uint64_t duration)
+        This oddly named function takes a measured frame time and
+        returns the closest "nearby" common display refresh rate frame duration
+        in ticks. If the input duration isn't close to any common display
+        refresh rate, the input duration will be returned unchanged as a fallback.
+        The main purpose of this function is to remove jitter/inaccuracies from
+        measured frame times, and instead use the display refresh rate as
+        frame duration.
+
+    Use the following functions to convert a duration in ticks into
+    useful time units:
+
+    double stm_sec(uint64_t ticks);
+    double stm_ms(uint64_t ticks);
+    double stm_us(uint64_t ticks);
+    double stm_ns(uint64_t ticks);
+        Converts a tick value into seconds, milliseconds, microseconds
+        or nanoseconds. Note that not all platforms will have nanosecond
+        or even microsecond precision.
+
+    Uses the following time measurement functions under the hood:
+
+    Windows:        QueryPerformanceFrequency() / QueryPerformanceCounter()
+    MacOS/iOS:      mach_absolute_time()
+    emscripten:     performance.now()
+    Linux+others:   clock_gettime(CLOCK_MONOTONIC)
+
+    zlib/libpng license
+
+    Copyright (c) 2018 Andre Weissflog
+
+    This software is provided 'as-is', without any express or implied warranty.
+    In no event will the authors be held liable for any damages arising from the
+    use of this software.
+
+    Permission is granted to anyone to use this software for any purpose,
+    including commercial applications, and to alter it and redistribute it
+    freely, subject to the following restrictions:
+
+        1. The origin of this software must not be misrepresented; you must not
+        claim that you wrote the original software. If you use this software in a
+        product, an acknowledgment in the product documentation would be
+        appreciated but is not required.
+
+        2. Altered source versions must be plainly marked as such, and must not
+        be misrepresented as being the original software.
+
+        3. This notice may not be removed or altered from any source
+        distribution.
+*/
+#define SOKOL_TIME_INCLUDED (1)
+#include <stdint.h>
+
+#if defined(SOKOL_API_DECL) && !defined(SOKOL_TIME_API_DECL)
+#define SOKOL_TIME_API_DECL SOKOL_API_DECL
+#endif
+#ifndef SOKOL_TIME_API_DECL
+#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_TIME_IMPL)
+#define SOKOL_TIME_API_DECL __declspec(dllexport)
+#elif defined(_WIN32) && defined(SOKOL_DLL)
+#define SOKOL_TIME_API_DECL __declspec(dllimport)
+#else
+#define SOKOL_TIME_API_DECL extern
+#endif
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+SOKOL_TIME_API_DECL void stm_setup(void);
+SOKOL_TIME_API_DECL uint64_t stm_now(void);
+SOKOL_TIME_API_DECL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks);
+SOKOL_TIME_API_DECL uint64_t stm_since(uint64_t start_ticks);
+SOKOL_TIME_API_DECL uint64_t stm_laptime(uint64_t* last_time);
+SOKOL_TIME_API_DECL uint64_t stm_round_to_common_refresh_rate(uint64_t frame_ticks);
+SOKOL_TIME_API_DECL double stm_sec(uint64_t ticks);
+SOKOL_TIME_API_DECL double stm_ms(uint64_t ticks);
+SOKOL_TIME_API_DECL double stm_us(uint64_t ticks);
+SOKOL_TIME_API_DECL double stm_ns(uint64_t ticks);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+#endif // SOKOL_TIME_INCLUDED
+
+/*-- IMPLEMENTATION ----------------------------------------------------------*/
+#ifdef SOKOL_TIME_IMPL
+#define SOKOL_TIME_IMPL_INCLUDED (1)
+#include <string.h> /* memset */
+
+#ifndef SOKOL_API_IMPL
+    #define SOKOL_API_IMPL
+#endif
+#ifndef SOKOL_ASSERT
+    #include <assert.h>
+    #define SOKOL_ASSERT(c) assert(c)
+#endif
+#ifndef _SOKOL_PRIVATE
+    #if defined(__GNUC__) || defined(__clang__)
+        #define _SOKOL_PRIVATE __attribute__((unused)) static
+    #else
+        #define _SOKOL_PRIVATE static
+    #endif
+#endif
+
+#if defined(_WIN32)
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+typedef struct {
+    uint32_t initialized;
+    LARGE_INTEGER freq;
+    LARGE_INTEGER start;
+} _stm_state_t;
+#elif defined(__APPLE__) && defined(__MACH__)
+#include <mach/mach_time.h>
+typedef struct {
+    uint32_t initialized;
+    mach_timebase_info_data_t timebase;
+    uint64_t start;
+} _stm_state_t;
+#elif defined(__EMSCRIPTEN__)
+#include <emscripten/emscripten.h>
+typedef struct {
+    uint32_t initialized;
+    double start;
+} _stm_state_t;
+#else /* anything else, this will need more care for non-Linux platforms */
+#ifdef ESP8266
+// On the ESP8266, clock_gettime ignores the first argument and CLOCK_MONOTONIC isn't defined
+#define CLOCK_MONOTONIC 0
+#endif
+#include <time.h>
+typedef struct {
+    uint32_t initialized;
+    uint64_t start;
+} _stm_state_t;
+#endif
+static _stm_state_t _stm;
+
+/* prevent 64-bit overflow when computing relative timestamp
+    see https://gist.github.com/jspohr/3dc4f00033d79ec5bdaf67bc46c813e3
+*/
+#if defined(_WIN32) || (defined(__APPLE__) && defined(__MACH__))
+_SOKOL_PRIVATE int64_t int64_muldiv(int64_t value, int64_t numer, int64_t denom) {
+    int64_t q = value / denom;
+    int64_t r = value % denom;
+    return q * numer + r * numer / denom;
+}
+#endif
+
+#if defined(__EMSCRIPTEN__)
+EM_JS(double, stm_js_perfnow, (void), {
+    return performance.now();
+});
+#endif
+
+SOKOL_API_IMPL void stm_setup(void) {
+    memset(&_stm, 0, sizeof(_stm));
+    _stm.initialized = 0xABCDABCD;
+    #if defined(_WIN32)
+        QueryPerformanceFrequency(&_stm.freq);
+        QueryPerformanceCounter(&_stm.start);
+    #elif defined(__APPLE__) && defined(__MACH__)
+        mach_timebase_info(&_stm.timebase);
+        _stm.start = mach_absolute_time();
+    #elif defined(__EMSCRIPTEN__)
+        _stm.start = stm_js_perfnow();
+    #else
+        struct timespec ts;
+        clock_gettime(CLOCK_MONOTONIC, &ts);
+        _stm.start = (uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec;
+    #endif
+}
+
+SOKOL_API_IMPL uint64_t stm_now(void) {
+    SOKOL_ASSERT(_stm.initialized == 0xABCDABCD);
+    uint64_t now;
+    #if defined(_WIN32)
+        LARGE_INTEGER qpc_t;
+        QueryPerformanceCounter(&qpc_t);
+        now = (uint64_t) int64_muldiv(qpc_t.QuadPart - _stm.start.QuadPart, 1000000000, _stm.freq.QuadPart);
+    #elif defined(__APPLE__) && defined(__MACH__)
+        const uint64_t mach_now = mach_absolute_time() - _stm.start;
+        now = (uint64_t) int64_muldiv((int64_t)mach_now, (int64_t)_stm.timebase.numer, (int64_t)_stm.timebase.denom);
+    #elif defined(__EMSCRIPTEN__)
+        double js_now = stm_js_perfnow() - _stm.start;
+        SOKOL_ASSERT(js_now >= 0.0);
+        now = (uint64_t) (js_now * 1000000.0);
+    #else
+        struct timespec ts;
+        clock_gettime(CLOCK_MONOTONIC, &ts);
+        now = ((uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec) - _stm.start;
+    #endif
+    return now;
+}
+
+SOKOL_API_IMPL uint64_t stm_diff(uint64_t new_ticks, uint64_t old_ticks) {
+    if (new_ticks > old_ticks) {
+        return new_ticks - old_ticks;
+    }
+    else {
+        return 1;
+    }
+}
+
+SOKOL_API_IMPL uint64_t stm_since(uint64_t start_ticks) {
+    return stm_diff(stm_now(), start_ticks);
+}
+
+SOKOL_API_IMPL uint64_t stm_laptime(uint64_t* last_time) {
+    SOKOL_ASSERT(last_time);
+    uint64_t dt = 0;
+    uint64_t now = stm_now();
+    if (0 != *last_time) {
+        dt = stm_diff(now, *last_time);
+    }
+    *last_time = now;
+    return dt;
+}
+
+// first number is frame duration in ns, second number is tolerance in ns,
+// the resulting min/max values must not overlap!
+static const uint64_t _stm_refresh_rates[][2] = {
+    { 16666667, 1000000 },  //  60 Hz: 16.6667 +- 1ms
+    { 13888889,  250000 },  //  72 Hz: 13.8889 +- 0.25ms
+    { 13333333,  250000 },  //  75 Hz: 13.3333 +- 0.25ms
+    { 11764706,  250000 },  //  85 Hz: 11.7647 +- 0.25
+    { 11111111,  250000 },  //  90 Hz: 11.1111 +- 0.25ms
+    { 10000000,  500000 },  // 100 Hz: 10.0000 +- 0.5ms
+    {  8333333,  500000 },  // 120 Hz:  8.3333 +- 0.5ms
+    {  6944445,  500000 },  // 144 Hz:  6.9445 +- 0.5ms
+    {  4166667, 1000000 },  // 240 Hz:  4.1666 +- 1ms
+    {        0,       0 },  // keep the last element always at zero
+};
+
+SOKOL_API_IMPL uint64_t stm_round_to_common_refresh_rate(uint64_t ticks) {
+    uint64_t ns;
+    int i = 0;
+    while (0 != (ns = _stm_refresh_rates[i][0])) {
+        uint64_t tol = _stm_refresh_rates[i][1];
+        if ((ticks > (ns - tol)) && (ticks < (ns + tol))) {
+            return ns;
+        }
+        i++;
+    }
+    // fallthough: didn't fit into any buckets
+    return ticks;
+}
+
+SOKOL_API_IMPL double stm_sec(uint64_t ticks) {
+    return (double)ticks / 1000000000.0;
+}
+
+SOKOL_API_IMPL double stm_ms(uint64_t ticks) {
+    return (double)ticks / 1000000.0;
+}
+
+SOKOL_API_IMPL double stm_us(uint64_t ticks) {
+    return (double)ticks / 1000.0;
+}
+
+SOKOL_API_IMPL double stm_ns(uint64_t ticks) {
+    return (double)ticks;
+}
+#endif /* SOKOL_TIME_IMPL */
+
--- /dev/null
+++ b/src/libs/stb_image_write.h
@@ -1,0 +1,1724 @@
+/* stb_image_write - v1.16 - public domain - http://nothings.org/stb
+   writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015
+                                     no warranty implied; use at your own risk
+
+   Before #including,
+
+       #define STB_IMAGE_WRITE_IMPLEMENTATION
+
+   in the file that you want to have the implementation.
+
+   Will probably not work correctly with strict-aliasing optimizations.
+
+ABOUT:
+
+   This header file is a library for writing images to C stdio or a callback.
+
+   The PNG output is not optimal; it is 20-50% larger than the file
+   written by a decent optimizing implementation; though providing a custom
+   zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that.
+   This library is designed for source code compactness and simplicity,
+   not optimal image file size or run-time performance.
+
+BUILDING:
+
+   You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h.
+   You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace
+   malloc,realloc,free.
+   You can #define STBIW_MEMMOVE() to replace memmove()
+   You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function
+   for PNG compression (instead of the builtin one), it must have the following signature:
+   unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality);
+   The returned data will be freed with STBIW_FREE() (free() by default),
+   so it must be heap allocated with STBIW_MALLOC() (malloc() by default),
+
+UNICODE:
+
+   If compiling for Windows and you wish to use Unicode filenames, compile
+   with
+       #define STBIW_WINDOWS_UTF8
+   and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert
+   Windows wchar_t filenames to utf8.
+
+USAGE:
+
+   There are five functions, one for each image file format:
+
+     int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);
+     int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data);
+     int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data);
+     int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality);
+     int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);
+
+     void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically
+
+   There are also five equivalent functions that use an arbitrary write function. You are
+   expected to open/close your file-equivalent before and after calling these:
+
+     int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data, int stride_in_bytes);
+     int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);
+     int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);
+     int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);
+     int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality);
+
+   where the callback is:
+      void stbi_write_func(void *context, void *data, int size);
+
+   You can configure it with these global variables:
+      int stbi_write_tga_with_rle;             // defaults to true; set to 0 to disable RLE
+      int stbi_write_png_compression_level;    // defaults to 8; set to higher for more compression
+      int stbi_write_force_png_filter;         // defaults to -1; set to 0..5 to force a filter mode
+
+
+   You can define STBI_WRITE_NO_STDIO to disable the file variant of these
+   functions, so the library will not use stdio.h at all. However, this will
+   also disable HDR writing, because it requires stdio for formatted output.
+
+   Each function returns 0 on failure and non-0 on success.
+
+   The functions create an image file defined by the parameters. The image
+   is a rectangle of pixels stored from left-to-right, top-to-bottom.
+   Each pixel contains 'comp' channels of data stored interleaved with 8-bits
+   per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is
+   monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall.
+   The *data pointer points to the first byte of the top-left-most pixel.
+   For PNG, "stride_in_bytes" is the distance in bytes from the first byte of
+   a row of pixels to the first byte of the next row of pixels.
+
+   PNG creates output files with the same number of components as the input.
+   The BMP format expands Y to RGB in the file format and does not
+   output alpha.
+
+   PNG supports writing rectangles of data even when the bytes storing rows of
+   data are not consecutive in memory (e.g. sub-rectangles of a larger image),
+   by supplying the stride between the beginning of adjacent rows. The other
+   formats do not. (Thus you cannot write a native-format BMP through the BMP
+   writer, both because it is in BGR order and because it may have padding
+   at the end of the line.)
+
+   PNG allows you to set the deflate compression level by setting the global
+   variable 'stbi_write_png_compression_level' (it defaults to 8).
+
+   HDR expects linear float data. Since the format is always 32-bit rgb(e)
+   data, alpha (if provided) is discarded, and for monochrome data it is
+   replicated across all three channels.
+
+   TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed
+   data, set the global variable 'stbi_write_tga_with_rle' to 0.
+
+   JPEG does ignore alpha channels in input data; quality is between 1 and 100.
+   Higher quality looks better but results in a bigger image.
+   JPEG baseline (no JPEG progressive).
+
+CREDITS:
+
+
+   Sean Barrett           -    PNG/BMP/TGA
+   Baldur Karlsson        -    HDR
+   Jean-Sebastien Guay    -    TGA monochrome
+   Tim Kelsey             -    misc enhancements
+   Alan Hickman           -    TGA RLE
+   Emmanuel Julien        -    initial file IO callback implementation
+   Jon Olick              -    original jo_jpeg.cpp code
+   Daniel Gibson          -    integrate JPEG, allow external zlib
+   Aarni Koskela          -    allow choosing PNG filter
+
+   bugfixes:
+      github:Chribba
+      Guillaume Chereau
+      github:jry2
+      github:romigrou
+      Sergio Gonzalez
+      Jonas Karlsson
+      Filip Wasil
+      Thatcher Ulrich
+      github:poppolopoppo
+      Patrick Boettcher
+      github:xeekworx
+      Cap Petschulat
+      Simon Rodriguez
+      Ivan Tikhonov
+      github:ignotion
+      Adam Schackart
+      Andrew Kensler
+
+LICENSE
+
+  See end of file for license information.
+
+*/
+
+#ifndef INCLUDE_STB_IMAGE_WRITE_H
+#define INCLUDE_STB_IMAGE_WRITE_H
+
+#include <stdlib.h>
+
+// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline'
+#ifndef STBIWDEF
+#ifdef STB_IMAGE_WRITE_STATIC
+#define STBIWDEF  static
+#else
+#ifdef __cplusplus
+#define STBIWDEF  extern "C"
+#else
+#define STBIWDEF  extern
+#endif
+#endif
+#endif
+
+#ifndef STB_IMAGE_WRITE_STATIC  // C++ forbids static forward declarations
+STBIWDEF int stbi_write_tga_with_rle;
+STBIWDEF int stbi_write_png_compression_level;
+STBIWDEF int stbi_write_force_png_filter;
+#endif
+
+#ifndef STBI_WRITE_NO_STDIO
+STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void  *data, int stride_in_bytes);
+STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void  *data);
+STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void  *data);
+STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);
+STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void  *data, int quality);
+
+#ifdef STBIW_WINDOWS_UTF8
+STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input);
+#endif
+#endif
+
+typedef void stbi_write_func(void *context, void *data, int size);
+
+STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data, int stride_in_bytes);
+STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);
+STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void  *data);
+STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);
+STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void  *data, int quality);
+
+STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean);
+
+#endif//INCLUDE_STB_IMAGE_WRITE_H
+
+#ifdef STB_IMAGE_WRITE_IMPLEMENTATION
+
+#ifdef _WIN32
+   #ifndef _CRT_SECURE_NO_WARNINGS
+   #define _CRT_SECURE_NO_WARNINGS
+   #endif
+   #ifndef _CRT_NONSTDC_NO_DEPRECATE
+   #define _CRT_NONSTDC_NO_DEPRECATE
+   #endif
+#endif
+
+#ifndef STBI_WRITE_NO_STDIO
+#include <stdio.h>
+#endif // STBI_WRITE_NO_STDIO
+
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+
+#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED))
+// ok
+#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED)
+// ok
+#else
+#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)."
+#endif
+
+#ifndef STBIW_MALLOC
+#define STBIW_MALLOC(sz)        malloc(sz)
+#define STBIW_REALLOC(p,newsz)  realloc(p,newsz)
+#define STBIW_FREE(p)           free(p)
+#endif
+
+#ifndef STBIW_REALLOC_SIZED
+#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz)
+#endif
+
+
+#ifndef STBIW_MEMMOVE
+#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz)
+#endif
+
+
+#ifndef STBIW_ASSERT
+#include <assert.h>
+#define STBIW_ASSERT(x) assert(x)
+#endif
+
+#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff)
+
+#ifdef STB_IMAGE_WRITE_STATIC
+static int stbi_write_png_compression_level = 8;
+static int stbi_write_tga_with_rle = 1;
+static int stbi_write_force_png_filter = -1;
+#else
+int stbi_write_png_compression_level = 8;
+int stbi_write_tga_with_rle = 1;
+int stbi_write_force_png_filter = -1;
+#endif
+
+static int stbi__flip_vertically_on_write = 0;
+
+STBIWDEF void stbi_flip_vertically_on_write(int flag)
+{
+   stbi__flip_vertically_on_write = flag;
+}
+
+typedef struct
+{
+   stbi_write_func *func;
+   void *context;
+   unsigned char buffer[64];
+   int buf_used;
+} stbi__write_context;
+
+// initialize a callback-based context
+static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context)
+{
+   s->func    = c;
+   s->context = context;
+}
+
+#ifndef STBI_WRITE_NO_STDIO
+
+static void stbi__stdio_write(void *context, void *data, int size)
+{
+   fwrite(data,1,size,(FILE*) context);
+}
+
+#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8)
+#ifdef __cplusplus
+#define STBIW_EXTERN extern "C"
+#else
+#define STBIW_EXTERN extern
+#endif
+STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide);
+STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default);
+
+STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input)
+{
+   return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL);
+}
+#endif
+
+static FILE *stbiw__fopen(char const *filename, char const *mode)
+{
+   FILE *f;
+#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8)
+   wchar_t wMode[64];
+   wchar_t wFilename[1024];
+   if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename)))
+      return 0;
+
+   if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode)))
+      return 0;
+
+#if defined(_MSC_VER) && _MSC_VER >= 1400
+   if (0 != _wfopen_s(&f, wFilename, wMode))
+      f = 0;
+#else
+   f = _wfopen(wFilename, wMode);
+#endif
+
+#elif defined(_MSC_VER) && _MSC_VER >= 1400
+   if (0 != fopen_s(&f, filename, mode))
+      f=0;
+#else
+   f = fopen(filename, mode);
+#endif
+   return f;
+}
+
+static int stbi__start_write_file(stbi__write_context *s, const char *filename)
+{
+   FILE *f = stbiw__fopen(filename, "wb");
+   stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f);
+   return f != NULL;
+}
+
+static void stbi__end_write_file(stbi__write_context *s)
+{
+   fclose((FILE *)s->context);
+}
+
+#endif // !STBI_WRITE_NO_STDIO
+
+typedef unsigned int stbiw_uint32;
+typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1];
+
+static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v)
+{
+   while (*fmt) {
+      switch (*fmt++) {
+         case ' ': break;
+         case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int));
+                     s->func(s->context,&x,1);
+                     break; }
+         case '2': { int x = va_arg(v,int);
+                     unsigned char b[2];
+                     b[0] = STBIW_UCHAR(x);
+                     b[1] = STBIW_UCHAR(x>>8);
+                     s->func(s->context,b,2);
+                     break; }
+         case '4': { stbiw_uint32 x = va_arg(v,int);
+                     unsigned char b[4];
+                     b[0]=STBIW_UCHAR(x);
+                     b[1]=STBIW_UCHAR(x>>8);
+                     b[2]=STBIW_UCHAR(x>>16);
+                     b[3]=STBIW_UCHAR(x>>24);
+                     s->func(s->context,b,4);
+                     break; }
+         default:
+            STBIW_ASSERT(0);
+            return;
+      }
+   }
+}
+
+static void stbiw__writef(stbi__write_context *s, const char *fmt, ...)
+{
+   va_list v;
+   va_start(v, fmt);
+   stbiw__writefv(s, fmt, v);
+   va_end(v);
+}
+
+static void stbiw__write_flush(stbi__write_context *s)
+{
+   if (s->buf_used) {
+      s->func(s->context, &s->buffer, s->buf_used);
+      s->buf_used = 0;
+   }
+}
+
+static void stbiw__putc(stbi__write_context *s, unsigned char c)
+{
+   s->func(s->context, &c, 1);
+}
+
+static void stbiw__write1(stbi__write_context *s, unsigned char a)
+{
+   if ((size_t)s->buf_used + 1 > sizeof(s->buffer))
+      stbiw__write_flush(s);
+   s->buffer[s->buf_used++] = a;
+}
+
+static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c)
+{
+   int n;
+   if ((size_t)s->buf_used + 3 > sizeof(s->buffer))
+      stbiw__write_flush(s);
+   n = s->buf_used;
+   s->buf_used = n+3;
+   s->buffer[n+0] = a;
+   s->buffer[n+1] = b;
+   s->buffer[n+2] = c;
+}
+
+static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d)
+{
+   unsigned char bg[3] = { 255, 0, 255}, px[3];
+   int k;
+
+   if (write_alpha < 0)
+      stbiw__write1(s, d[comp - 1]);
+
+   switch (comp) {
+      case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case
+      case 1:
+         if (expand_mono)
+            stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp
+         else
+            stbiw__write1(s, d[0]);  // monochrome TGA
+         break;
+      case 4:
+         if (!write_alpha) {
+            // composite against pink background
+            for (k = 0; k < 3; ++k)
+               px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255;
+            stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]);
+            break;
+         }
+         /* FALLTHROUGH */
+      case 3:
+         stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]);
+         break;
+   }
+   if (write_alpha > 0)
+      stbiw__write1(s, d[comp - 1]);
+}
+
+static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono)
+{
+   stbiw_uint32 zero = 0;
+   int i,j, j_end;
+
+   if (y <= 0)
+      return;
+
+   if (stbi__flip_vertically_on_write)
+      vdir *= -1;
+
+   if (vdir < 0) {
+      j_end = -1; j = y-1;
+   } else {
+      j_end =  y; j = 0;
+   }
+
+   for (; j != j_end; j += vdir) {
+      for (i=0; i < x; ++i) {
+         unsigned char *d = (unsigned char *) data + (j*x+i)*comp;
+         stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d);
+      }
+      stbiw__write_flush(s);
+      s->func(s->context, &zero, scanline_pad);
+   }
+}
+
+static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...)
+{
+   if (y < 0 || x < 0) {
+      return 0;
+   } else {
+      va_list v;
+      va_start(v, fmt);
+      stbiw__writefv(s, fmt, v);
+      va_end(v);
+      stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono);
+      return 1;
+   }
+}
+
+static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data)
+{
+   if (comp != 4) {
+      // write RGB bitmap
+      int pad = (-x*3) & 3;
+      return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad,
+              "11 4 22 4" "4 44 22 444444",
+              'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40,  // file header
+               40, x,y, 1,24, 0,0,0,0,0,0);             // bitmap header
+   } else {
+      // RGBA bitmaps need a v4 header
+      // use BI_BITFIELDS mode with 32bpp and alpha mask
+      // (straight BI_RGB with alpha mask doesn't work in most readers)
+      return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0,
+         "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444",
+         'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header
+         108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header
+   }
+}
+
+STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data)
+{
+   stbi__write_context s = { 0 };
+   stbi__start_write_callbacks(&s, func, context);
+   return stbi_write_bmp_core(&s, x, y, comp, data);
+}
+
+#ifndef STBI_WRITE_NO_STDIO
+STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data)
+{
+   stbi__write_context s = { 0 };
+   if (stbi__start_write_file(&s,filename)) {
+      int r = stbi_write_bmp_core(&s, x, y, comp, data);
+      stbi__end_write_file(&s);
+      return r;
+   } else
+      return 0;
+}
+#endif //!STBI_WRITE_NO_STDIO
+
+static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data)
+{
+   int has_alpha = (comp == 2 || comp == 4);
+   int colorbytes = has_alpha ? comp-1 : comp;
+   int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3
+
+   if (y < 0 || x < 0)
+      return 0;
+
+   if (!stbi_write_tga_with_rle) {
+      return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0,
+         "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8);
+   } else {
+      int i,j,k;
+      int jend, jdir;
+
+      stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8);
+
+      if (stbi__flip_vertically_on_write) {
+         j = 0;
+         jend = y;
+         jdir = 1;
+      } else {
+         j = y-1;
+         jend = -1;
+         jdir = -1;
+      }
+      for (; j != jend; j += jdir) {
+         unsigned char *row = (unsigned char *) data + j * x * comp;
+         int len;
+
+         for (i = 0; i < x; i += len) {
+            unsigned char *begin = row + i * comp;
+            int diff = 1;
+            len = 1;
+
+            if (i < x - 1) {
+               ++len;
+               diff = memcmp(begin, row + (i + 1) * comp, comp);
+               if (diff) {
+                  const unsigned char *prev = begin;
+                  for (k = i + 2; k < x && len < 128; ++k) {
+                     if (memcmp(prev, row + k * comp, comp)) {
+                        prev += comp;
+                        ++len;
+                     } else {
+                        --len;
+                        break;
+                     }
+                  }
+               } else {
+                  for (k = i + 2; k < x && len < 128; ++k) {
+                     if (!memcmp(begin, row + k * comp, comp)) {
+                        ++len;
+                     } else {
+                        break;
+                     }
+                  }
+               }
+            }
+
+            if (diff) {
+               unsigned char header = STBIW_UCHAR(len - 1);
+               stbiw__write1(s, header);
+               for (k = 0; k < len; ++k) {
+                  stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp);
+               }
+            } else {
+               unsigned char header = STBIW_UCHAR(len - 129);
+               stbiw__write1(s, header);
+               stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin);
+            }
+         }
+      }
+      stbiw__write_flush(s);
+   }
+   return 1;
+}
+
+STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data)
+{
+   stbi__write_context s = { 0 };
+   stbi__start_write_callbacks(&s, func, context);
+   return stbi_write_tga_core(&s, x, y, comp, (void *) data);
+}
+
+#ifndef STBI_WRITE_NO_STDIO
+STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data)
+{
+   stbi__write_context s = { 0 };
+   if (stbi__start_write_file(&s,filename)) {
+      int r = stbi_write_tga_core(&s, x, y, comp, (void *) data);
+      stbi__end_write_file(&s);
+      return r;
+   } else
+      return 0;
+}
+#endif
+
+// *************************************************************************************************
+// Radiance RGBE HDR writer
+// by Baldur Karlsson
+
+#define stbiw__max(a, b)  ((a) > (b) ? (a) : (b))
+
+#ifndef STBI_WRITE_NO_STDIO
+
+static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear)
+{
+   int exponent;
+   float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2]));
+
+   if (maxcomp < 1e-32f) {
+      rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
+   } else {
+      float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp;
+
+      rgbe[0] = (unsigned char)(linear[0] * normalize);
+      rgbe[1] = (unsigned char)(linear[1] * normalize);
+      rgbe[2] = (unsigned char)(linear[2] * normalize);
+      rgbe[3] = (unsigned char)(exponent + 128);
+   }
+}
+
+static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte)
+{
+   unsigned char lengthbyte = STBIW_UCHAR(length+128);
+   STBIW_ASSERT(length+128 <= 255);
+   s->func(s->context, &lengthbyte, 1);
+   s->func(s->context, &databyte, 1);
+}
+
+static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data)
+{
+   unsigned char lengthbyte = STBIW_UCHAR(length);
+   STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code
+   s->func(s->context, &lengthbyte, 1);
+   s->func(s->context, data, length);
+}
+
+static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline)
+{
+   unsigned char scanlineheader[4] = { 2, 2, 0, 0 };
+   unsigned char rgbe[4];
+   float linear[3];
+   int x;
+
+   scanlineheader[2] = (width&0xff00)>>8;
+   scanlineheader[3] = (width&0x00ff);
+
+   /* skip RLE for images too small or large */
+   if (width < 8 || width >= 32768) {
+      for (x=0; x < width; x++) {
+         switch (ncomp) {
+            case 4: /* fallthrough */
+            case 3: linear[2] = scanline[x*ncomp + 2];
+                    linear[1] = scanline[x*ncomp + 1];
+                    linear[0] = scanline[x*ncomp + 0];
+                    break;
+            default:
+                    linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0];
+                    break;
+         }
+         stbiw__linear_to_rgbe(rgbe, linear);
+         s->func(s->context, rgbe, 4);
+      }
+   } else {
+      int c,r;
+      /* encode into scratch buffer */
+      for (x=0; x < width; x++) {
+         switch(ncomp) {
+            case 4: /* fallthrough */
+            case 3: linear[2] = scanline[x*ncomp + 2];
+                    linear[1] = scanline[x*ncomp + 1];
+                    linear[0] = scanline[x*ncomp + 0];
+                    break;
+            default:
+                    linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0];
+                    break;
+         }
+         stbiw__linear_to_rgbe(rgbe, linear);
+         scratch[x + width*0] = rgbe[0];
+         scratch[x + width*1] = rgbe[1];
+         scratch[x + width*2] = rgbe[2];
+         scratch[x + width*3] = rgbe[3];
+      }
+
+      s->func(s->context, scanlineheader, 4);
+
+      /* RLE each component separately */
+      for (c=0; c < 4; c++) {
+         unsigned char *comp = &scratch[width*c];
+
+         x = 0;
+         while (x < width) {
+            // find first run
+            r = x;
+            while (r+2 < width) {
+               if (comp[r] == comp[r+1] && comp[r] == comp[r+2])
+                  break;
+               ++r;
+            }
+            if (r+2 >= width)
+               r = width;
+            // dump up to first run
+            while (x < r) {
+               int len = r-x;
+               if (len > 128) len = 128;
+               stbiw__write_dump_data(s, len, &comp[x]);
+               x += len;
+            }
+            // if there's a run, output it
+            if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd
+               // find next byte after run
+               while (r < width && comp[r] == comp[x])
+                  ++r;
+               // output run up to r
+               while (x < r) {
+                  int len = r-x;
+                  if (len > 127) len = 127;
+                  stbiw__write_run_data(s, len, comp[x]);
+                  x += len;
+               }
+            }
+         }
+      }
+   }
+}
+
+static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data)
+{
+   if (y <= 0 || x <= 0 || data == NULL)
+      return 0;
+   else {
+      // Each component is stored separately. Allocate scratch space for full output scanline.
+      unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4);
+      int i, len;
+      char buffer[128];
+      char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n";
+      s->func(s->context, header, sizeof(header)-1);
+
+#ifdef __STDC_LIB_EXT1__
+      len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE=          1.0000000000000\n\n-Y %d +X %d\n", y, x);
+#else
+      len = sprintf(buffer, "EXPOSURE=          1.0000000000000\n\n-Y %d +X %d\n", y, x);
+#endif
+      s->func(s->context, buffer, len);
+
+      for(i=0; i < y; i++)
+         stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i));
+      STBIW_FREE(scratch);
+      return 1;
+   }
+}
+
+STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data)
+{
+   stbi__write_context s = { 0 };
+   stbi__start_write_callbacks(&s, func, context);
+   return stbi_write_hdr_core(&s, x, y, comp, (float *) data);
+}
+
+STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data)
+{
+   stbi__write_context s = { 0 };
+   if (stbi__start_write_file(&s,filename)) {
+      int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data);
+      stbi__end_write_file(&s);
+      return r;
+   } else
+      return 0;
+}
+#endif // STBI_WRITE_NO_STDIO
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// PNG writer
+//
+
+#ifndef STBIW_ZLIB_COMPRESS
+// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size()
+#define stbiw__sbraw(a) ((int *) (void *) (a) - 2)
+#define stbiw__sbm(a)   stbiw__sbraw(a)[0]
+#define stbiw__sbn(a)   stbiw__sbraw(a)[1]
+
+#define stbiw__sbneedgrow(a,n)  ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a))
+#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0)
+#define stbiw__sbgrow(a,n)  stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a)))
+
+#define stbiw__sbpush(a, v)      (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v))
+#define stbiw__sbcount(a)        ((a) ? stbiw__sbn(a) : 0)
+#define stbiw__sbfree(a)         ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0)
+
+static void *stbiw__sbgrowf(void **arr, int increment, int itemsize)
+{
+   int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1;
+   void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2);
+   STBIW_ASSERT(p);
+   if (p) {
+      if (!*arr) ((int *) p)[1] = 0;
+      *arr = (void *) ((int *) p + 2);
+      stbiw__sbm(*arr) = m;
+   }
+   return *arr;
+}
+
+static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount)
+{
+   while (*bitcount >= 8) {
+      stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer));
+      *bitbuffer >>= 8;
+      *bitcount -= 8;
+   }
+   return data;
+}
+
+static int stbiw__zlib_bitrev(int code, int codebits)
+{
+   int res=0;
+   while (codebits--) {
+      res = (res << 1) | (code & 1);
+      code >>= 1;
+   }
+   return res;
+}
+
+static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit)
+{
+   int i;
+   for (i=0; i < limit && i < 258; ++i)
+      if (a[i] != b[i]) break;
+   return i;
+}
+
+static unsigned int stbiw__zhash(unsigned char *data)
+{
+   stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16);
+   hash ^= hash << 3;
+   hash += hash >> 5;
+   hash ^= hash << 4;
+   hash += hash >> 17;
+   hash ^= hash << 25;
+   hash += hash >> 6;
+   return hash;
+}
+
+#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount))
+#define stbiw__zlib_add(code,codebits) \
+      (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush())
+#define stbiw__zlib_huffa(b,c)  stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c)
+// default huffman tables
+#define stbiw__zlib_huff1(n)  stbiw__zlib_huffa(0x30 + (n), 8)
+#define stbiw__zlib_huff2(n)  stbiw__zlib_huffa(0x190 + (n)-144, 9)
+#define stbiw__zlib_huff3(n)  stbiw__zlib_huffa(0 + (n)-256,7)
+#define stbiw__zlib_huff4(n)  stbiw__zlib_huffa(0xc0 + (n)-280,8)
+#define stbiw__zlib_huff(n)  ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n))
+#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n))
+
+#define stbiw__ZHASH   16384
+
+#endif // STBIW_ZLIB_COMPRESS
+
+STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality)
+{
+#ifdef STBIW_ZLIB_COMPRESS
+   // user provided a zlib compress implementation, use that
+   return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality);
+#else // use builtin
+   static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 };
+   static unsigned char  lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4,  4,  5,  5,  5,  5,  0 };
+   static unsigned short distc[]   = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 };
+   static unsigned char  disteb[]  = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 };
+   unsigned int bitbuf=0;
+   int i,j, bitcount=0;
+   unsigned char *out = NULL;
+   unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**));
+   if (hash_table == NULL)
+      return NULL;
+   if (quality < 5) quality = 5;
+
+   stbiw__sbpush(out, 0x78);   // DEFLATE 32K window
+   stbiw__sbpush(out, 0x5e);   // FLEVEL = 1
+   stbiw__zlib_add(1,1);  // BFINAL = 1
+   stbiw__zlib_add(1,2);  // BTYPE = 1 -- fixed huffman
+
+   for (i=0; i < stbiw__ZHASH; ++i)
+      hash_table[i] = NULL;
+
+   i=0;
+   while (i < data_len-3) {
+      // hash next 3 bytes of data to be compressed
+      int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3;
+      unsigned char *bestloc = 0;
+      unsigned char **hlist = hash_table[h];
+      int n = stbiw__sbcount(hlist);
+      for (j=0; j < n; ++j) {
+         if (hlist[j]-data > i-32768) { // if entry lies within window
+            int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i);
+            if (d >= best) { best=d; bestloc=hlist[j]; }
+         }
+      }
+      // when hash table entry is too long, delete half the entries
+      if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) {
+         STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality);
+         stbiw__sbn(hash_table[h]) = quality;
+      }
+      stbiw__sbpush(hash_table[h],data+i);
+
+      if (bestloc) {
+         // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal
+         h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1);
+         hlist = hash_table[h];
+         n = stbiw__sbcount(hlist);
+         for (j=0; j < n; ++j) {
+            if (hlist[j]-data > i-32767) {
+               int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1);
+               if (e > best) { // if next match is better, bail on current match
+                  bestloc = NULL;
+                  break;
+               }
+            }
+         }
+      }
+
+      if (bestloc) {
+         int d = (int) (data+i - bestloc); // distance back
+         STBIW_ASSERT(d <= 32767 && best <= 258);
+         for (j=0; best > lengthc[j+1]-1; ++j);
+         stbiw__zlib_huff(j+257);
+         if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]);
+         for (j=0; d > distc[j+1]-1; ++j);
+         stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5);
+         if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]);
+         i += best;
+      } else {
+         stbiw__zlib_huffb(data[i]);
+         ++i;
+      }
+   }
+   // write out final bytes
+   for (;i < data_len; ++i)
+      stbiw__zlib_huffb(data[i]);
+   stbiw__zlib_huff(256); // end of block
+   // pad with 0 bits to byte boundary
+   while (bitcount)
+      stbiw__zlib_add(0,1);
+
+   for (i=0; i < stbiw__ZHASH; ++i)
+      (void) stbiw__sbfree(hash_table[i]);
+   STBIW_FREE(hash_table);
+
+   // store uncompressed instead if compression was worse
+   if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) {
+      stbiw__sbn(out) = 2;  // truncate to DEFLATE 32K window and FLEVEL = 1
+      for (j = 0; j < data_len;) {
+         int blocklen = data_len - j;
+         if (blocklen > 32767) blocklen = 32767;
+         stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression
+         stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN
+         stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8));
+         stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN
+         stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8));
+         memcpy(out+stbiw__sbn(out), data+j, blocklen);
+         stbiw__sbn(out) += blocklen;
+         j += blocklen;
+      }
+   }
+
+   {
+      // compute adler32 on input
+      unsigned int s1=1, s2=0;
+      int blocklen = (int) (data_len % 5552);
+      j=0;
+      while (j < data_len) {
+         for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; }
+         s1 %= 65521; s2 %= 65521;
+         j += blocklen;
+         blocklen = 5552;
+      }
+      stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8));
+      stbiw__sbpush(out, STBIW_UCHAR(s2));
+      stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8));
+      stbiw__sbpush(out, STBIW_UCHAR(s1));
+   }
+   *out_len = stbiw__sbn(out);
+   // make returned pointer freeable
+   STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len);
+   return (unsigned char *) stbiw__sbraw(out);
+#endif // STBIW_ZLIB_COMPRESS
+}
+
+static unsigned int stbiw__crc32(unsigned char *buffer, int len)
+{
+#ifdef STBIW_CRC32
+    return STBIW_CRC32(buffer, len);
+#else
+   static unsigned int crc_table[256] =
+   {
+      0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
+      0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
+      0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
+      0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
+      0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
+      0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
+      0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
+      0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
+      0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
+      0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
+      0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
+      0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+      0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
+      0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
+      0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
+      0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
+      0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
+      0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
+      0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
+      0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
+      0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+      0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
+      0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
+      0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
+      0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
+      0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
+      0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+      0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
+      0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
+      0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
+      0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
+      0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
+   };
+
+   unsigned int crc = ~0u;
+   int i;
+   for (i=0; i < len; ++i)
+      crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)];
+   return ~crc;
+#endif
+}
+
+#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4)
+#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v));
+#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3])
+
+static void stbiw__wpcrc(unsigned char **data, int len)
+{
+   unsigned int crc = stbiw__crc32(*data - len - 4, len+4);
+   stbiw__wp32(*data, crc);
+}
+
+static unsigned char stbiw__paeth(int a, int b, int c)
+{
+   int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c);
+   if (pa <= pb && pa <= pc) return STBIW_UCHAR(a);
+   if (pb <= pc) return STBIW_UCHAR(b);
+   return STBIW_UCHAR(c);
+}
+
+// @OPTIMIZE: provide an option that always forces left-predict or paeth predict
+static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer)
+{
+   static int mapping[] = { 0,1,2,3,4 };
+   static int firstmap[] = { 0,1,0,5,6 };
+   int *mymap = (y != 0) ? mapping : firstmap;
+   int i;
+   int type = mymap[filter_type];
+   unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y);
+   int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes;
+
+   if (type==0) {
+      memcpy(line_buffer, z, width*n);
+      return;
+   }
+
+   // first loop isn't optimized since it's just one pixel
+   for (i = 0; i < n; ++i) {
+      switch (type) {
+         case 1: line_buffer[i] = z[i]; break;
+         case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break;
+         case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break;
+         case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break;
+         case 5: line_buffer[i] = z[i]; break;
+         case 6: line_buffer[i] = z[i]; break;
+      }
+   }
+   switch (type) {
+      case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break;
+      case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break;
+      case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break;
+      case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break;
+      case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break;
+      case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break;
+   }
+}
+
+STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len)
+{
+   int force_filter = stbi_write_force_png_filter;
+   int ctype[5] = { -1, 0, 4, 2, 6 };
+   unsigned char sig[8] = { 137,80,78,71,13,10,26,10 };
+   unsigned char *out,*o, *filt, *zlib;
+   signed char *line_buffer;
+   int j,zlen;
+
+   if (stride_bytes == 0)
+      stride_bytes = x * n;
+
+   if (force_filter >= 5) {
+      force_filter = -1;
+   }
+
+   filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0;
+   line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; }
+   for (j=0; j < y; ++j) {
+      int filter_type;
+      if (force_filter > -1) {
+         filter_type = force_filter;
+         stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer);
+      } else { // Estimate the best filter by running through all of them:
+         int best_filter = 0, best_filter_val = 0x7fffffff, est, i;
+         for (filter_type = 0; filter_type < 5; filter_type++) {
+            stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer);
+
+            // Estimate the entropy of the line using this filter; the less, the better.
+            est = 0;
+            for (i = 0; i < x*n; ++i) {
+               est += abs((signed char) line_buffer[i]);
+            }
+            if (est < best_filter_val) {
+               best_filter_val = est;
+               best_filter = filter_type;
+            }
+         }
+         if (filter_type != best_filter) {  // If the last iteration already got us the best filter, don't redo it
+            stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer);
+            filter_type = best_filter;
+         }
+      }
+      // when we get here, filter_type contains the filter type, and line_buffer contains the data
+      filt[j*(x*n+1)] = (unsigned char) filter_type;
+      STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n);
+   }
+   STBIW_FREE(line_buffer);
+   zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level);
+   STBIW_FREE(filt);
+   if (!zlib) return 0;
+
+   // each tag requires 12 bytes of overhead
+   out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12);
+   if (!out) return 0;
+   *out_len = 8 + 12+13 + 12+zlen + 12;
+
+   o=out;
+   STBIW_MEMMOVE(o,sig,8); o+= 8;
+   stbiw__wp32(o, 13); // header length
+   stbiw__wptag(o, "IHDR");
+   stbiw__wp32(o, x);
+   stbiw__wp32(o, y);
+   *o++ = 8;
+   *o++ = STBIW_UCHAR(ctype[n]);
+   *o++ = 0;
+   *o++ = 0;
+   *o++ = 0;
+   stbiw__wpcrc(&o,13);
+
+   stbiw__wp32(o, zlen);
+   stbiw__wptag(o, "IDAT");
+   STBIW_MEMMOVE(o, zlib, zlen);
+   o += zlen;
+   STBIW_FREE(zlib);
+   stbiw__wpcrc(&o, zlen);
+
+   stbiw__wp32(o,0);
+   stbiw__wptag(o, "IEND");
+   stbiw__wpcrc(&o,0);
+
+   STBIW_ASSERT(o == out + *out_len);
+
+   return out;
+}
+
+#ifndef STBI_WRITE_NO_STDIO
+STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes)
+{
+   FILE *f;
+   int len;
+   unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);
+   if (png == NULL) return 0;
+
+   f = stbiw__fopen(filename, "wb");
+   if (!f) { STBIW_FREE(png); return 0; }
+   fwrite(png, 1, len, f);
+   fclose(f);
+   STBIW_FREE(png);
+   return 1;
+}
+#endif
+
+STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes)
+{
+   int len;
+   unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);
+   if (png == NULL) return 0;
+   func(context, png, len);
+   STBIW_FREE(png);
+   return 1;
+}
+
+
+/* ***************************************************************************
+ *
+ * JPEG writer
+ *
+ * This is based on Jon Olick's jo_jpeg.cpp:
+ * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html
+ */
+
+static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,
+      24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 };
+
+static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) {
+   int bitBuf = *bitBufP, bitCnt = *bitCntP;
+   bitCnt += bs[1];
+   bitBuf |= bs[0] << (24 - bitCnt);
+   while(bitCnt >= 8) {
+      unsigned char c = (bitBuf >> 16) & 255;
+      stbiw__putc(s, c);
+      if(c == 255) {
+         stbiw__putc(s, 0);
+      }
+      bitBuf <<= 8;
+      bitCnt -= 8;
+   }
+   *bitBufP = bitBuf;
+   *bitCntP = bitCnt;
+}
+
+static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) {
+   float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p;
+   float z1, z2, z3, z4, z5, z11, z13;
+
+   float tmp0 = d0 + d7;
+   float tmp7 = d0 - d7;
+   float tmp1 = d1 + d6;
+   float tmp6 = d1 - d6;
+   float tmp2 = d2 + d5;
+   float tmp5 = d2 - d5;
+   float tmp3 = d3 + d4;
+   float tmp4 = d3 - d4;
+
+   // Even part
+   float tmp10 = tmp0 + tmp3;   // phase 2
+   float tmp13 = tmp0 - tmp3;
+   float tmp11 = tmp1 + tmp2;
+   float tmp12 = tmp1 - tmp2;
+
+   d0 = tmp10 + tmp11;       // phase 3
+   d4 = tmp10 - tmp11;
+
+   z1 = (tmp12 + tmp13) * 0.707106781f; // c4
+   d2 = tmp13 + z1;       // phase 5
+   d6 = tmp13 - z1;
+
+   // Odd part
+   tmp10 = tmp4 + tmp5;       // phase 2
+   tmp11 = tmp5 + tmp6;
+   tmp12 = tmp6 + tmp7;
+
+   // The rotator is modified from fig 4-8 to avoid extra negations.
+   z5 = (tmp10 - tmp12) * 0.382683433f; // c6
+   z2 = tmp10 * 0.541196100f + z5; // c2-c6
+   z4 = tmp12 * 1.306562965f + z5; // c2+c6
+   z3 = tmp11 * 0.707106781f; // c4
+
+   z11 = tmp7 + z3;      // phase 5
+   z13 = tmp7 - z3;
+
+   *d5p = z13 + z2;         // phase 6
+   *d3p = z13 - z2;
+   *d1p = z11 + z4;
+   *d7p = z11 - z4;
+
+   *d0p = d0;  *d2p = d2;  *d4p = d4;  *d6p = d6;
+}
+
+static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) {
+   int tmp1 = val < 0 ? -val : val;
+   val = val < 0 ? val-1 : val;
+   bits[1] = 1;
+   while(tmp1 >>= 1) {
+      ++bits[1];
+   }
+   bits[0] = val & ((1<<bits[1])-1);
+}
+
+static int stbiw__jpg_processDU(stbi__write_context *s, int *bitBuf, int *bitCnt, float *CDU, int du_stride, float *fdtbl, int DC, const unsigned short HTDC[256][2], const unsigned short HTAC[256][2]) {
+   const unsigned short EOB[2] = { HTAC[0x00][0], HTAC[0x00][1] };
+   const unsigned short M16zeroes[2] = { HTAC[0xF0][0], HTAC[0xF0][1] };
+   int dataOff, i, j, n, diff, end0pos, x, y;
+   int DU[64];
+
+   // DCT rows
+   for(dataOff=0, n=du_stride*8; dataOff<n; dataOff+=du_stride) {
+      stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+1], &CDU[dataOff+2], &CDU[dataOff+3], &CDU[dataOff+4], &CDU[dataOff+5], &CDU[dataOff+6], &CDU[dataOff+7]);
+   }
+   // DCT columns
+   for(dataOff=0; dataOff<8; ++dataOff) {
+      stbiw__jpg_DCT(&CDU[dataOff], &CDU[dataOff+du_stride], &CDU[dataOff+du_stride*2], &CDU[dataOff+du_stride*3], &CDU[dataOff+du_stride*4],
+                     &CDU[dataOff+du_stride*5], &CDU[dataOff+du_stride*6], &CDU[dataOff+du_stride*7]);
+   }
+   // Quantize/descale/zigzag the coefficients
+   for(y = 0, j=0; y < 8; ++y) {
+      for(x = 0; x < 8; ++x,++j) {
+         float v;
+         i = y*du_stride+x;
+         v = CDU[i]*fdtbl[j];
+         // DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? ceilf(v - 0.5f) : floorf(v + 0.5f));
+         // ceilf() and floorf() are C99, not C89, but I /think/ they're not needed here anyway?
+         DU[stbiw__jpg_ZigZag[j]] = (int)(v < 0 ? v - 0.5f : v + 0.5f);
+      }
+   }
+
+   // Encode DC
+   diff = DU[0] - DC;
+   if (diff == 0) {
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[0]);
+   } else {
+      unsigned short bits[2];
+      stbiw__jpg_calcBits(diff, bits);
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTDC[bits[1]]);
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits);
+   }
+   // Encode ACs
+   end0pos = 63;
+   for(; (end0pos>0)&&(DU[end0pos]==0); --end0pos) {
+   }
+   // end0pos = first element in reverse order !=0
+   if(end0pos == 0) {
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB);
+      return DU[0];
+   }
+   for(i = 1; i <= end0pos; ++i) {
+      int startpos = i;
+      int nrzeroes;
+      unsigned short bits[2];
+      for (; DU[i]==0 && i<=end0pos; ++i) {
+      }
+      nrzeroes = i-startpos;
+      if ( nrzeroes >= 16 ) {
+         int lng = nrzeroes>>4;
+         int nrmarker;
+         for (nrmarker=1; nrmarker <= lng; ++nrmarker)
+            stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes);
+         nrzeroes &= 15;
+      }
+      stbiw__jpg_calcBits(DU[i], bits);
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]);
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits);
+   }
+   if(end0pos != 63) {
+      stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB);
+   }
+   return DU[0];
+}
+
+static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) {
+   // Constants that don't pollute global namespace
+   static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0};
+   static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11};
+   static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d};
+   static const unsigned char std_ac_luminance_values[] = {
+      0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08,
+      0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28,
+      0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59,
+      0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89,
+      0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6,
+      0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2,
+      0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa
+   };
+   static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0};
+   static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11};
+   static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77};
+   static const unsigned char std_ac_chrominance_values[] = {
+      0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91,
+      0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26,
+      0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,
+      0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87,
+      0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,
+      0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,
+      0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa
+   };
+   // Huffman tables
+   static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}};
+   static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}};
+   static const unsigned short YAC_HT[256][2] = {
+      {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0}
+   };
+   static const unsigned short UVAC_HT[256][2] = {
+      {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0},
+      {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0}
+   };
+   static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,
+                             37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99};
+   static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,
+                              99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99};
+   static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f,
+                                 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f };
+
+   int row, col, i, k, subsample;
+   float fdtbl_Y[64], fdtbl_UV[64];
+   unsigned char YTable[64], UVTable[64];
+
+   if(!data || !width || !height || comp > 4 || comp < 1) {
+      return 0;
+   }
+
+   quality = quality ? quality : 90;
+   subsample = quality <= 90 ? 1 : 0;
+   quality = quality < 1 ? 1 : quality > 100 ? 100 : quality;
+   quality = quality < 50 ? 5000 / quality : 200 - quality * 2;
+
+   for(i = 0; i < 64; ++i) {
+      int uvti, yti = (YQT[i]*quality+50)/100;
+      YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti);
+      uvti = (UVQT[i]*quality+50)/100;
+      UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti);
+   }
+
+   for(row = 0, k = 0; row < 8; ++row) {
+      for(col = 0; col < 8; ++col, ++k) {
+         fdtbl_Y[k]  = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]);
+         fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]);
+      }
+   }
+
+   // Write Headers
+   {
+      static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 };
+      static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 };
+      const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width),
+                                      3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 };
+      s->func(s->context, (void*)head0, sizeof(head0));
+      s->func(s->context, (void*)YTable, sizeof(YTable));
+      stbiw__putc(s, 1);
+      s->func(s->context, UVTable, sizeof(UVTable));
+      s->func(s->context, (void*)head1, sizeof(head1));
+      s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1);
+      s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values));
+      stbiw__putc(s, 0x10); // HTYACinfo
+      s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1);
+      s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values));
+      stbiw__putc(s, 1); // HTUDCinfo
+      s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1);
+      s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values));
+      stbiw__putc(s, 0x11); // HTUACinfo
+      s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1);
+      s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values));
+      s->func(s->context, (void*)head2, sizeof(head2));
+   }
+
+   // Encode 8x8 macroblocks
+   {
+      static const unsigned short fillBits[] = {0x7F, 7};
+      int DCY=0, DCU=0, DCV=0;
+      int bitBuf=0, bitCnt=0;
+      // comp == 2 is grey+alpha (alpha is ignored)
+      int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0;
+      const unsigned char *dataR = (const unsigned char *)data;
+      const unsigned char *dataG = dataR + ofsG;
+      const unsigned char *dataB = dataR + ofsB;
+      int x, y, pos;
+      if(subsample) {
+         for(y = 0; y < height; y += 16) {
+            for(x = 0; x < width; x += 16) {
+               float Y[256], U[256], V[256];
+               for(row = y, pos = 0; row < y+16; ++row) {
+                  // row >= height => use last input row
+                  int clamped_row = (row < height) ? row : height - 1;
+                  int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp;
+                  for(col = x; col < x+16; ++col, ++pos) {
+                     // if col >= width => use pixel from last input column
+                     int p = base_p + ((col < width) ? col : (width-1))*comp;
+                     float r = dataR[p], g = dataG[p], b = dataB[p];
+                     Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128;
+                     U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b;
+                     V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b;
+                  }
+               }
+               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0,   16, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8,   16, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT);
+
+               // subsample U,V
+               {
+                  float subU[64], subV[64];
+                  int yy, xx;
+                  for(yy = 0, pos = 0; yy < 8; ++yy) {
+                     for(xx = 0; xx < 8; ++xx, ++pos) {
+                        int j = yy*32+xx*2;
+                        subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f;
+                        subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f;
+                     }
+                  }
+                  DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+                  DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+               }
+            }
+         }
+      } else {
+         for(y = 0; y < height; y += 8) {
+            for(x = 0; x < width; x += 8) {
+               float Y[64], U[64], V[64];
+               for(row = y, pos = 0; row < y+8; ++row) {
+                  // row >= height => use last input row
+                  int clamped_row = (row < height) ? row : height - 1;
+                  int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp;
+                  for(col = x; col < x+8; ++col, ++pos) {
+                     // if col >= width => use pixel from last input column
+                     int p = base_p + ((col < width) ? col : (width-1))*comp;
+                     float r = dataR[p], g = dataG[p], b = dataB[p];
+                     Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128;
+                     U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b;
+                     V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b;
+                  }
+               }
+
+               DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y,  DCY, YDC_HT, YAC_HT);
+               DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT);
+               DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT);
+            }
+         }
+      }
+
+      // Do the bit alignment of the EOI marker
+      stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits);
+   }
+
+   // EOI
+   stbiw__putc(s, 0xFF);
+   stbiw__putc(s, 0xD9);
+
+   return 1;
+}
+
+STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality)
+{
+   stbi__write_context s = { 0 };
+   stbi__start_write_callbacks(&s, func, context);
+   return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality);
+}
+
+
+#ifndef STBI_WRITE_NO_STDIO
+STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality)
+{
+   stbi__write_context s = { 0 };
+   if (stbi__start_write_file(&s,filename)) {
+      int r = stbi_write_jpg_core(&s, x, y, comp, data, quality);
+      stbi__end_write_file(&s);
+      return r;
+   } else
+      return 0;
+}
+#endif
+
+#endif // STB_IMAGE_WRITE_IMPLEMENTATION
+
+/* Revision history
+      1.16  (2021-07-11)
+             make Deflate code emit uncompressed blocks when it would otherwise expand
+             support writing BMPs with alpha channel
+      1.15  (2020-07-13) unknown
+      1.14  (2020-02-02) updated JPEG writer to downsample chroma channels
+      1.13
+      1.12
+      1.11  (2019-08-11)
+
+      1.10  (2019-02-07)
+             support utf8 filenames in Windows; fix warnings and platform ifdefs
+      1.09  (2018-02-11)
+             fix typo in zlib quality API, improve STB_I_W_STATIC in C++
+      1.08  (2018-01-29)
+             add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter
+      1.07  (2017-07-24)
+             doc fix
+      1.06 (2017-07-23)
+             writing JPEG (using Jon Olick's code)
+      1.05   ???
+      1.04 (2017-03-03)
+             monochrome BMP expansion
+      1.03   ???
+      1.02 (2016-04-02)
+             avoid allocating large structures on the stack
+      1.01 (2016-01-16)
+             STBIW_REALLOC_SIZED: support allocators with no realloc support
+             avoid race-condition in crc initialization
+             minor compile issues
+      1.00 (2015-09-14)
+             installable file IO function
+      0.99 (2015-09-13)
+             warning fixes; TGA rle support
+      0.98 (2015-04-08)
+             added STBIW_MALLOC, STBIW_ASSERT etc
+      0.97 (2015-01-18)
+             fixed HDR asserts, rewrote HDR rle logic
+      0.96 (2015-01-17)
+             add HDR output
+             fix monochrome BMP
+      0.95 (2014-08-17)
+             add monochrome TGA output
+      0.94 (2014-05-31)
+             rename private functions to avoid conflicts with stb_image.h
+      0.93 (2014-05-27)
+             warning fixes
+      0.92 (2010-08-01)
+             casts to unsigned char to fix warnings
+      0.91 (2010-07-17)
+             first public release
+      0.90   first internal release
+*/
+
+/*
+------------------------------------------------------------------------------
+This software is available under 2 licenses -- choose whichever you prefer.
+------------------------------------------------------------------------------
+ALTERNATIVE A - MIT License
+Copyright (c) 2017 Sean Barrett
+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.
+------------------------------------------------------------------------------
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+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 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.
+------------------------------------------------------------------------------
+*/
--- /dev/null
+++ b/src/mem.c
@@ -1,0 +1,81 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "mem.h"
+#include "utils.h"
+
+
+static uint8_t hunk[MEM_HUNK_BYTES];
+static uint32_t bump_len = 0;
+static uint32_t temp_len = 0;
+
+static uint32_t temp_objects[MEM_TEMP_OBJECTS_MAX] = {};
+static uint32_t temp_objects_len;
+
+
+// Bump allocator - returns bytes from the front of the hunk
+
+// These allocations persist for many frames. The allocator level is reset
+// whenever we load a new race track or menu in game_set_scene()
+
+void *mem_mark() {
+	return &hunk[bump_len];
+}
+
+void *mem_bump(uint32_t size) {
+	error_if(bump_len + temp_len + size >= MEM_HUNK_BYTES, "Failed to allocate %d bytes in hunk mem", size);
+	uint8_t *p = &hunk[bump_len];
+	bump_len += size;
+	memset(p, 0, size);
+	return p;
+}
+
+void mem_reset(void *p) {
+	uint32_t offset = (uint8_t *)p - (uint8_t *)hunk;
+	error_if(offset > bump_len || offset > MEM_HUNK_BYTES, "Invalid mem reset");
+	bump_len = offset;
+}
+
+
+
+// Temp allocator - returns bytes from the back of the hunk
+
+// Temporary allocated bytes are not allowed to persist for multiple frames. You
+// need to explicitly free them when you are done. Temp allocated bytes don't 
+// have be freed in reverse allocation order. I.e. you can allocate A then B, 
+// and aftewards free A then B.
+
+void *mem_temp_alloc(uint32_t size) {
+	size = ((size >> 3) + 7) << 3; // allign to 8 bytes
+
+	error_if(bump_len + temp_len + size >= MEM_HUNK_BYTES, "Failed to allocate %d bytes in temp mem", size);
+	error_if(temp_objects_len >= MEM_TEMP_OBJECTS_MAX, "MEM_TEMP_OBJECTS_MAX reached");
+
+	temp_len += size;
+	void *p = &hunk[MEM_HUNK_BYTES - temp_len];
+	temp_objects[temp_objects_len++] = temp_len;
+	return p;
+}
+
+void mem_temp_free(void *p) {
+	uint32_t offset = (uint8_t *)&hunk[MEM_HUNK_BYTES] - (uint8_t *)p;
+	error_if(offset > MEM_HUNK_BYTES, "Object 0x%p not in temp hunk", p);
+
+	bool found = false;
+	uint32_t remaining_max = 0;
+	for (int i = 0; i < temp_objects_len; i++) {
+		if (temp_objects[i] == offset) {
+			temp_objects[i--] = temp_objects[--temp_objects_len];
+			found = true;
+		}
+		else if (temp_objects[i] > remaining_max) {
+			remaining_max = temp_objects[i];
+		}
+	}
+	error_if(!found, "Object 0x%p not in temp hunk", p);
+	temp_len = remaining_max;
+}
+
+void mem_temp_check() {
+	error_if(temp_len != 0, "Temp memory not free: %d object(s)", temp_objects_len);
+}
--- /dev/null
+++ b/src/mem.h
@@ -1,0 +1,17 @@
+#ifndef MEM_H
+#define MEM_H
+
+#include "types.h"
+
+#define MEM_TEMP_OBJECTS_MAX 8
+#define MEM_HUNK_BYTES (4 * 1024 * 1024)
+
+void *mem_bump(uint32_t size);
+void *mem_mark();
+void mem_reset(void *p);
+
+void *mem_temp_alloc(uint32_t size);
+void mem_temp_free(void *p);
+void mem_temp_check();
+
+#endif
--- /dev/null
+++ b/src/platform.h
@@ -1,0 +1,12 @@
+#ifndef PLATFORM_H
+#define PLATFORM_H
+
+#include "types.h"
+
+void platform_exit();
+vec2i_t platform_screen_size();
+double platform_now();
+void platform_set_fullscreen(bool fullscreen);
+void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len));
+
+#endif
--- /dev/null
+++ b/src/platform_sdl.c
@@ -1,0 +1,281 @@
+#include <SDL2/SDL.h>
+
+#include "platform.h"
+#include "input.h"
+#include "system.h"
+
+static uint64_t perf_freq = 0;
+static bool wants_to_exit = false;
+static SDL_Window *window;
+static SDL_AudioDeviceID audio_device;
+static SDL_GameController *gamepad;
+static void (*audio_callback)(float *buffer, uint32_t len) = NULL;
+
+
+uint8_t platform_sdl_gamepad_map[] = {
+	[SDL_CONTROLLER_BUTTON_A] = INPUT_GAMEPAD_A,
+	[SDL_CONTROLLER_BUTTON_B] = INPUT_GAMEPAD_B,
+	[SDL_CONTROLLER_BUTTON_X] = INPUT_GAMEPAD_X,
+	[SDL_CONTROLLER_BUTTON_Y] = INPUT_GAMEPAD_Y,
+	[SDL_CONTROLLER_BUTTON_BACK] = INPUT_GAMEPAD_SELECT,
+	[SDL_CONTROLLER_BUTTON_GUIDE] = INPUT_INVALID,
+	[SDL_CONTROLLER_BUTTON_START] = INPUT_GAMEPAD_START,
+	[SDL_CONTROLLER_BUTTON_LEFTSTICK] = INPUT_GAMEPAD_L_STICK_PRESS,
+	[SDL_CONTROLLER_BUTTON_RIGHTSTICK] = INPUT_GAMEPAD_R_STICK_PRESS,
+	[SDL_CONTROLLER_BUTTON_LEFTSHOULDER] = INPUT_GAMEPAD_L_SHOULDER,
+	[SDL_CONTROLLER_BUTTON_RIGHTSHOULDER] = INPUT_GAMEPAD_R_SHOULDER,
+	[SDL_CONTROLLER_BUTTON_DPAD_UP] = INPUT_GAMEPAD_DPAD_UP,
+	[SDL_CONTROLLER_BUTTON_DPAD_DOWN] = INPUT_GAMEPAD_DPAD_DOWN,
+	[SDL_CONTROLLER_BUTTON_DPAD_LEFT] = INPUT_GAMEPAD_DPAD_LEFT,
+	[SDL_CONTROLLER_BUTTON_DPAD_RIGHT] = INPUT_GAMEPAD_DPAD_RIGHT,
+	[SDL_CONTROLLER_BUTTON_MAX] = INPUT_INVALID
+};
+
+
+uint8_t platform_sdl_axis_map[] = {
+	[SDL_CONTROLLER_AXIS_LEFTX] = INPUT_GAMEPAD_L_STICK_LEFT,
+    [SDL_CONTROLLER_AXIS_LEFTY] = INPUT_GAMEPAD_L_STICK_UP,
+    [SDL_CONTROLLER_AXIS_RIGHTX] = INPUT_GAMEPAD_R_STICK_LEFT,
+    [SDL_CONTROLLER_AXIS_RIGHTY] = INPUT_GAMEPAD_R_STICK_UP,
+    [SDL_CONTROLLER_AXIS_TRIGGERLEFT] = INPUT_GAMEPAD_L_TRIGGER,
+    [SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = INPUT_GAMEPAD_R_TRIGGER,
+    [SDL_CONTROLLER_AXIS_MAX] = INPUT_INVALID
+};
+
+
+void platform_exit() {
+	wants_to_exit = true;
+}
+
+SDL_GameController *platform_find_gamepad() {
+	for (int i = 0; i < SDL_NumJoysticks(); i++) {
+		if (SDL_IsGameController(i)) {
+			return SDL_GameControllerOpen(i);
+		}
+	}
+
+	return NULL;
+}
+
+
+void platform_pump_events() {
+	SDL_Event ev;
+	while (SDL_PollEvent(&ev)) {
+		// Input Keyboard
+		if (ev.type == SDL_KEYDOWN || ev.type == SDL_KEYUP) {
+			int code = ev.key.keysym.scancode;
+			float state = ev.type == SDL_KEYDOWN ? 1.0 : 0.0;
+			if (code >= SDL_SCANCODE_LCTRL && code <= SDL_SCANCODE_RALT) {
+				int code_internal = code - SDL_SCANCODE_LCTRL + INPUT_KEY_LCTRL;
+				input_set_button_state(code_internal, state);
+			}
+			else if (code > 0 && code < INPUT_KEY_MAX) {
+				input_set_button_state(code, state);
+			}
+		}
+
+		else if (ev.type == SDL_TEXTINPUT) {
+			input_textinput(ev.text.text[0]);
+		}
+
+		// Gamepads connect/disconnect
+		else if (ev.type == SDL_CONTROLLERDEVICEADDED) {
+			gamepad = SDL_GameControllerOpen(ev.cdevice.which);
+		}
+		else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) {
+			if (gamepad && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(gamepad))) {
+				SDL_GameControllerClose(gamepad);
+				gamepad = platform_find_gamepad();
+			}
+		}
+
+		// Input Gamepad Buttons
+		else if (
+			ev.type == SDL_CONTROLLERBUTTONDOWN || 
+			ev.type == SDL_CONTROLLERBUTTONUP
+		) {
+			if (ev.cbutton.button < SDL_CONTROLLER_BUTTON_MAX) {
+				button_t button = platform_sdl_gamepad_map[ev.cbutton.button];
+				if (button != INPUT_INVALID) {
+					float state = ev.type == SDL_CONTROLLERBUTTONDOWN ? 1.0 : 0.0;
+					input_set_button_state(button, state);
+				}
+			}
+		}
+
+		// Input Gamepad Axis
+		else if (ev.type == SDL_CONTROLLERAXISMOTION) {
+			float state = (float)ev.caxis.value / 32767.0;
+
+			if (ev.caxis.axis < SDL_CONTROLLER_AXIS_MAX) {
+				int code = platform_sdl_axis_map[ev.caxis.axis];
+				if (
+					code == INPUT_GAMEPAD_L_TRIGGER || 
+					code == INPUT_GAMEPAD_R_TRIGGER
+				) {
+					input_set_button_state(code, state);
+				}
+				else if (state > 0) {
+					input_set_button_state(code, 0.0);
+					input_set_button_state(code+1, state);
+				}
+				else {
+					input_set_button_state(code, -state);
+					input_set_button_state(code+1, 0.0);
+				}
+			}
+		}
+
+		// Mouse buttons
+		else if (
+			ev.type == SDL_MOUSEBUTTONDOWN ||
+			ev.type == SDL_MOUSEBUTTONUP
+		) {
+			button_t button = INPUT_BUTTON_NONE;
+			switch (ev.button.button) {
+				case SDL_BUTTON_LEFT: button = INPUT_MOUSE_LEFT; break;
+				case SDL_BUTTON_MIDDLE: button = INPUT_MOUSE_MIDDLE; break;
+				case SDL_BUTTON_RIGHT: button = INPUT_MOUSE_RIGHT; break;
+				default: break;
+			}
+			if (button != INPUT_BUTTON_NONE) {
+				float state = ev.type == SDL_MOUSEBUTTONDOWN ? 1.0 : 0.0;
+				input_set_button_state(button, state);
+			}
+		}
+
+		// Mouse wheel
+		else if (ev.type == SDL_MOUSEWHEEL) {
+			button_t button = ev.wheel.y > 0 
+				? INPUT_MOUSE_WHEEL_UP
+				: INPUT_MOUSE_WHEEL_DOWN;
+			input_set_button_state(button, 1.0);
+			input_set_button_state(button, 0.0);
+		}
+
+		// Mouse move
+		else if (ev.type == SDL_MOUSEMOTION) {
+			input_set_mouse_pos(ev.motion.x, ev.motion.y);
+		}
+
+		// Window Events
+		if (ev.type == SDL_QUIT) {
+			wants_to_exit = true;
+		}
+		else if (
+			ev.type == SDL_WINDOWEVENT &&
+			(
+				ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ||
+				ev.window.event == SDL_WINDOWEVENT_RESIZED
+			)
+		) {
+			system_resize(platform_screen_size());
+		}
+	}
+}
+
+vec2i_t platform_screen_size() {
+	int width, height;
+	SDL_GL_GetDrawableSize(window, &width, &height);
+	return vec2i(width, height);
+}
+
+double platform_now() {
+	uint64_t perf_counter = SDL_GetPerformanceCounter();
+	return (double)perf_counter / (double)perf_freq;
+}
+
+void platform_set_fullscreen(bool fullscreen) {
+	if (fullscreen) {
+		int32_t display = SDL_GetWindowDisplayIndex(window);
+		
+		SDL_DisplayMode mode;
+		SDL_GetDesktopDisplayMode(display, &mode);
+		SDL_SetWindowDisplayMode(window, &mode);
+		SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
+	}
+	else {
+		SDL_SetWindowFullscreen(window, 0);	
+	}
+}
+
+void platform_audio_callback(void* userdata, uint8_t* stream, int len) {
+	if (audio_callback) {
+		audio_callback((float *)stream, len/sizeof(float));
+	}
+	else {
+		memset(stream, 0, len);
+	}
+}
+
+void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) {
+	audio_callback = cb;
+	SDL_PauseAudioDevice(audio_device, 0);
+}
+
+
+#if defined(RENDERER_GL)
+	#define PLATFORM_WINDOW_FLAGS SDL_WINDOW_OPENGL
+	SDL_GLContext platform_gl;
+
+	void platform_video_init() {
+		platform_gl = SDL_GL_CreateContext(window);
+		SDL_GL_SetSwapInterval(1);
+	}
+
+	void platform_prepare_frame() {
+		// nothing
+	}
+
+	void platform_video_cleanup() {
+		SDL_GL_DeleteContext(platform_gl);
+	}
+
+	void platform_end_frame() {
+		SDL_GL_SwapWindow(window);
+	}
+#else
+	#error "Unsupported renderer for platform SDL"
+#endif
+
+
+
+int main(int argc, char *argv[]) {
+	SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER);
+
+	audio_device = SDL_OpenAudioDevice(NULL, 0, &(SDL_AudioSpec){
+		.freq = 44100,
+		.format = AUDIO_F32,
+		.channels = 2,
+		.samples = 4096,
+		.callback = platform_audio_callback
+	}, NULL, 0);
+
+	gamepad = platform_find_gamepad();
+
+	perf_freq = SDL_GetPerformanceFrequency();
+
+	window = SDL_CreateWindow(
+		SYSTEM_WINDOW_NAME,
+		SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
+		SYSTEM_WINDOW_WIDTH, SYSTEM_WINDOW_HEIGHT,
+		SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | PLATFORM_WINDOW_FLAGS | SDL_WINDOW_ALLOW_HIGHDPI
+	);
+
+	platform_video_init();
+	system_init();
+
+	while (!wants_to_exit) {
+		platform_pump_events();
+		platform_prepare_frame();
+		system_update();
+		platform_end_frame();
+	}
+
+	system_cleanup();
+	platform_video_cleanup();
+
+	SDL_CloseAudioDevice(audio_device);
+	SDL_Quit();
+	return 0;
+}
--- /dev/null
+++ b/src/platform_sokol.c
@@ -1,0 +1,255 @@
+#include "platform.h"
+#include "system.h"
+
+#if defined(RENDERER_GL)
+	#ifdef __EMSCRIPTEN__
+		#define SOKOL_GLES2
+	#else
+		#define SOKOL_GLCORE33
+	#endif
+#else
+	#error "Unsupported renderer for platform SOKOL"
+#endif
+
+#define SOKOL_IMPL
+#include "libs/sokol_audio.h"
+#include "libs/sokol_time.h"
+#include "libs/sokol_app.h"
+#include "input.h"
+
+static const uint8_t keyboard_map[] = {
+	[SAPP_KEYCODE_SPACE] = INPUT_KEY_SPACE,
+	[SAPP_KEYCODE_APOSTROPHE] = INPUT_KEY_APOSTROPHE,
+	[SAPP_KEYCODE_COMMA] = INPUT_KEY_COMMA,
+	[SAPP_KEYCODE_MINUS] = INPUT_KEY_MINUS,
+	[SAPP_KEYCODE_PERIOD] = INPUT_KEY_PERIOD,
+	[SAPP_KEYCODE_SLASH] = INPUT_KEY_SLASH,
+	[SAPP_KEYCODE_0] = INPUT_KEY_0,
+	[SAPP_KEYCODE_1] = INPUT_KEY_1,
+	[SAPP_KEYCODE_2] = INPUT_KEY_2,
+	[SAPP_KEYCODE_3] = INPUT_KEY_3,
+	[SAPP_KEYCODE_4] = INPUT_KEY_4,
+	[SAPP_KEYCODE_5] = INPUT_KEY_5,
+	[SAPP_KEYCODE_6] = INPUT_KEY_6,
+	[SAPP_KEYCODE_7] = INPUT_KEY_7,
+	[SAPP_KEYCODE_8] = INPUT_KEY_8,
+	[SAPP_KEYCODE_9] = INPUT_KEY_9,
+	[SAPP_KEYCODE_SEMICOLON] = INPUT_KEY_SEMICOLON,
+	[SAPP_KEYCODE_EQUAL] = INPUT_KEY_EQUALS,
+	[SAPP_KEYCODE_A] = INPUT_KEY_A,
+	[SAPP_KEYCODE_B] = INPUT_KEY_B,
+	[SAPP_KEYCODE_C] = INPUT_KEY_C,
+	[SAPP_KEYCODE_D] = INPUT_KEY_D,
+	[SAPP_KEYCODE_E] = INPUT_KEY_E,
+	[SAPP_KEYCODE_F] = INPUT_KEY_F,
+	[SAPP_KEYCODE_G] = INPUT_KEY_G,
+	[SAPP_KEYCODE_H] = INPUT_KEY_H,
+	[SAPP_KEYCODE_I] = INPUT_KEY_I,
+	[SAPP_KEYCODE_J] = INPUT_KEY_J,
+	[SAPP_KEYCODE_K] = INPUT_KEY_K,
+	[SAPP_KEYCODE_L] = INPUT_KEY_L,
+	[SAPP_KEYCODE_M] = INPUT_KEY_M,
+	[SAPP_KEYCODE_N] = INPUT_KEY_N,
+	[SAPP_KEYCODE_O] = INPUT_KEY_O,
+	[SAPP_KEYCODE_P] = INPUT_KEY_P,
+	[SAPP_KEYCODE_Q] = INPUT_KEY_Q,
+	[SAPP_KEYCODE_R] = INPUT_KEY_R,
+	[SAPP_KEYCODE_S] = INPUT_KEY_S,
+	[SAPP_KEYCODE_T] = INPUT_KEY_T,
+	[SAPP_KEYCODE_U] = INPUT_KEY_U,
+	[SAPP_KEYCODE_V] = INPUT_KEY_V,
+	[SAPP_KEYCODE_W] = INPUT_KEY_W,
+	[SAPP_KEYCODE_X] = INPUT_KEY_X,
+	[SAPP_KEYCODE_Y] = INPUT_KEY_Y,
+	[SAPP_KEYCODE_Z] = INPUT_KEY_Z,
+	[SAPP_KEYCODE_LEFT_BRACKET] = INPUT_KEY_LEFTBRACKET,
+	[SAPP_KEYCODE_BACKSLASH] = INPUT_KEY_BACKSLASH,
+	[SAPP_KEYCODE_RIGHT_BRACKET] = INPUT_KEY_RIGHTBRACKET,
+	[SAPP_KEYCODE_GRAVE_ACCENT] = INPUT_KEY_TILDE,
+	[SAPP_KEYCODE_WORLD_1] = INPUT_INVALID,				// not implemented
+	[SAPP_KEYCODE_WORLD_2] = INPUT_INVALID,				// not implemented
+	[SAPP_KEYCODE_ESCAPE] = INPUT_KEY_ESCAPE,
+	[SAPP_KEYCODE_ENTER] = INPUT_KEY_RETURN,
+	[SAPP_KEYCODE_TAB] = INPUT_KEY_TAB,
+	[SAPP_KEYCODE_BACKSPACE] = INPUT_KEY_BACKSPACE,
+	[SAPP_KEYCODE_INSERT] = INPUT_KEY_INSERT,
+	[SAPP_KEYCODE_DELETE] = INPUT_KEY_DELETE,
+	[SAPP_KEYCODE_RIGHT] = INPUT_KEY_RIGHT,
+	[SAPP_KEYCODE_LEFT] = INPUT_KEY_LEFT,
+	[SAPP_KEYCODE_DOWN] = INPUT_KEY_DOWN,
+	[SAPP_KEYCODE_UP] = INPUT_KEY_UP,
+	[SAPP_KEYCODE_PAGE_UP] = INPUT_KEY_PAGEUP,
+	[SAPP_KEYCODE_PAGE_DOWN] = INPUT_KEY_PAGEDOWN,
+	[SAPP_KEYCODE_HOME] = INPUT_KEY_HOME,
+	[SAPP_KEYCODE_END] = INPUT_KEY_END,
+	[SAPP_KEYCODE_CAPS_LOCK] = INPUT_KEY_CAPSLOCK,
+	[SAPP_KEYCODE_SCROLL_LOCK] = INPUT_KEY_SCROLLLOCK,
+	[SAPP_KEYCODE_NUM_LOCK] = INPUT_KEY_NUMLOCK,
+	[SAPP_KEYCODE_PRINT_SCREEN] = INPUT_KEY_PRINTSCREEN,
+	[SAPP_KEYCODE_PAUSE] = INPUT_KEY_PAUSE,
+	[SAPP_KEYCODE_F1] = INPUT_KEY_F1,
+	[SAPP_KEYCODE_F2] = INPUT_KEY_F2,
+	[SAPP_KEYCODE_F3] = INPUT_KEY_F3,
+	[SAPP_KEYCODE_F4] = INPUT_KEY_F4,
+	[SAPP_KEYCODE_F5] = INPUT_KEY_F5,
+	[SAPP_KEYCODE_F6] = INPUT_KEY_F6,
+	[SAPP_KEYCODE_F7] = INPUT_KEY_F7,
+	[SAPP_KEYCODE_F8] = INPUT_KEY_F8,
+	[SAPP_KEYCODE_F9] = INPUT_KEY_F9,
+	[SAPP_KEYCODE_F10] = INPUT_KEY_F10,
+	[SAPP_KEYCODE_F11] = INPUT_KEY_F11,
+	[SAPP_KEYCODE_F12] = INPUT_KEY_F12,
+	[SAPP_KEYCODE_F13] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F14] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F15] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F16] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F17] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F18] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F19] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F20] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F21] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F22] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F23] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F24] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_F25] = INPUT_INVALID, 				// not implemented
+	[SAPP_KEYCODE_KP_0] = INPUT_KEY_KP_0,
+	[SAPP_KEYCODE_KP_1] = INPUT_KEY_KP_1,
+	[SAPP_KEYCODE_KP_2] = INPUT_KEY_KP_2,
+	[SAPP_KEYCODE_KP_3] = INPUT_KEY_KP_3,
+	[SAPP_KEYCODE_KP_4] = INPUT_KEY_KP_4,
+	[SAPP_KEYCODE_KP_5] = INPUT_KEY_KP_5,
+	[SAPP_KEYCODE_KP_6] = INPUT_KEY_KP_6,
+	[SAPP_KEYCODE_KP_7] = INPUT_KEY_KP_7,
+	[SAPP_KEYCODE_KP_8] = INPUT_KEY_KP_8,
+	[SAPP_KEYCODE_KP_9] = INPUT_KEY_KP_9,
+	[SAPP_KEYCODE_KP_DECIMAL] = INPUT_KEY_KP_PERIOD,
+	[SAPP_KEYCODE_KP_DIVIDE] = INPUT_KEY_KP_DIVIDE,
+	[SAPP_KEYCODE_KP_MULTIPLY] = INPUT_KEY_KP_MULTIPLY,
+	[SAPP_KEYCODE_KP_SUBTRACT] = INPUT_KEY_KP_MINUS,
+	[SAPP_KEYCODE_KP_ADD] = INPUT_KEY_KP_PLUS,
+	[SAPP_KEYCODE_KP_ENTER] = INPUT_KEY_KP_ENTER,
+	[SAPP_KEYCODE_KP_EQUAL] = INPUT_INVALID, 			// not implemented
+	[SAPP_KEYCODE_LEFT_SHIFT] = INPUT_KEY_LSHIFT,
+	[SAPP_KEYCODE_LEFT_CONTROL] = INPUT_KEY_LCTRL,
+	[SAPP_KEYCODE_LEFT_ALT] = INPUT_KEY_LALT,
+	[SAPP_KEYCODE_LEFT_SUPER] = INPUT_INVALID, 			// not implemented
+	[SAPP_KEYCODE_RIGHT_SHIFT] = INPUT_KEY_RSHIFT,
+	[SAPP_KEYCODE_RIGHT_CONTROL] = INPUT_KEY_RCTRL,
+	[SAPP_KEYCODE_RIGHT_ALT] = INPUT_KEY_RALT,
+	[SAPP_KEYCODE_RIGHT_SUPER] = INPUT_INVALID, 		// not implemented
+	[SAPP_KEYCODE_MENU] = INPUT_INVALID, 				// not implemented
+};
+
+
+static void (*audio_callback)(float *buffer, uint32_t len) = NULL;
+
+void platform_exit() {
+	sapp_quit();
+}
+
+vec2i_t platform_screen_size() {
+	return vec2i(sapp_width(), sapp_height());
+}
+
+double platform_now() {
+	return stm_sec(stm_now());
+}
+
+void platform_set_fullscreen(bool fullscreen) {
+	if (fullscreen == sapp_is_fullscreen()) {
+		return;
+	}
+
+	sapp_toggle_fullscreen();
+}
+
+void platform_handle_event(const sapp_event* ev) {
+	// Input Keyboard
+	if (ev->type == SAPP_EVENTTYPE_KEY_DOWN || ev->type == SAPP_EVENTTYPE_KEY_UP) {
+		float state = ev->type == SAPP_EVENTTYPE_KEY_DOWN ? 1.0 : 0.0;
+		if (ev->key_code > 0 && ev->key_code < sizeof(keyboard_map)) {
+			int code = keyboard_map[ev->key_code];
+			input_set_button_state(code, state);
+		}
+	}
+
+	else if (ev->type == SAPP_EVENTTYPE_CHAR) {
+		input_textinput(ev->char_code);
+	}
+
+
+	// Input Gamepad Axis
+	// TODO: not implemented by sokol_app itself
+
+	// Mouse buttons
+	else if (
+		ev->type == SAPP_EVENTTYPE_MOUSE_DOWN ||
+		ev->type == SAPP_EVENTTYPE_MOUSE_UP
+	) {
+		button_t button = INPUT_BUTTON_NONE;
+		switch (ev->mouse_button) {
+			case SAPP_MOUSEBUTTON_LEFT: button = INPUT_MOUSE_LEFT; break;
+			case SAPP_MOUSEBUTTON_MIDDLE: button = INPUT_MOUSE_MIDDLE; break;
+			case SAPP_MOUSEBUTTON_RIGHT: button = INPUT_MOUSE_RIGHT; break;
+			default: break;
+		}
+		if (button != INPUT_BUTTON_NONE) {
+			float state = ev->type == SAPP_EVENTTYPE_MOUSE_DOWN ? 1.0 : 0.0;
+			input_set_button_state(button, state);
+		}
+	}
+
+	// Mouse wheel
+	else if (ev->type == SAPP_EVENTTYPE_MOUSE_SCROLL) {
+		button_t button = ev->scroll_y > 0 
+			? INPUT_MOUSE_WHEEL_UP
+			: INPUT_MOUSE_WHEEL_DOWN;
+		input_set_button_state(button, 1.0);
+		input_set_button_state(button, 0.0);
+	}
+
+	// Mouse move
+	else if (ev->type == SAPP_EVENTTYPE_MOUSE_MOVE) {
+		input_set_mouse_pos(ev->mouse_x, ev->mouse_y);
+	}
+
+	// Window Events
+	if (ev->type == SAPP_EVENTTYPE_RESIZED) {
+		system_resize(vec2i(ev->window_width, ev->window_height));
+	}
+}
+
+void platform_audio_callback(float* buffer, int num_frames, int num_channels) {
+	if (audio_callback) {
+		audio_callback(buffer, num_frames * num_channels);
+	}
+	else {
+		memset(buffer, 0, num_frames * sizeof(float));
+	}
+}
+
+void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) {
+	audio_callback = cb;
+}
+
+sapp_desc sokol_main(int argc, char* argv[]) {
+	stm_setup();
+
+	saudio_setup(&(saudio_desc){
+		.sample_rate = 44100,
+		.buffer_frames = 4096,
+		.num_packets = 256,
+		.num_channels = 2,
+		.stream_cb = platform_audio_callback,
+	});
+
+	return (sapp_desc) {
+		.width = SYSTEM_WINDOW_WIDTH,
+		.height = SYSTEM_WINDOW_HEIGHT,
+		.init_cb = system_init,
+		.frame_cb = system_update,
+		.cleanup_cb = system_cleanup,
+		.event_cb = platform_handle_event,
+		.win32_console_attach = true
+	};
+}
--- /dev/null
+++ b/src/render.h
@@ -1,0 +1,50 @@
+#ifndef RENDER_H
+#define RENDER_H
+
+#include "types.h"
+
+typedef enum {
+	RENDER_BLEND_NORMAL,
+	RENDER_BLEND_LIGHTER
+} render_blend_mode_t;
+
+#define RENDER_USE_MIPMAPS 1
+
+#define RENDER_FADEOUT_NEAR 48000.0
+#define RENDER_FADEOUT_FAR 64000.0
+
+extern uint16_t RENDER_NO_TEXTURE;
+
+void render_init(vec2i_t size);
+void render_cleanup();
+
+void render_resize(vec2i_t size);
+vec2i_t render_size();
+
+void render_frame_prepare();
+void render_frame_end();
+
+void render_set_view(vec3_t pos, vec3_t angles);
+void render_set_view_2d();
+void render_set_model_mat(mat4_t *m);
+void render_set_depth_write(bool enabled);
+void render_set_depth_test(bool enabled);
+void render_set_depth_offset(float offset);
+void render_set_screen_position(vec2_t pos);
+void render_set_blend_mode(render_blend_mode_t mode);
+void render_set_cull_backface(bool enabled);
+
+vec3_t render_transform(vec3_t pos);
+void render_push_tris(tris_t tris, uint16_t texture);
+void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture);
+void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture);
+void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index);
+
+uint16_t render_texture_create(uint32_t width, uint32_t height, rgba_t *pixels);
+vec2i_t render_texture_size(uint16_t texture_index);
+void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels);
+uint16_t render_textures_len();
+void render_textures_reset(uint16_t len);
+void render_textures_dump(const char *path);
+
+#endif
--- /dev/null
+++ b/src/render_gl.c
@@ -1,0 +1,719 @@
+
+// macOS
+#if defined(__APPLE__) && defined(__MACH__)
+	#include <OpenGL/gl.h>
+	#include <OpenGL/glext.h>
+
+	void glCreateTextures(GLuint ignored, GLsizei n, GLuint *name) {
+		glGenTextures(1, name);
+	}
+	#define glGenVertexArrays glGenVertexArraysAPPLE
+	#define glBindVertexArray glBindVertexArrayAPPLE
+	#define glDeleteVertexArrays glDeleteVertexArraysAPPLE
+
+// Linux
+#elif defined(__unix__)
+	#include <GL/glew.h>
+	
+	#ifdef __EMSCRIPTEN__
+		void glCreateTextures(GLuint ignored, GLsizei n, GLuint *name) {
+			glGenTextures(1, name);
+		}
+	#endif
+
+// WINDOWS
+#else
+	#include <windows.h>
+
+	#define GL3_PROTOTYPES 1
+	#include <glew.h>
+	#pragma comment(lib, "glew32.lib")
+
+	#include <gl/GL.h>
+	#pragma comment(lib, "opengl32.lib")
+#endif
+
+
+
+#include "libs/stb_image_write.h"
+
+#include "render.h"
+#include "mem.h"
+#include "utils.h"
+
+
+#define NEAR_PLANE 16.0
+#define FAR_PLANE 262144.0
+
+#define ATLAS_SIZE 64
+#define ATLAS_GRID 32
+#define ATLAS_BORDER 16
+
+#define RENDER_TRIS_BUFFER_CAPACITY 2048
+#define TEXTURES_MAX 1024
+
+// WebGL (GLES) needs the `precision` to be set, OpenGL 2.something 
+// doesn't like that...
+#ifdef __EMSCRIPTEN__
+	#define SHADER_SOURCE(...) "precision highp float;" #__VA_ARGS__
+#else
+	#define SHADER_SOURCE(...) #__VA_ARGS__
+#endif
+	
+
+typedef struct {
+	vec2i_t offset;
+	vec2i_t size;
+} render_texture_t;
+
+uint16_t RENDER_NO_TEXTURE;
+
+static GLuint u_color;
+static GLuint u_view;
+static GLuint u_model;
+static GLuint u_projection;
+static GLuint u_screen;
+static GLuint u_camera_pos;
+static GLuint u_fade;
+
+static GLuint a_pos;
+static GLuint a_uv;
+static GLuint a_color;
+
+static GLuint vbo;
+
+static tris_t tris_buffer[RENDER_TRIS_BUFFER_CAPACITY];
+static uint32_t tris_len = 0;
+
+static vec2i_t screen_size;
+
+static uint32_t atlas_map[ATLAS_SIZE] = {0};
+static GLuint atlas_texture = 0;
+static render_blend_mode_t blend_mode = RENDER_BLEND_NORMAL;
+
+static mat4_t projection_mat_2d = mat4_identity();
+static mat4_t projection_mat_3d = mat4_identity();
+static mat4_t sprite_mat = mat4_identity();
+static mat4_t view_mat = mat4_identity();
+
+
+static render_texture_t textures[TEXTURES_MAX];
+static uint32_t textures_len = 0;
+static bool texture_mipmap_is_dirty = false;
+
+static const char * const VERTEX_SHADER = SHADER_SOURCE(
+	attribute vec3 pos;
+	attribute vec2 uv;
+	attribute vec4 color;
+
+	varying vec4 v_color;
+	varying vec2 v_uv;
+	uniform mat4 view;
+	uniform mat4 model;
+	uniform mat4 projection;
+	uniform vec2 screen;
+	uniform vec3 camera_pos;
+	uniform vec2 fade;
+	
+	void main() {
+		gl_Position = projection * view * model * vec4(pos, 1.0);
+		gl_Position.xy += screen.xy * gl_Position.w;
+		v_color = color;
+		v_color.a *= smoothstep(
+			fade.y, fade.x, // fadeout far, near
+			length(vec4(camera_pos, 1.0) - model * vec4(pos, 1.0))
+		);
+		v_uv = uv;
+		v_uv = uv / 2048.0; // ATLAS_GRID * ATLAS_SIZE
+	}
+);
+
+static const char * const FRAGMENT_SHADER_YCRCB = SHADER_SOURCE(
+	varying vec4 v_color;
+	varying vec2 v_uv;
+	uniform sampler2D texture;
+	void main() {
+		vec4 tex_color = texture2D(texture, v_uv);
+		vec4 color = tex_color * v_color;
+		if (color.a == 0.0) {
+			discard;
+		}
+		color.rgb = color.rgb * 2.0;
+		gl_FragColor = color;
+	}
+);
+
+
+#define render_bind_va_f(index, container, member, start) \
+	glVertexAttribPointer( \
+		index, member_size(container, member)/sizeof(float), GL_FLOAT, false, \
+		sizeof(container), \
+		(GLvoid*)(offsetof(container, member) + start) \
+	)
+
+#define render_bind_va_color(index, container, member, start) \
+	glVertexAttribPointer( \
+		index, 4,  GL_UNSIGNED_BYTE, true, \
+		sizeof(container), \
+		(GLvoid*)(offsetof(container, member) + start) \
+	)
+
+static void render_flush();
+static GLuint compile_shader(GLenum type, const char *source);
+
+static GLuint compile_shader(GLenum type, const char *source) {
+	GLuint shader = glCreateShader(type);
+	glShaderSource(shader, 1, &source, NULL);
+	glCompileShader(shader);
+	
+	GLint success;
+	glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+	if (!success) {
+		int log_written;
+		char log[256];
+		glGetShaderInfoLog(shader, 256, &log_written, log);
+		die("Error compiling shader: %s\n", log);
+	}
+	return shader;
+}
+
+// static void gl_message_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) {
+// 	puts(message);
+// }
+
+void render_init(vec2i_t size) {	
+	#if defined(__APPLE__) && defined(__MACH__)
+		// OSX
+		// (nothing to do here)
+	#else
+		// Windows, Linux
+		glewExperimental = GL_TRUE;
+		glewInit();
+	#endif
+
+	// glEnable(GL_DEBUG_OUTPUT);
+	// glDebugMessageCallback(gl_message_callback, NULL);
+	// glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);
+
+
+	// Atlas Texture
+
+	glCreateTextures(GL_TEXTURE_2D, 1, &atlas_texture);
+	glBindTexture(GL_TEXTURE_2D, atlas_texture);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, RENDER_USE_MIPMAPS ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+	float anisotropy = 0;
+	glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisotropy);
+	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy);
+
+	uint32_t tw = ATLAS_SIZE * ATLAS_GRID;
+	uint32_t th = ATLAS_SIZE * ATLAS_GRID;
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
+	printf("atlas texture %5d\n", atlas_texture);
+
+
+	// Shaders
+
+	GLuint fragment_shader = compile_shader(GL_FRAGMENT_SHADER, FRAGMENT_SHADER_YCRCB);
+	GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, VERTEX_SHADER);
+	GLuint shader_program = glCreateProgram();
+
+	glAttachShader(shader_program, vertex_shader);
+	glAttachShader(shader_program, fragment_shader);
+	glLinkProgram(shader_program);
+	glUseProgram(shader_program);
+
+	u_color = glGetUniformLocation(shader_program, "color");
+	u_view = glGetUniformLocation(shader_program, "view");
+	u_model = glGetUniformLocation(shader_program, "model");
+	u_projection = glGetUniformLocation(shader_program, "projection");
+	u_screen = glGetUniformLocation(shader_program, "screen");
+	u_camera_pos = glGetUniformLocation(shader_program, "camera_pos");
+	u_fade = glGetUniformLocation(shader_program, "fade");
+
+	a_pos = glGetAttribLocation(shader_program, "pos");
+	a_uv = glGetAttribLocation(shader_program, "uv");
+	a_color = glGetAttribLocation(shader_program, "color");
+
+	// Tris buffer
+
+	glGenBuffers(1, &vbo);
+	glBindBuffer(GL_ARRAY_BUFFER, vbo);
+
+	GLuint va;
+	glGenVertexArrays(1, &va);
+	glBindVertexArray(va);
+
+
+	// Defaults
+
+	glEnableVertexAttribArray(a_pos);
+	glEnableVertexAttribArray(a_uv);
+	glEnableVertexAttribArray(a_color);
+
+	render_bind_va_f(a_pos, vertex_t, pos, 0);
+	render_bind_va_f(a_uv, vertex_t, uv, 0);
+	render_bind_va_color(a_color, vertex_t, color, 0);
+
+	render_resize(size);
+	render_set_view(vec3(0, 0, 0), vec3(0, 0, 0));
+	render_set_model_mat(&mat4_identity());
+
+	glEnable(GL_CULL_FACE);
+	glEnable(GL_BLEND);
+	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+
+	// Create white texture
+
+	rgba_t white_pixels[4] = {
+		rgba(128,128,128,255), rgba(128,128,128,255),
+		rgba(128,128,128,255), rgba(128,128,128,255)
+	};
+	RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels);
+}
+
+void render_cleanup() {
+	// TODO
+}
+
+
+static void render_setup_2d_projection_mat() {
+	float near = -1;
+	float far = 1;
+	float left = 0;
+	float right = screen_size.x;
+	float bottom = screen_size.y;
+	float top = 0;
+	float lr = 1 / (left - right);
+	float bt = 1 / (bottom - top);
+	float nf = 1 / (near - far);
+  	projection_mat_2d = mat4(
+		-2 * lr,  0,  0,  0,
+		0,  -2 * bt,  0,  0,
+		0,        0,  2 * nf,    0, 
+		(left + right) * lr, (top + bottom) * bt, (far + near) * nf, 1
+	);
+}
+
+static void render_setup_3d_projection_mat() {
+	// wipeout has a horizontal fov of 90deg, but we want the fov to be fixed 
+	// for the vertical axis, so that widescreen displays just have a wider 
+	// view. For the original 4/3 aspect ratio this equates to a vertial fov
+	// of 73.75deg.
+	float aspect = (float)screen_size.x / (float)screen_size.y;
+	float fov = (73.75 / 180.0) * 3.14159265358;
+	float f = 1.0 / tan(fov / 2);
+	float nf = 1.0 / (NEAR_PLANE - FAR_PLANE);
+	projection_mat_3d = mat4(
+		f / aspect, 0, 0, 0,
+		0, f, 0, 0, 
+		0, 0, (FAR_PLANE + NEAR_PLANE) * nf, -1, 
+		0, 0, 2 * FAR_PLANE * NEAR_PLANE * nf, 0
+	);
+}
+
+void render_resize(vec2i_t size) {
+	glViewport(0, 0, size.x, size.y);
+	screen_size = size;
+
+	render_setup_2d_projection_mat();
+	render_setup_3d_projection_mat();
+}
+
+vec2i_t render_size() {
+	return screen_size;
+}
+
+void render_frame_prepare() {
+	glUniform2f(u_screen, 0, 0);
+	glEnable(GL_DEPTH_TEST);
+	glDepthMask(true);
+	glDisable(GL_POLYGON_OFFSET_FILL);
+	glClearColor(0, 0, 0, 1);
+	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+	glEnable(GL_DEPTH_TEST); 
+}
+
+void render_frame_end() {	
+	render_flush();
+}
+
+void render_flush() {
+	if (tris_len == 0) {
+		return;
+	}
+
+	if (texture_mipmap_is_dirty) {
+		glGenerateMipmap(GL_TEXTURE_2D);
+		texture_mipmap_is_dirty = false;
+	}
+
+	glBindBuffer(GL_ARRAY_BUFFER, vbo);
+	glBufferData(GL_ARRAY_BUFFER, sizeof(tris_t) * tris_len, tris_buffer, GL_DYNAMIC_DRAW);
+	glDrawArrays(GL_TRIANGLES, 0, tris_len * 3);
+	tris_len = 0;
+}
+
+
+void render_set_view(vec3_t pos, vec3_t angles) {
+	render_flush();
+	render_set_depth_write(true);
+	render_set_depth_test(true);
+
+	view_mat = mat4_identity();
+	mat4_set_translation(&view_mat, vec3(0, 0, 0));
+	mat4_set_roll_pitch_yaw(&view_mat, vec3(angles.x, -angles.y + M_PI, angles.z + M_PI));
+	mat4_translate(&view_mat, vec3_inv(pos));
+	mat4_set_yaw_pitch_roll(&sprite_mat, vec3(-angles.x, angles.y - M_PI, 0));
+
+	render_set_model_mat(&mat4_identity());
+
+	render_flush();
+	glUniformMatrix4fv(u_view, 1, false, view_mat.m);
+	glUniformMatrix4fv(u_projection, 1, false, projection_mat_3d.m);
+	glUniform3f(u_camera_pos, pos.x, pos.y, pos.z);
+	glUniform2f(u_fade, RENDER_FADEOUT_NEAR, RENDER_FADEOUT_FAR);
+}
+
+void render_set_view_2d() {
+	render_flush();
+	render_set_depth_test(false);
+	render_set_depth_write(false);
+
+	render_set_model_mat(&mat4_identity());
+	glUniform3f(u_camera_pos, 0, 0, 0);
+	glUniformMatrix4fv(u_view, 1, false, mat4_identity().m);
+	glUniformMatrix4fv(u_projection, 1, false, projection_mat_2d.m);
+}
+
+void render_set_model_mat(mat4_t *m) {
+	render_flush();
+	glUniformMatrix4fv(u_model, 1, false, m->m);
+}
+
+void render_set_depth_write(bool enabled) {
+	render_flush();
+	glDepthMask(enabled);
+}
+
+void render_set_depth_test(bool enabled) {
+	render_flush();
+	if (enabled) {
+		glEnable(GL_DEPTH_TEST);
+	}
+	else {
+		glDisable(GL_DEPTH_TEST); 
+	}
+}
+
+void render_set_depth_offset(float offset) {
+	render_flush();
+	if (offset == 0) {
+		glDisable(GL_POLYGON_OFFSET_FILL);
+		return;	
+	}
+
+	glEnable(GL_POLYGON_OFFSET_FILL);
+	glPolygonOffset(offset, 1.0);
+}
+
+void render_set_screen_position(vec2_t pos) {
+	render_flush();
+	glUniform2f(u_screen, pos.x, -pos.y);
+}
+
+void render_set_blend_mode(render_blend_mode_t new_mode) {
+	if (new_mode == blend_mode) {
+		return;
+	}
+	render_flush();
+
+	blend_mode = new_mode;
+	if (blend_mode == RENDER_BLEND_NORMAL) {
+		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+	}
+	else if (blend_mode == RENDER_BLEND_LIGHTER) {
+		glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+	}
+}
+
+void render_set_cull_backface(bool enabled) {
+	render_flush();
+	if (enabled) {
+		glEnable(GL_CULL_FACE);
+	}
+	else {
+		glDisable(GL_CULL_FACE);
+	}
+}
+
+
+
+
+vec3_t render_transform(vec3_t pos) {
+	return vec3_transform(vec3_transform(pos, &view_mat), &projection_mat_3d);
+}
+
+void render_push_tris(tris_t tris, uint16_t texture_index) {
+	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
+	
+	if (tris_len >= RENDER_TRIS_BUFFER_CAPACITY) {
+		render_flush();
+	}
+
+	render_texture_t *t = &textures[texture_index];
+
+	for (int i = 0; i < 3; i++) {
+		tris.vertices[i].uv.x += t->offset.x;
+		tris.vertices[i].uv.y += t->offset.y;
+	}
+	tris_buffer[tris_len++] = tris;
+}
+
+void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
+	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
+
+	vec3_t p1 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
+	vec3_t p2 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
+	vec3_t p3 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5,  size.y * 0.5, 0), &sprite_mat));
+	vec3_t p4 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5,  size.y * 0.5, 0), &sprite_mat));
+
+	render_texture_t *t = &textures[texture_index];
+	render_push_tris((tris_t){
+		.vertices = {
+			{
+				.pos = p1,
+				.uv = {0, 0},
+				.color = color
+			},
+			{
+				.pos = p2,
+				.uv = {0 + t->size.x ,0},
+				.color = color
+			},
+			{
+				.pos = p3,
+				.uv = {0, 0 + t->size.y},
+				.color = color
+			},
+		}
+	}, texture_index);
+	render_push_tris((tris_t){
+		.vertices = {
+			{
+				.pos = p3,
+				.uv = {0, 0 + t->size.y},
+				.color = color
+			},
+			{
+				.pos = p2,
+				.uv = {0 + t->size.x, 0},
+				.color = color
+			},
+			{
+				.pos = p4,
+				.uv = {0 + t->size.x, 0 + t->size.y},
+				.color = color
+			},
+		}
+	}, texture_index);
+}
+
+void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
+	render_push_2d_tile(pos, vec2i(0, 0), render_texture_size(texture_index), size, color, texture_index);
+}
+
+void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index) {
+	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
+	render_push_tris((tris_t){
+		.vertices = {
+			{
+				.pos = {pos.x, pos.y + size.y, 0},
+				.uv = {uv_offset.x , uv_offset.y + uv_size.y},
+				.color = color
+			},
+			{
+				.pos = {pos.x + size.x, pos.y, 0},
+				.uv = {uv_offset.x +  uv_size.x, uv_offset.y},
+				.color = color
+			},
+			{
+				.pos = {pos.x, pos.y, 0},
+				.uv = {uv_offset.x , uv_offset.y},
+				.color = color
+			},
+		}
+	}, texture_index);
+
+	render_push_tris((tris_t){
+		.vertices = {
+			{
+				.pos = {pos.x + size.x, pos.y + size.y, 0},
+				.uv = {uv_offset.x + uv_size.x, uv_offset.y + uv_size.y},
+				.color = color
+			},
+			{
+				.pos = {pos.x + size.x, pos.y, 0},
+				.uv = {uv_offset.x + uv_size.x, uv_offset.y},
+				.color = color
+			},
+			{
+				.pos = {pos.x, pos.y + size.y, 0},
+				.uv = {uv_offset.x , uv_offset.y + uv_size.y},
+				.color = color
+			},
+		}
+	}, texture_index);
+}
+
+
+uint16_t render_texture_create(uint32_t tw, uint32_t th, rgba_t *pixels) {
+	error_if(textures_len >= TEXTURES_MAX, "TEXTURES_MAX reached");
+
+	uint32_t bw = tw + ATLAS_BORDER * 2;
+	uint32_t bh = th + ATLAS_BORDER * 2;
+
+	// Find a position in the atlas for this texture (with added border)
+	uint32_t grid_width = (bw + ATLAS_GRID - 1) / ATLAS_GRID;
+	uint32_t grid_height = (bh + ATLAS_GRID - 1) / ATLAS_GRID;
+	uint32_t grid_x = 0;
+	uint32_t grid_y = ATLAS_SIZE - grid_height + 1;
+
+	for (uint32_t cx = 0; cx < ATLAS_SIZE - grid_width; cx++) {
+		if (atlas_map[cx] >= grid_y) {
+			continue;
+		}
+
+		uint32_t cy = atlas_map[cx];
+		bool is_best = true;
+
+		for (uint32_t bx = cx; bx < cx + grid_width; bx++) {
+			if (atlas_map[bx] >= grid_y) {
+				is_best = false;
+				cx = bx;
+				break;
+			}
+			if (atlas_map[bx] > cy) {
+				cy = atlas_map[bx];
+			}
+		}
+		if (is_best) {
+			grid_y = cy;
+			grid_x = cx;
+		}
+	}
+
+	error_if(grid_y + grid_height > ATLAS_SIZE, "Render atlas ran out of space");
+
+	for (uint32_t cx = grid_x; cx < grid_x + grid_width; cx++) {
+		atlas_map[cx] = grid_y + grid_height;
+	}
+
+	// Add the border pixels for this texture
+	rgba_t *pb = mem_temp_alloc(sizeof(rgba_t) * bw * bh);
+
+	if (tw && th) {
+		// Top border
+		for (int32_t y = 0; y < ATLAS_BORDER; y++) {
+			memcpy(pb + bw * y + ATLAS_BORDER, pixels, tw * sizeof(rgba_t));
+		}
+
+		// Bottom border
+		for (int32_t y = 0; y < ATLAS_BORDER; y++) {
+			memcpy(pb + bw * (bh - ATLAS_BORDER + y) + ATLAS_BORDER, pixels + tw * (th-1), tw * sizeof(rgba_t));
+		}
+		
+		// Left border
+		for (int32_t y = 0; y < bh; y++) {
+			for (int32_t x = 0; x < ATLAS_BORDER; x++) {
+				pb[y * bw + x] = pixels[clamp(y-ATLAS_BORDER, 0, th-1) * tw];
+			}
+		}
+
+		// Right border
+		for (int32_t y = 0; y < bh; y++) {
+			for (int32_t x = 0; x < ATLAS_BORDER; x++) {
+				pb[y * bw + x + bw - ATLAS_BORDER] = pixels[tw - 1 + clamp(y-ATLAS_BORDER, 0, th-1) * tw];
+			}
+		}
+
+		// Texture
+		for (int32_t y = 0; y < th; y++) {
+			memcpy(pb + bw * (y + ATLAS_BORDER) + ATLAS_BORDER, pixels + tw * y, tw * sizeof(rgba_t));
+		}
+	}
+
+	uint32_t x = grid_x * ATLAS_GRID;
+	uint32_t y = grid_y * ATLAS_GRID;
+	glBindTexture(GL_TEXTURE_2D, atlas_texture);
+	glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, bw, bh, GL_RGBA, GL_UNSIGNED_BYTE, pb);
+	mem_temp_free(pb);
+
+
+	texture_mipmap_is_dirty = RENDER_USE_MIPMAPS;
+	uint16_t texture_index = textures_len;
+	textures_len++;
+	textures[texture_index] = (render_texture_t){ {x + ATLAS_BORDER, y + ATLAS_BORDER}, {tw, th} };
+
+	printf("inserted atlas texture (%3dx%3d) at (%3d,%3d)\n", tw, th, grid_x, grid_y);
+	return texture_index;
+}
+
+vec2i_t render_texture_size(uint16_t texture_index) {
+	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
+	return textures[texture_index].size;
+}
+
+void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels) {
+	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
+
+	render_texture_t *t = &textures[texture_index];
+	glBindTexture(GL_TEXTURE_2D, atlas_texture);
+	glTexSubImage2D(GL_TEXTURE_2D, 0, t->offset.x, t->offset.y, t->size.x, t->size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+}
+
+uint16_t render_textures_len() {
+	return textures_len;
+}
+
+void render_textures_reset(uint16_t len) {
+	error_if(len > textures_len, "Invalid texture reset len %d >= %d", len, textures_len);
+	render_flush();
+
+	textures_len = len;
+	clear(atlas_map);
+
+	// Clear completely and recreate the default white texture
+	if (len == 0) {
+		rgba_t white_pixels[4] = {
+			rgba(128,128,128,255), rgba(128,128,128,255),
+			rgba(128,128,128,255), rgba(128,128,128,255)
+		};
+		RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels);
+		return;
+	}
+
+	// Replay all texture grid insertions up to the reset len
+	for (int i = 0; i < textures_len; i++) {
+		uint32_t grid_x = (textures[i].offset.x - ATLAS_BORDER) / ATLAS_GRID;
+		uint32_t grid_y = (textures[i].offset.y - ATLAS_BORDER) / ATLAS_GRID;
+		uint32_t grid_width = (textures[i].size.x + ATLAS_BORDER * 2 + ATLAS_GRID - 1) / ATLAS_GRID;
+		uint32_t grid_height = (textures[i].size.y + ATLAS_BORDER * 2 + ATLAS_GRID - 1) / ATLAS_GRID;
+		for (uint32_t cx = grid_x; cx < grid_x + grid_width; cx++) {
+			atlas_map[cx] = grid_y + grid_height;
+		}
+	}
+}
+
+void render_textures_dump(const char *path) {
+	int width = ATLAS_SIZE * ATLAS_GRID;
+	int height = ATLAS_SIZE * ATLAS_GRID;
+	rgba_t *pixels = malloc(sizeof(rgba_t) * width * height);
+	glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+	stbi_write_png(path, width, height, 4, pixels, 0);
+	free(pixels);
+}
--- /dev/null
+++ b/src/system.c
@@ -1,0 +1,81 @@
+#include "system.h"
+#include "input.h"
+#include "render.h"
+#include "platform.h"
+#include "mem.h"
+#include "utils.h"
+
+#include "wipeout/game.h"
+
+static double time_real;
+static double time_scaled;
+static double time_scale = 1.0;
+static double tick_last;
+static double cycle_time = 0;
+
+void system_init() {
+	time_real = platform_now();
+	input_init();
+	render_init(platform_screen_size());
+	game_init();
+}
+
+void system_cleanup() {
+	render_cleanup();
+	input_cleanup();
+}
+
+void system_exit() {
+	platform_exit();
+}
+
+void system_update() {
+	double time_real_now = platform_now();
+	double real_delta = time_real_now - time_real;
+	time_real = time_real_now;
+	tick_last = min(real_delta, 0.1) * time_scale;
+	time_scaled += tick_last;
+
+	// FIXME: come up with a better way to wrap the cycle_time, so that it
+	// doesn't lose precission, but also doesn't jump upon reset.
+	cycle_time = time_scaled;
+	if (cycle_time > 3600 * M_PI) {
+		cycle_time -= 3600 * M_PI;
+	}
+	
+	render_frame_prepare();
+	
+	game_update();
+
+	render_frame_end();
+	input_clear();
+	mem_temp_check();
+}
+
+void system_reset_cycle_time() {
+	cycle_time = 0;
+}
+
+void system_resize(vec2i_t size) {
+	render_resize(size);
+}
+
+double system_time_scale_get() {
+	return time_scale;
+}
+
+void system_time_scale_set(double scale) {
+	time_scale = scale;
+}
+
+double system_tick() {
+	return tick_last;
+}
+
+double system_time() {
+	return time_scaled;
+}
+
+double system_cycle_time() {
+	return cycle_time;
+}
--- /dev/null
+++ b/src/system.h
@@ -1,0 +1,23 @@
+#ifndef SYSTEM_H
+#define SYSTEM_H
+
+#include "types.h"
+
+#define SYSTEM_WINDOW_NAME "wipEout"
+#define SYSTEM_WINDOW_WIDTH 1280
+#define SYSTEM_WINDOW_HEIGHT 720
+
+void system_init();
+void system_update();
+void system_cleanup();
+void system_exit();
+void system_resize(vec2i_t size);
+
+double system_time();
+double system_tick();
+double system_cycle_time();
+void system_reset_cycle_time();
+double system_time_scale_get();
+void system_time_scale_set(double ts);
+
+#endif
--- /dev/null
+++ b/src/types.c
@@ -1,0 +1,116 @@
+#include <math.h>
+#include "types.h"
+#include "utils.h"
+
+vec3_t vec3_wrap_angle(vec3_t a) {
+	return vec3(wrap_angle(a.x), wrap_angle(a.y), wrap_angle(a.z));
+}
+
+float vec3_angle(vec3_t a, vec3_t b) {
+	float magnitude = sqrt(
+		(a.x * a.x + a.y * a.y + a.z * a.z) * 
+		(b.x * b.x + b.y * b.y + b.z * b.z)
+	);
+	float cosine = (magnitude == 0)
+		? 1
+		: vec3_dot(a, b) / magnitude;
+	return acos(clamp(cosine, -1, 1));
+}
+
+vec3_t vec3_transform(vec3_t a, mat4_t *mat) {
+	float w = mat->m[3] * a.x + mat->m[7] * a.y + mat->m[11] * a.z + mat->m[15];
+	if (w == 0) {
+		w = 1;
+	}
+	return vec3(
+		(mat->m[0] * a.x + mat->m[4] * a.y + mat->m[ 8] * a.z + mat->m[12]) / w,
+		(mat->m[1] * a.x + mat->m[5] * a.y + mat->m[ 9] * a.z + mat->m[13]) / w,
+		(mat->m[2] * a.x + mat->m[6] * a.y + mat->m[10] * a.z + mat->m[14]) / w
+	);
+}
+
+vec3_t vec3_project_to_ray(vec3_t p, vec3_t r0, vec3_t r1) {
+	vec3_t ray = vec3_normalize(vec3_sub(r1, r0));
+	float dp = vec3_dot(vec3_sub(p, r0), ray);
+	return vec3_add(r0, vec3_mulf(ray, dp));
+}
+
+float vec3_distance_to_plane(vec3_t p, vec3_t plane_pos, vec3_t plane_normal) {
+	float dot_product = vec3_dot(vec3_sub(plane_pos, p), plane_normal);
+	float norm_dot_product = vec3_dot(vec3_mulf(plane_normal, -1), plane_normal);
+	return dot_product / norm_dot_product;
+}
+
+vec3_t vec3_reflect(vec3_t incidence, vec3_t normal, float f) {
+	return vec3_add(incidence, vec3_mulf(normal, vec3_dot(normal, vec3_mulf(incidence, -1)) * f));
+}
+
+void mat4_set_translation(mat4_t *mat, vec3_t pos) {
+	mat->cols[3][0] = pos.x;
+	mat->cols[3][1] = pos.y;
+	mat->cols[3][2] = pos.z;
+}
+
+void mat4_set_yaw_pitch_roll(mat4_t *mat, vec3_t rot) {
+	float sx = sin( rot.x);
+    float sy = sin(-rot.y);
+    float sz = sin(-rot.z);
+    float cx = cos( rot.x);
+    float cy = cos(-rot.y);
+    float cz = cos(-rot.z);
+
+	mat->cols[0][0] = cy * cz + sx * sy * sz;
+	mat->cols[1][0] = cz * sx * sy - cy * sz;
+	mat->cols[2][0] = cx * sy;
+	mat->cols[0][1] = cx * sz;
+	mat->cols[1][1] = cx * cz;
+	mat->cols[2][1] = -sx;
+	mat->cols[0][2] = -cz * sy + cy * sx * sz;
+	mat->cols[1][2] = cy * cz * sx + sy * sz;
+	mat->cols[2][2] = cx * cy;
+}
+
+void mat4_set_roll_pitch_yaw(mat4_t *mat, vec3_t rot) {
+	float sx = sin( rot.x);
+    float sy = sin(-rot.y);
+    float sz = sin(-rot.z);
+    float cx = cos( rot.x);
+    float cy = cos(-rot.y);
+    float cz = cos(-rot.z);
+
+	mat->cols[0][0] = cy * cz - sx * sy * sz;
+	mat->cols[1][0] = -cx * sz;
+	mat->cols[2][0] = cz * sy + cy * sx * sz;
+	mat->cols[0][1] = cz * sx * sy + cy * sz;
+	mat->cols[1][1] = cx *cz;
+	mat->cols[2][1] = -cy * cz * sx + sy * sz;
+	mat->cols[0][2] = -cx * sy;
+	mat->cols[1][2] = sx;
+	mat->cols[2][2] = cx * cy;
+}
+
+void mat4_translate(mat4_t *mat, vec3_t translation) {
+	mat->m[12] = mat->m[0] * translation.x + mat->m[4] * translation.y + mat->m[8] * translation.z + mat->m[12];
+	mat->m[13] = mat->m[1] * translation.x + mat->m[5] * translation.y + mat->m[9] * translation.z + mat->m[13];
+	mat->m[14] = mat->m[2] * translation.x + mat->m[6] * translation.y + mat->m[10] * translation.z + mat->m[14];
+	mat->m[15] = mat->m[3] * translation.x + mat->m[7] * translation.y + mat->m[11] * translation.z + mat->m[15];
+}
+
+void mat4_mul(mat4_t *res, mat4_t *a, mat4_t *b) {
+	res->m[ 0] = b->m[ 0] * a->m[0] + b->m[ 1] * a->m[4] + b->m[ 2] * a->m[ 8] + b->m[ 3] * a->m[12];
+	res->m[ 1] = b->m[ 0] * a->m[1] + b->m[ 1] * a->m[5] + b->m[ 2] * a->m[ 9] + b->m[ 3] * a->m[13];
+	res->m[ 2] = b->m[ 0] * a->m[2] + b->m[ 1] * a->m[6] + b->m[ 2] * a->m[10] + b->m[ 3] * a->m[14];
+	res->m[ 3] = b->m[ 0] * a->m[3] + b->m[ 1] * a->m[7] + b->m[ 2] * a->m[11] + b->m[ 3] * a->m[15];
+	res->m[ 4] = b->m[ 4] * a->m[0] + b->m[ 5] * a->m[4] + b->m[ 6] * a->m[ 8] + b->m[ 7] * a->m[12];
+	res->m[ 5] = b->m[ 4] * a->m[1] + b->m[ 5] * a->m[5] + b->m[ 6] * a->m[ 9] + b->m[ 7] * a->m[13];
+	res->m[ 6] = b->m[ 4] * a->m[2] + b->m[ 5] * a->m[6] + b->m[ 6] * a->m[10] + b->m[ 7] * a->m[14];
+	res->m[ 7] = b->m[ 4] * a->m[3] + b->m[ 5] * a->m[7] + b->m[ 6] * a->m[11] + b->m[ 7] * a->m[15];
+	res->m[ 8] = b->m[ 8] * a->m[0] + b->m[ 9] * a->m[4] + b->m[10] * a->m[ 8] + b->m[11] * a->m[12];
+	res->m[ 9] = b->m[ 8] * a->m[1] + b->m[ 9] * a->m[5] + b->m[10] * a->m[ 9] + b->m[11] * a->m[13];
+	res->m[10] = b->m[ 8] * a->m[2] + b->m[ 9] * a->m[6] + b->m[10] * a->m[10] + b->m[11] * a->m[14];
+	res->m[11] = b->m[ 8] * a->m[3] + b->m[ 9] * a->m[7] + b->m[10] * a->m[11] + b->m[11] * a->m[15];
+	res->m[12] = b->m[12] * a->m[0] + b->m[13] * a->m[4] + b->m[14] * a->m[ 8] + b->m[15] * a->m[12];
+	res->m[13] = b->m[12] * a->m[1] + b->m[13] * a->m[5] + b->m[14] * a->m[ 9] + b->m[15] * a->m[13];
+	res->m[14] = b->m[12] * a->m[2] + b->m[13] * a->m[6] + b->m[14] * a->m[10] + b->m[15] * a->m[14];
+	res->m[15] = b->m[12] * a->m[3] + b->m[13] * a->m[7] + b->m[14] * a->m[11] + b->m[15] * a->m[15];
+}
--- /dev/null
+++ b/src/types.h
@@ -1,0 +1,184 @@
+#ifndef TYPES_H
+#define TYPES_H
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+
+typedef union rgba_t { 
+	struct { 
+		uint8_t r, g, b, a;
+	} as_rgba;
+	uint8_t as_components[4];
+	uint32_t as_uint32;
+} rgba_t;
+
+typedef struct {
+	float x, y;
+} vec2_t;
+
+
+typedef struct {
+	int32_t x, y;
+} vec2i_t;
+
+
+typedef struct {
+	float x, y, z;
+} vec3_t;
+
+typedef union {
+	float m[16];
+	float cols[4][4];
+} mat4_t;
+
+typedef struct {
+	vec3_t pos;
+	vec2_t uv;
+	rgba_t color;
+} vertex_t;
+
+typedef struct {
+	vertex_t vertices[3];
+} tris_t;
+
+
+#define rgba(R, G, B, A) ((rgba_t){.as_rgba = {.r = R, .g = G, .b = B, .a = A}})
+#define vec2(X, Y) ((vec2_t){.x = X, .y = Y})
+#define vec3(X, Y, Z) ((vec3_t){.x = X, .y = Y, .z = Z})
+#define vec2i(X, Y) ((vec2i_t){.x = X, .y = Y})
+
+#define mat4(m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,m13,m14,m15) \
+	(mat4_t){.m = { \
+		m0,   m1,  m2,  m3, \
+		m4,   m5,  m6,  m7, \
+		m8,   m9, m10, m11, \
+		m12, m13, m14, m15  \
+	}}
+
+#define mat4_identity() mat4( \
+		1, 0, 0, 0, \
+		0, 1, 0, 0, \
+		0, 0, 1, 0, \
+		0, 0, 0, 1 \
+	)
+
+static inline vec2_t vec2_mulf(vec2_t a, float f) {
+	return vec2(
+		a.x * f,
+		a.y * f
+	);
+}
+
+static inline vec2i_t vec2i_mulf(vec2i_t a, float f) {
+	return vec2i(
+		a.x * f,
+		a.y * f
+	);
+}
+
+
+static inline vec3_t vec3_add(vec3_t a, vec3_t b) {
+	return vec3(
+		a.x + b.x,
+		a.y + b.y,
+		a.z + b.z
+	);
+}
+
+static inline vec3_t vec3_sub(vec3_t a, vec3_t b) {
+	return vec3(
+		a.x - b.x,
+		a.y - b.y,
+		a.z - b.z
+	);
+}
+
+static inline vec3_t vec3_mul(vec3_t a, vec3_t b) {
+	return vec3(
+		a.x * b.x,
+		a.y * b.y,
+		a.z * b.z
+	);
+}
+
+static inline vec3_t vec3_mulf(vec3_t a, float f) {
+	return vec3(
+		a.x * f,
+		a.y * f,
+		a.z * f
+	);
+}
+
+static inline vec3_t vec3_inv(vec3_t a) {
+	return vec3(-a.x, -a.y, -a.z);
+}
+
+static inline vec3_t vec3_divf(vec3_t a, float f) {
+	return vec3(
+		a.x / f,
+		a.y / f,
+		a.z / f
+	);
+}
+
+static inline float vec3_len(vec3_t a) {
+	return sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
+}
+
+static inline vec3_t vec3_cross(vec3_t a, vec3_t b) {
+	return vec3(
+		a.y * b.z - a.z * b.y,
+		a.z * b.x - a.x * b.z,
+		a.x * b.y - a.y * b.x
+	);
+}
+
+static inline float vec3_dot(vec3_t a, vec3_t b) {
+	return a.x * b.x + a.y * b.y + a.z * b.z;
+}
+
+static inline vec3_t vec3_lerp(vec3_t a, vec3_t b, float t) {
+	return vec3(
+		a.x + t * (b.x - a.x),
+		a.y + t * (b.y - a.y),
+		a.z + t * (b.z - a.z)
+	);
+}
+
+static inline vec3_t vec3_normalize(vec3_t a) {
+	float length = vec3_len(a);
+	return vec3(
+		a.x / length,
+		a.y / length,
+		a.z / length
+	);
+}
+
+static inline float wrap_angle(float a) {
+	a = fmod(a + M_PI, M_PI * 2);
+	if (a < 0) {
+		a += M_PI * 2;
+	}
+	return a - M_PI;
+}
+
+float vec3_angle(vec3_t a, vec3_t b);
+vec3_t vec3_wrap_angle(vec3_t a);
+vec3_t vec3_normalize(vec3_t a);
+vec3_t vec3_project_to_ray(vec3_t p, vec3_t r0, vec3_t r1);
+float vec3_distance_to_plane(vec3_t p, vec3_t plane_pos, vec3_t plane_normal);
+vec3_t vec3_reflect(vec3_t incidence, vec3_t normal, float f);
+
+float wrap_angle(float a);
+
+vec3_t vec3_transform(vec3_t a, mat4_t *mat);
+void mat4_set_translation(mat4_t *mat, vec3_t pos);
+void mat4_set_yaw_pitch_roll(mat4_t *m, vec3_t rot);
+void mat4_set_roll_pitch_yaw(mat4_t *mat, vec3_t rot);
+void mat4_translate(mat4_t *mat, vec3_t translation);
+void mat4_mul(mat4_t *res, mat4_t *a, mat4_t *b);
+
+#endif
--- /dev/null
+++ b/src/utils.c
@@ -1,0 +1,68 @@
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include "utils.h"
+#include "mem.h"
+
+char temp_path[64];
+char *get_path(const char *dir, const char *file) {
+	strcpy(temp_path, dir);
+	strcpy(temp_path + strlen(dir), file);
+	return temp_path;
+}
+
+
+bool file_exists(char *path) {
+  struct stat s;   
+  return (stat(path, &s) == 0);
+}
+
+uint8_t *file_load(char *path, uint32_t *bytes_read) {
+	FILE *f = fopen(path, "rb");
+	error_if(!f, "Could not open file for reading: %s", path);
+
+	fseek(f, 0, SEEK_END);
+	int32_t size = ftell(f);
+	if (size <= 0) {
+		fclose(f);
+		return NULL;
+	}
+	fseek(f, 0, SEEK_SET);
+
+	uint8_t *bytes = mem_temp_alloc(size);
+	if (!bytes) {
+		fclose(f);
+		return NULL;
+	}
+
+	*bytes_read = fread(bytes, 1, size, f);
+	fclose(f);
+	
+	error_if(*bytes_read != size, "Could not read file: %s", path);
+	return bytes;
+}
+
+uint32_t file_store(char *path, void *bytes, int32_t len) {
+	FILE *f = fopen(path, "wb");
+	error_if(!f, "Could not open file for writing: %s", path);
+
+	if (fwrite(bytes, 1, len, f) != len) {
+		die("Could not write file file %s", path);
+	}
+   
+	fclose(f);
+	return len;
+}
+
+bool str_starts_with(const char *haystack, const char *needle) {
+	return (strncmp(haystack, needle, strlen(needle)) == 0);
+}
+
+float rand_float(float min, float max) {
+	return min + ((float)rand() / (float)RAND_MAX) * (max - min);
+}
+
+int32_t rand_int(int32_t min, int32_t max) {
+	return min + rand() % (max - min);
+}
--- /dev/null
+++ b/src/utils.h
@@ -1,0 +1,139 @@
+#ifndef UTILS_H
+#define UTILS_H
+
+#include <string.h>
+#include "types.h"
+
+
+#if !defined(offsetof)
+	#define offsetof(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))
+#endif
+#define member_size(type, member) sizeof(((type *)0)->member)
+
+#define max(a,b) ({ \
+		__typeof__ (a) _a = (a); \
+		__typeof__ (b) _b = (b); \
+		_a > _b ? _a : _b; \
+	})
+
+#define min(a,b) ({ \
+		__typeof__ (a) _a = (a); \
+		__typeof__ (b) _b = (b); \
+		_a < _b ? _a : _b; \
+	})
+
+#define swap(a, b) ({ \
+		__typeof__(a) tmp = a; a = b; b = tmp; \
+	})
+
+#define clamp(v, min, max) ({ \
+		__typeof__(v) _v = v, _min = min, _max = max; \
+		_v > _max ? _max : _v < _min ? _min : _v; \
+	})
+#define scale(v, in_min, in_max, out_min, out_max) ({ \
+		__typeof__(v) _in_min = in_min, _out_min = out_min; \
+		_out_min + ((out_max) - _out_min) * (((v) - _in_min) / ((in_max) - _in_min)); \
+	})
+#define lerp(a, b, t) ({ \
+		__typeof__(a) _a = a; \
+		_a + ((b) - _a) * (t); \
+	})
+
+#define len(A) (sizeof(A) / sizeof(A[0]))
+#define clear(A) memset(A, 0, sizeof(A))
+
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+#define die(...) \
+	printf("Abort at " TOSTRING(__FILE__) " line " TOSTRING(__LINE__) ": " __VA_ARGS__); \
+	printf("\n"); \
+	exit(1)
+
+#define error_if(TEST, ...) \
+	if (TEST) { \
+		die(__VA_ARGS__); \
+	}
+
+
+#define flags_add(FLAGS, F)  (FLAGS |= (F))
+#define flags_rm(FLAGS, F)   (FLAGS &= ~(F))
+#define flags_is(FLAGS, F)   ((FLAGS & (F)) == (F))
+#define flags_any(FLAGS, F)  (FLAGS & (F))
+#define flags_not(FLAGS, F)  ((FLAGS & (F)) != (F))
+#define flags_none(FLAGS, F) ((FLAGS & (F)) == 0)
+#define flags_set(FLAGS, F)  (FLAGS = (F))
+
+	
+
+char *get_path(const char *dir, const char *file);
+bool str_starts_with(const char *haystack, const char *needle);
+float rand_float(float min, float max);
+int32_t rand_int(int32_t min, int32_t max); 
+
+bool file_exists(char *path);
+uint8_t *file_load(char *path, uint32_t *bytes_read);
+uint32_t file_store(char *path, void *bytes, int32_t len);
+
+
+#define sort(LIST, LEN, COMPARE_FUNC) \
+	for (uint32_t sort_i = 1, sort_j; sort_i < (LEN); sort_i++) { \
+		sort_j = sort_i; \
+		__typeof__((LIST)[0]) sort_temp = (LIST)[sort_j]; \
+		while (sort_j > 0 && COMPARE_FUNC(&(LIST)[sort_j-1], &sort_temp)) { \
+			(LIST)[sort_j] = (LIST)[sort_j-1]; \
+			sort_j--; \
+		} \
+		(LIST)[sort_j] = sort_temp; \
+	}
+
+#define shuffle(LIST, LEN) \
+	for (int i = (LEN) - 1; i > 0; i--) { \
+		int j = rand_int(0, i+1); \
+		swap((LIST)[i], (LIST)[j]); \
+	}
+
+
+static inline uint8_t get_u8(uint8_t *bytes, uint32_t *p) {
+	return bytes[(*p)++];
+}
+
+static inline uint16_t get_u16(uint8_t *bytes, uint32_t *p) {
+	uint16_t v = 0;
+	v |= bytes[(*p)++] << 8;
+	v |= bytes[(*p)++] << 0;
+	return v;
+}
+
+static inline uint32_t get_u32(uint8_t *bytes, uint32_t *p) {
+	uint32_t v = 0;
+	v |= bytes[(*p)++] << 24;
+	v |= bytes[(*p)++] << 16;
+	v |= bytes[(*p)++] <<  8;
+	v |= bytes[(*p)++] <<  0;
+	return v;
+}
+
+static inline uint16_t get_u16_le(uint8_t *bytes, uint32_t *p) {
+	uint16_t v = 0;
+	v |= bytes[(*p)++] << 0;
+	v |= bytes[(*p)++] << 8;
+	return v;
+}
+
+static inline uint32_t get_u32_le(uint8_t *bytes, uint32_t *p) {
+	uint32_t v = 0;
+	v |= bytes[(*p)++] <<  0;
+	v |= bytes[(*p)++] <<  8;
+	v |= bytes[(*p)++] << 16;
+	v |= bytes[(*p)++] << 24;
+	return v;
+}
+
+#define get_i8(BYTES, P) ((int8_t)get_u8(BYTES, P))
+#define get_i16(BYTES, P) ((int16_t)get_u16(BYTES, P))
+#define get_i16_le(BYTES, P) ((int16_t)get_u16_le(BYTES, P))
+#define get_i32(BYTES, P) ((int32_t)get_u32(BYTES, P))
+#define get_i32_le(BYTES, P) ((int32_t)get_u32_le(BYTES, P))
+
+#endif
--- /dev/null
+++ b/src/wasm-index.html
@@ -1,0 +1,238 @@
+<!doctype html>
+<html lang="en-us">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		<title>wipEout</title>
+		<style>
+			html, body {
+				color: #ccc;
+				font-family: Monospace;
+				font-size: 13px;
+				background-color: #111;
+				margin: 0px;
+			}
+			a {
+				color: #ffc603;
+				text-decoration: none;
+			}
+			a:hover {
+				text-decoration: underline;	
+			}
+			/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
+			canvas {
+				border: 0px none; 
+				background-color: black; 
+				width: 100%; 
+				height: 100%;
+				aspect-ratio: 16 / 9;
+			}
+
+			#head {
+				left: 0;
+				right: 0;
+				top: 0;
+				padding: 4px 16px 6px 16px;
+				xheight: 24px;
+				background-color: #222;
+				display: flex;
+				justify-content: space-between;
+			}
+			@keyframes page-loader { 
+				0% {transform: rotate(0deg); } 
+				100% { transform: rotate(360deg);}
+			}
+			.container {
+				position: relative;
+			}
+			.info-overlay {
+				text-align: center;
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				right: 0;
+				margin: auto;
+				width: 200px;
+				height: 200px;
+				display: none;
+			}
+			.fullscreen {
+				margin: 0 8px;
+			}
+			#spinner {
+				content: "";
+				border-radius: 50%;
+				width: 48px;
+				height: 48px;
+				position: absolute;
+				margin: auto;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				right: 0;
+				border-top: 2px solid #222;
+				border-right: 2px solid #222;
+				border-bottom: 2px solid #222;
+				border-left: 2px solid #ffc603;
+				transform: translateZ(0);
+				animation: page-loader 1.1s infinite linear;
+			}
+			h1 {
+				color: #fff;
+				display: inline-block;
+				margin: 0;
+				padding: 0;
+				font-size: 12px;
+			}
+			.key {
+				margin-right: 16px;
+			}
+			kbd {
+				background-color: #eee;
+				border-radius: 3px;
+				border: 1px solid #b4b4b4;
+				box-shadow:
+					0 1px 1px rgba(0, 0, 0, 0.2),
+					0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
+				color: #333;
+				display: inline-block;
+				font-size: 0.85em;
+				font-weight: 700;
+				line-height: 1;
+				padding: 1px 2px;
+				white-space: nowrap;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="head">
+			<div>
+				<h1>wipEout</h1>
+				<a href="#" id="fullscreen">fullscreen</a>
+				<span class="key"><kbd>◀</kbd><kbd>▲</kbd><kbd>▼</kbd><kbd>▶</kbd> steering</span>
+				<span class="key"><kbd>X</kbd> thrust</span>
+				<span class="key"><kbd>Y</kbd> shoot</span>
+				<span class="key"><kbd>C</kbd> brake left</span>
+				<span class="key"><kbd>V</kbd> brake right</span>
+				<span class="key"><kbd>A</kbd> view</span>
+			</div>
+			<div>
+				Read: 
+				<a href="https://phoboslab.org/log/2023/08/rewriting-wipeout">
+					Rewriting wipEout
+				</a>
+			</div>
+		</div>
+		<div id="game" class="container">
+			<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
+			<div class="info-overlay" id="loading">
+				<div id="spinner"></div>
+				<div id="status">Downloading...</div>
+				<div>
+					<progress value="0" max="100" id="progress" hidden=1></progress>  
+				</div>
+			</div>
+			<div class="info-overlay" id="select-version">
+				<p>
+					<a href="#" id="load-full-version">FULL VERSION</a><br/>
+					the complete game ~144mb
+				</p>
+				<p>
+					<a href="#" id="load-minimal-version">MINIMAL VERSION</a><br/>
+					no intro, no music ~11mb
+				</p>
+			</div>
+		</div>
+			
+		</div>
+		<script type='text/javascript'>
+			var loadScript = (ev, src) => {
+				ev.preventDefault();
+
+				// Hide select-version, show loader
+  				document.getElementById('select-version').style.display = 'none';
+  				document.getElementById('loading').style.display = 'block';
+
+  				// Load the requested script
+  				var s = document.createElement('script');
+  				s.setAttribute('src', src);
+  				document.head.appendChild(s);
+
+				// Attemp to unlock Audio :(
+				var audioCtx = new AudioContext();
+				audioCtx.resume();
+				return false;
+			};
+			var requestFullscreen = (ev) => {
+				ev.preventDefault();
+				document.getElementById('game').requestFullscreen();
+			};
+			document.getElementById('select-version').style.display = 'block';
+			document.getElementById('fullscreen').addEventListener('click', (ev) => requestFullscreen(ev, 'wipeout.js'))
+			document.getElementById('load-full-version').addEventListener('click', (ev) => loadScript(ev, 'wipeout.js'));
+			document.getElementById('load-minimal-version').addEventListener('click', (ev) => loadScript(ev, 'wipeout-minimal.js'));
+
+			var statusElement = document.getElementById('status');
+			var progressElement = document.getElementById('progress');
+			var spinnerElement = document.getElementById('spinner');
+
+			var Module = {
+				preRun: [],
+				postRun: [],
+				print: function(text) {
+					if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
+					console.log(text);
+				},
+				canvas: document.getElementById('canvas'),
+				setStatus: (text) => {
+					if (!Module.setStatus.last) {
+						Module.setStatus.last = { time: Date.now(), text: '' };
+					}
+					if (text === Module.setStatus.last.text) {
+						return;
+					}
+					var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
+					var now = Date.now();
+					if (m && now - Module.setStatus.last.time < 30) {
+						return; // if this is a progress update, skip it if too soon
+					}
+					Module.setStatus.last.time = now;
+					Module.setStatus.last.text = text;
+					if (m) {
+						text = m[1];
+						progressElement.value = parseInt(m[2])*100;
+						progressElement.max = parseInt(m[4])*100;
+						progressElement.hidden = false;
+						spinnerElement.hidden = false;
+					} else {
+						progressElement.value = null;
+						progressElement.max = null;
+						progressElement.hidden = true;
+						if (!text) {
+							spinnerElement.hidden = true;
+							document.getElementById('loading').style.display = 'none';
+						}
+					}
+					statusElement.innerHTML = text;
+				},
+				totalDependencies: 0,
+				monitorRunDependencies: (left) => {
+					Module.totalDependencies = Math.max(Module.totalDependencies, left);
+					Module.setStatus(left ? 'preparing... (' + (Module.totalDependencies-left) + '/' + Module.totalDependencies + ')' : 'all downloads complete.');
+				}
+			};
+			Module.setStatus('downloading...');
+			window.onerror = () => {
+				Module.setStatus('Exception thrown, see JavaScript console');
+				document.getElementById('loading').style.display = 'block';
+				spinnerElement.style.display = 'none';
+				Module.setStatus = (text) => {
+					if (text) {
+						console.error('[post-exception status] ' + text);
+					}
+				};
+			};
+		</script>
+	</body>
+</html>
--- /dev/null
+++ b/src/wipeout/camera.c
@@ -1,0 +1,167 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../types.h"
+#include "../render.h"
+#include "../system.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "droid.h"
+#include "camera.h"
+
+void camera_init(camera_t *camera, section_t *section) {
+	camera->section = section;
+	for (int i = 0; i < 10; i++) {
+		camera->section = camera->section->next;
+	}
+
+	camera->position = camera->section->center;
+	camera->velocity = vec3(0, 0, 0);
+	camera->angle = vec3(0, 0, 0);
+	camera->angular_velocity = vec3(0, 0, 0);
+	camera->mat = mat4_identity();
+	camera->has_initial_section = false;
+}
+
+void camera_update(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->last_position = camera->position;
+	(camera->update_func)(camera, ship, droid);
+	camera->real_velocity = vec3_mulf(vec3_sub(camera->position, camera->last_position), 1.0/system_tick());
+}
+
+void camera_update_race_external(camera_t *camera, ship_t *ship, droid_t *droid) {
+	vec3_t pos = vec3_sub(ship->position, vec3_mulf(ship->dir_forward, 1024));
+	pos.y -= 200;
+	camera->section = track_nearest_section(pos, camera->section, NULL);
+	section_t *next = camera->section->next;
+
+	vec3_t target = vec3_project_to_ray(pos, next->center, camera->section->center);
+
+	vec3_t diff_from_center = vec3_sub(pos, target);
+	vec3_t acc = diff_from_center;
+	acc.y += vec3_len(diff_from_center) * 0.5;
+	
+	camera->velocity = vec3_sub(camera->velocity, vec3_mulf(acc, 0.015625 * 30 * system_tick()));
+	camera->velocity = vec3_sub(camera->velocity, vec3_mulf(camera->velocity, 0.125 * 30 * system_tick()));
+	pos = vec3_add(pos, camera->velocity);
+
+	camera->position = pos;
+	camera->angle = vec3(ship->angle.x, ship->angle.y, 0);
+}
+
+void camera_update_race_internal(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->section = ship->section;
+	camera->position = ship_cockpit(ship);
+	camera->angle = vec3(ship->angle.x, ship->angle.y, ship->angle.z);
+}
+
+void camera_update_race_intro(camera_t *camera, ship_t *ship, droid_t *droid) {
+	// Set to final position
+	vec3_t pos = vec3_sub(ship->position, vec3_mulf(ship->dir_forward, 0.25 * 4096));
+
+	pos.x += sin(( (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30 * 3.0 * M_PI * 2) / 4096.0) * 4096;
+	pos.y -= (2 *  (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30) + 200;
+	pos.z += sin(( (ship->update_timer - UPDATE_TIME_RACE_VIEW) * 30 * 3.0 * M_PI * 2) / 4096.0) * 4096;
+
+	if (!camera->has_initial_section) {
+		camera->section = ship->section;
+		camera->has_initial_section = true;
+	}
+	else {
+		camera->section = track_nearest_section(pos, camera->section, NULL);
+	}
+
+	camera->position = pos;
+	camera->angle.z = 0;
+	camera->angle.x = ship->angle.x * 0.5;
+	vec3_t target = vec3_sub(ship->position, pos);
+
+	camera->angle.y = -atan2(target.x, target.z);
+
+	if (ship->update_timer <= UPDATE_TIME_RACE_VIEW) {
+		flags_add(ship->flags, SHIP_VIEW_INTERNAL);
+		camera->update_func = camera_update_race_internal;
+	}
+}
+
+void camera_update_attract_circle(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->update_timer -= system_tick();
+	if (camera->update_timer <= 0) {
+		camera->update_func = camera_update_attract_random;
+	}
+	// FIXME: not exactly sure what I'm doing here. The PSX version behaves
+	// differently.
+	camera->section = ship->section;
+
+	camera->position.x = ship->position.x + sin(ship->angle.y) * 512;
+	camera->position.y = ship->position.y + ((ship->angle.x * 512 / (M_PI * 2)) - 200);
+	camera->position.z = ship->position.z - cos(ship->angle.y) * 512;
+
+	camera->position.x += sin(camera->update_timer * 0.25) * 512;
+	camera->position.y -= 400;
+	camera->position.z += cos(camera->update_timer * 0.25) * 512;
+	camera->position = vec3_sub(camera->position, vec3_mulf(ship->dir_up, 256));
+
+	vec3_t target = vec3_sub(ship->position, camera->position);
+	float height = sqrt(target.x * target.x + target.z * target.z);
+	camera->angle.x = -atan2(target.y, height);
+	camera->angle.y = -atan2(target.x, target.z);
+}
+
+void camera_update_rescue(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->position = vec3_add(camera->section->center, vec3(300, -1500, 300));
+
+	vec3_t target = vec3_sub(droid->position, camera->position);
+	float height = sqrt(target.x * target.x + target.z * target.z);
+	camera->angle.x = -atan2(target.y, height);
+	camera->angle.y = -atan2(target.x, target.z);
+}
+
+
+void camera_update_attract_internal(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->update_timer -= system_tick();
+	if (camera->update_timer <= 0) {
+		camera->update_func = camera_update_attract_random;
+	}
+
+	camera->section = ship->section;
+	camera->position = ship_cockpit(ship);
+	camera->angle = vec3(ship->angle.x, ship->angle.y, 0); // No roll
+}
+
+void camera_update_static_follow(camera_t *camera, ship_t *ship, droid_t *droid) {
+	camera->update_timer -= system_tick();
+	if (camera->update_timer <= 0) {
+		camera->update_func = camera_update_attract_random;
+	}
+
+	vec3_t target = vec3_sub(ship->position, camera->position);
+	float height = sqrt(target.x * target.x + target.z * target.z);
+	camera->angle.x = -atan2(target.y, height);
+	camera->angle.y = -atan2(target.x, target.z);
+}
+
+void camera_update_attract_random(camera_t *camera, ship_t *ship, droid_t *droid) {
+	flags_rm(ship->flags, SHIP_VIEW_INTERNAL);
+
+	if (rand() % 2) {
+		camera->update_func = camera_update_attract_circle;
+		camera->update_timer = 5;
+	}
+	else {
+		camera->update_func = camera_update_static_follow;
+		camera->update_timer = 5;
+		section_t *section = ship->section->next;
+		for (int i = 0; i < 10; i++) {
+			section = section->next;
+		}
+
+		camera->section = section;
+		camera->position = section->center;
+		camera->position.y -= 500;
+	}
+
+	(camera->update_func)(camera, ship, droid);
+}
--- /dev/null
+++ b/src/wipeout/camera.h
@@ -1,0 +1,32 @@
+#ifndef CAMERA_H
+#define CAMERA_H
+
+#include "../types.h"
+#include "droid.h"
+
+typedef struct camera_t {
+	vec3_t position;
+	vec3_t velocity;
+	vec3_t angle;
+	vec3_t angular_velocity;
+	vec3_t last_position;
+	vec3_t real_velocity;
+	mat4_t mat;
+	section_t *section;
+	bool has_initial_section;
+	float update_timer;
+	void (*update_func)(struct camera_t *, ship_t *, droid_t *);
+} camera_t;
+
+void camera_init(camera_t *camera, section_t *section);
+void camera_update(camera_t *camera, ship_t *ship, droid_t *droid);
+void camera_update_race_external(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_race_internal(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_race_intro(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_attract_circle(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_attract_internal(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_static_follow(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_attract_random(camera_t *, ship_t *camShip, droid_t *);
+void camera_update_rescue(camera_t *, ship_t *camShip, droid_t *);
+
+#endif
--- /dev/null
+++ b/src/wipeout/droid.c
@@ -1,0 +1,260 @@
+#include "../types.h"
+#include "../mem.h"
+#include "../system.h"
+#include "../utils.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "hud.h"
+#include "droid.h"
+#include "camera.h"
+#include "image.h"
+#include "scene.h"
+#include "object.h"
+#include "game.h"
+
+static Object *droid_model;
+
+void droid_load() {
+	texture_list_t droid_textures = image_get_compressed_textures("wipeout/common/rescu.cmp");
+	droid_model = objects_load("wipeout/common/rescu.prm", droid_textures);
+}
+
+void droid_init(droid_t *droid, ship_t *ship) {
+	droid->section = g.track.sections;
+
+	while (flags_not(droid->section->flags, SECTION_JUMP)) {
+		droid->section = droid->section->next;
+	}
+
+	droid->position = vec3_add(ship->position, vec3(0, -200, 0));
+	droid->velocity = vec3(0, 0, 0);
+	droid->acceleration = vec3(0, 0, 0);
+	droid->angle = vec3(0, 0, 0);
+	droid->angular_velocity = vec3(0, 0, 0);
+	droid->update_timer = DROID_UPDATE_TIME_INITIAL;
+	droid->mat = mat4_identity();
+
+	droid->cycle_timer = 0;
+	droid->update_func = droid_update_intro;
+
+	droid->sfx_tractor = sfx_reserve_loop(SFX_TRACTOR);
+	flags_rm(droid->sfx_tractor->flags, SFX_PLAY);
+}
+
+void droid_draw(droid_t *droid) {
+	droid->cycle_timer += system_tick() * M_PI * 2;
+
+	Prm prm = {.primitive = droid_model->primitives};
+	int rf = sin(droid->cycle_timer) * 127 + 128;
+	int gf = sin(droid->cycle_timer + 0.2) * 127 + 128;
+	int bf = sin(droid->cycle_timer * 0.5 + 0.1) * 127 + 128;
+
+	int r, g, b;
+
+	for (int i = 0; i < 11; i++) {
+		if (i < 2) {
+			r = 40;
+			g = gf;
+			b = 40;
+		}
+		else if (i < 6) {
+			r = bf >> 1;
+			b = bf;
+			g = bf >> 1;
+		}
+		else {
+			r = rf;
+			b = 40;
+			g = 40;
+		}
+
+		switch (prm.f3->type) {
+			case PRM_TYPE_GT3:
+				prm.gt3->colour[0].as_rgba.r = r;
+				prm.gt3->colour[0].as_rgba.g = g;
+				prm.gt3->colour[0].as_rgba.b = b;
+
+				prm.gt3->colour[1].as_rgba.r = r;
+				prm.gt3->colour[1].as_rgba.g = g;
+				prm.gt3->colour[1].as_rgba.b = b;
+
+				prm.gt3->colour[2].as_rgba.r = r;
+				prm.gt3->colour[2].as_rgba.g = g;
+				prm.gt3->colour[2].as_rgba.b = b;
+				prm.gt3++;
+				break;
+
+			case PRM_TYPE_GT4:
+				prm.gt4->colour[0].as_rgba.r = r;
+				prm.gt4->colour[0].as_rgba.g = g;
+				prm.gt4->colour[0].as_rgba.b = b;
+
+				prm.gt4->colour[1].as_rgba.r = r;
+				prm.gt4->colour[1].as_rgba.g = g;
+				prm.gt4->colour[1].as_rgba.b = b;
+
+				prm.gt4->colour[2].as_rgba.r = r;
+				prm.gt4->colour[2].as_rgba.g = g;
+				prm.gt4->colour[2].as_rgba.b = b;
+
+				prm.gt4->colour[3].as_rgba.r = 40;
+				prm.gt4->colour[3].as_rgba.g = 40;
+				prm.gt4->colour[3].as_rgba.b = 40;
+				prm.gt4++;
+				break;
+		}
+	}
+
+	mat4_set_translation(&droid->mat, droid->position);
+	mat4_set_yaw_pitch_roll(&droid->mat, droid->angle);
+	object_draw(droid_model, &droid->mat);
+}
+
+void droid_update(droid_t *droid, ship_t *ship) {
+	(droid->update_func)(droid, ship);
+
+	droid->velocity = vec3_add(droid->velocity, vec3_mulf(droid->acceleration, 30 * system_tick()));
+	droid->velocity = vec3_sub(droid->velocity, vec3_mulf(droid->velocity, 0.125 * 30 * system_tick()));
+	droid->position = vec3_add(droid->position, vec3_mulf(droid->velocity, 0.015625 * 30 * system_tick()));
+	droid->angle = vec3_add(droid->angle, vec3_mulf(droid->angular_velocity, system_tick()));
+	droid->angle = vec3_wrap_angle(droid->angle);
+	
+	if (flags_is(droid->sfx_tractor->flags, SFX_PLAY)) {
+		sfx_set_position(droid->sfx_tractor, droid->position, droid->velocity, 0.5);
+	}
+}
+
+void droid_update_intro(droid_t *droid, ship_t *ship) {
+	droid->update_timer -= system_tick();
+
+	if (droid->update_timer < DROID_UPDATE_TIME_INTRO_3) {
+		droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.25 * 4096.0;
+		droid->acceleration.y = 0;
+		droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.25 * 4096.0;
+		droid->angular_velocity.y = 0;
+	}
+
+	else if (droid->update_timer < DROID_UPDATE_TIME_INTRO_2) {
+		droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096.0;
+		droid->acceleration.y = -140;
+		droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096.0;
+		droid->angular_velocity.y = (-8.0 / 4096.0) * M_PI * 2 * 30;
+	}
+
+	else if (droid->update_timer < DROID_UPDATE_TIME_INTRO_1) {
+		droid->acceleration.y -= 90 * system_tick();
+		droid->angular_velocity.y = (8.0 / 4096.0) * M_PI * 2 * 30;
+	}
+
+	if (droid->update_timer <= 0) {
+		droid->update_timer = DROID_UPDATE_TIME_INITIAL;
+		droid->update_func = droid_update_idle;
+		droid->position.x = droid->section->center.x;
+		droid->position.y = -3000;
+		droid->position.z = droid->section->center.z;
+	}
+}
+
+void droid_update_idle(droid_t *droid, ship_t *ship) {
+	section_t *next = droid->section->next;
+
+	vec3_t target = vec3(
+		(droid->section->center.x + next->center.x) * 0.5,
+		droid->section->center.y - 3000,
+		(droid->section->center.z + next->center.z) * 0.5
+	);
+
+	vec3_t target_vector = vec3_sub(target, droid->position);
+
+	float target_heading = -atan2(target_vector.x, target_vector.z);
+	float quickest_turn = target_heading - droid->angle.y;
+	float turn;
+	if (droid->angle.y < 0) {
+		turn = target_heading - (droid->angle.y + M_PI*2);
+	}
+	else {
+		turn = target_heading - (droid->angle.y - M_PI*2);
+	}
+
+	if (fabsf(turn) < fabsf(quickest_turn)) {
+		droid->angular_velocity.y = turn * 30 / 64.0;
+	}
+	else {
+		droid->angular_velocity.y = quickest_turn * 30.0 / 64.0;
+	}
+
+	droid->acceleration.x = (-sin(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096;
+	droid->acceleration.y = target_vector.y / 64.0;
+	droid->acceleration.z = (cos(droid->angle.y) * cos(droid->angle.x)) * 0.125 * 4096;
+
+	if (flags_is(ship->flags, SHIP_IN_RESCUE)) {
+		flags_add(droid->sfx_tractor->flags, SFX_PLAY);
+
+		droid->update_func = droid_update_rescue;
+		droid->update_timer = DROID_UPDATE_TIME_INITIAL;
+
+		g.camera.update_func = camera_update_rescue;
+		flags_add(ship->flags, SHIP_VIEW_REMOTE);
+		if (flags_is(ship->section->flags, SECTION_JUMP)) {
+			g.camera.section = ship->section->next;
+		}
+		else {
+			g.camera.section = ship->section;
+		}
+
+		// If droid is not nearby the rescue position teleport it in!
+		if (droid->section != ship->section && droid->section != ship->section->prev) {
+			droid->section = ship->section;
+			section_t *next = droid->section->next;
+
+			droid->position.x = (droid->section->center.x + next->center.x) * 0.5;
+			droid->position.y = droid->section->center.y - 3000;
+			droid->position.z = (droid->section->center.z + next->center.z) * 0.5;
+		}
+		flags_rm(ship->flags, SHIP_IN_TOW);
+		droid->velocity = vec3(0,0,0);
+		droid->acceleration = vec3(0,0,0);
+	}
+
+	// AdjustDirectionalNote(START_SIREN, 0, 0, (VECTOR){droid->position.x, droid->position.y, droid->position.z});
+}
+
+void droid_update_rescue(droid_t *droid, ship_t *ship) {
+	droid->angular_velocity.y = 0;
+	droid->angle.y = ship->angle.y;
+
+	vec3_t target = vec3(ship->position.x, ship->position.y - 350, ship->position.z);
+	vec3_t distance = vec3_sub(target, droid->position);
+
+
+	if (flags_is(ship->flags, SHIP_IN_TOW)) {
+		droid->velocity = vec3(0,0,0);
+		droid->acceleration = vec3(0,0,0);
+		droid->position = target;
+	}
+	else if (vec3_len(distance) < 8) {
+		flags_add(ship->flags, SHIP_IN_TOW);
+		droid->velocity = vec3(0,0,0);
+		droid->acceleration = vec3(0,0,0);
+		droid->position = target;
+	}
+	else {
+		droid->velocity = vec3_mulf(distance, 16);	
+	}
+
+
+	// Are we done rescuing?
+	if (flags_not(ship->flags, SHIP_IN_RESCUE)) {
+		flags_rm(droid->sfx_tractor->flags, SFX_PLAY);
+		droid->siren_started = false;
+		droid->update_func = droid_update_idle;
+		droid->update_timer = DROID_UPDATE_TIME_INITIAL;
+
+		while (flags_not(droid->section->flags, SECTION_JUMP)) {
+			droid->section = droid->section->prev;
+		}
+	}
+}
--- /dev/null
+++ b/src/wipeout/droid.h
@@ -1,0 +1,39 @@
+#ifndef DROID_H
+#define DROID_H
+
+#include "../types.h"
+#include "track.h"
+#include "ship.h"
+#include "sfx.h"
+
+#define DROID_UPDATE_TIME_INITIAL (800 * (1.0/30.0))
+#define DROID_UPDATE_TIME_INTRO_1 (770 * (1.0/30.0))
+#define DROID_UPDATE_TIME_INTRO_2 (710 * (1.0/30.0))
+#define DROID_UPDATE_TIME_INTRO_3 (400 * (1.0/30.0))
+
+typedef struct droid_t {
+	section_t *section;
+	vec3_t position;
+	vec3_t velocity;
+	vec3_t acceleration;
+	vec3_t angle;
+	vec3_t angular_velocity;
+	bool siren_started;
+	float cycle_timer;
+	float update_timer;
+	void (*update_func)(struct droid_t *, ship_t *);
+	mat4_t mat;
+	Object *model;
+	sfx_t *sfx_tractor;
+} droid_t;
+
+void droid_draw(droid_t *droid);
+
+void droid_load();
+void droid_init(droid_t *droid, ship_t *ship);
+void droid_update(droid_t *droid, ship_t *ship);
+void droid_update_intro(droid_t *droid, ship_t *ship);
+void droid_update_idle(droid_t *droid, ship_t *ship);
+void droid_update_rescue(droid_t *droid, ship_t *ship);
+
+#endif
--- /dev/null
+++ b/src/wipeout/game.c
@@ -1,0 +1,643 @@
+#include <string.h>
+
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+#include "../platform.h"
+#include "../input.h"
+
+#include "game.h"
+#include "ship.h"
+#include "weapon.h"
+#include "droid.h"
+#include "object.h"
+#include "hud.h"
+#include "game.h"
+#include "sfx.h"
+#include "ui.h"
+#include "particle.h"
+#include "race.h"
+#include "main_menu.h"
+#include "title.h"
+#include "intro.h"
+
+#define TURN_ACCEL(V) NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(YAW_VELOCITY(V))))
+#define TURN_VEL(V)   NTSC_VELOCITY(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(YAW_VELOCITY(V))))
+
+const game_def_t def = {
+	.race_classes = {
+		[RACE_CLASS_VENOM] =  {.name = "VENOM CLASS"},
+		[RACE_CLASS_RAPIER] = {.name = "RAPIER CLASS"},
+	},
+
+	.race_types = {
+		[RACE_TYPE_CHAMPIONSHIP] = {.name = "CHAMPIONSHIP RACE"},
+		[RACE_TYPE_SINGLE]       = {.name = "SINGLE RACE"},
+		[RACE_TYPE_TIME_TRIAL]   = {.name = "TIME TRIAL"},
+	},
+
+	.pilots = {
+		[PILOT_JOHN_DEKKA]           = {.name = "JOHN DEKKA",           .portrait = "wipeout/textures/dekka.cmp", .team = 0, .logo_model = 0},
+		[PILOT_DANIEL_CHANG]         = {.name = "DANIEL CHANG",         .portrait = "wipeout/textures/chang.cmp", .team = 0, .logo_model = 4},
+		[PILOT_ARIAL_TETSUO]         = {.name = "ARIAL TETSUO",         .portrait = "wipeout/textures/arial.cmp", .team = 1, .logo_model = 6},
+		[PILOT_ANASTASIA_CHEROVOSKI] = {.name = "ANASTASIA CHEROVOSKI", .portrait = "wipeout/textures/anast.cmp", .team = 1, .logo_model = 7},
+		[PILOT_KEL_SOLAAR]           = {.name = "KEL SOLAAR",           .portrait = "wipeout/textures/solar.cmp", .team = 2, .logo_model = 2},
+		[PILOT_ARIAN_TETSUO]         = {.name = "ARIAN TETSUO",         .portrait = "wipeout/textures/arian.cmp", .team = 2, .logo_model = 5},
+		[PILOT_SOFIA_DE_LA_RENTE]    = {.name = "SOFIA DE LA RENTE",    .portrait = "wipeout/textures/sophi.cmp", .team = 3, .logo_model = 1},
+		[PILOT_PAUL_JACKSON]         = {.name = "PAUL JACKSON",         .portrait = "wipeout/textures/paul.cmp",  .team = 3, .logo_model = 3},
+	},
+
+	.ship_model_to_pilot = {6, 4, 7, 1, 5, 2, 3, 0},
+	.race_points_for_rank = {9, 7, 5, 3, 2, 1, 0, 0},
+
+	// SHIP ATTRIBUTES
+	//               TEAM 1   TEAM 2   TEAM 3   TEAM 4
+	// Acceleration:    ***    *****       **     ****
+	//    Top Speed:   ****       **     ****      ***
+	//       Armour:  *****      ***     ****       **
+	//    Turn Rate:     **     ****      ***    *****
+
+	.teams = {
+		[TEAM_AG_SYSTEMS] = {
+			.name = "AG SYSTEMS",
+			.logo_model = 2,
+			.pilots = {0, 1},
+			.attributes = {
+				[RACE_CLASS_VENOM]  = {.mass = 150, .thrust_max =  790, .resistance = 140, .turn_rate = TURN_ACCEL(160), .turn_rate_max = TURN_VEL(2560), .skid = 12},
+				[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1200, .resistance = 140, .turn_rate = TURN_ACCEL(160), .turn_rate_max = TURN_VEL(2560), .skid = 10},
+			},
+		},
+		[TEAM_AURICOM] = {
+			.name = "AURICOM",
+			.logo_model = 3,
+			.pilots = {2, 3},
+			.attributes = {
+				[RACE_CLASS_VENOM]  = {.mass = 150, .thrust_max =  850, .resistance = 134, .turn_rate = TURN_ACCEL(140), .turn_rate_max = TURN_VEL(1920), .skid = 20},
+				[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1400, .resistance = 140, .turn_rate = TURN_ACCEL(120), .turn_rate_max = TURN_VEL(1920), .skid = 14},
+			},
+		},
+		[TEAM_QIREX] = {
+			.name = "QIREX",
+			.logo_model = 1,
+			.pilots = {4, 5},
+			.attributes = {
+				[RACE_CLASS_VENOM]  = {.mass = 150, .thrust_max =  850, .resistance = 140, .turn_rate = TURN_ACCEL(120), .turn_rate_max = TURN_VEL(1920), .skid = 24},
+				[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1400, .resistance = 130, .turn_rate = TURN_ACCEL(140), .turn_rate_max = TURN_VEL(1920), .skid = 16},
+			},
+		},
+		[TEAM_FEISAR] = {
+			.name = "FEISAR",
+			.logo_model = 0,
+			.pilots = {6, 7},
+			.attributes = {
+				[RACE_CLASS_VENOM]  = {.mass = 150, .thrust_max =  790, .resistance = 134, .turn_rate = TURN_ACCEL(180), .turn_rate_max = TURN_VEL(2560), .skid = 12},
+				[RACE_CLASS_RAPIER] = {.mass = 150, .thrust_max = 1200, .resistance = 130, .turn_rate = TURN_ACCEL(180), .turn_rate_max = TURN_VEL(2560), .skid =  8},
+			},
+		},
+	},
+
+	.ai_settings = {
+		[RACE_CLASS_VENOM] = {
+			{.thrust_max = 2550, .thrust_magnitude = 44, .fight_back = 1},
+			{.thrust_max = 2600, .thrust_magnitude = 45, .fight_back = 1},
+			{.thrust_max = 2630, .thrust_magnitude = 45, .fight_back = 1},
+			{.thrust_max = 2660, .thrust_magnitude = 46, .fight_back = 1},
+			{.thrust_max = 2700, .thrust_magnitude = 47, .fight_back = 1},
+			{.thrust_max = 2720, .thrust_magnitude = 48, .fight_back = 1},
+			{.thrust_max = 2750, .thrust_magnitude = 49, .fight_back = 1},
+		},
+		[RACE_CLASS_RAPIER] = {
+			{.thrust_max = 3750, .thrust_magnitude = 50, .fight_back = 1},
+			{.thrust_max = 3780, .thrust_magnitude = 53, .fight_back = 1},
+			{.thrust_max = 3800, .thrust_magnitude = 55, .fight_back = 1},
+			{.thrust_max = 3850, .thrust_magnitude = 57, .fight_back = 1},
+			{.thrust_max = 3900, .thrust_magnitude = 60, .fight_back = 1},
+			{.thrust_max = 3950, .thrust_magnitude = 62, .fight_back = 1},
+			{.thrust_max = 4000, .thrust_magnitude = 65, .fight_back = 1},
+		},
+	},
+
+	.circuts = {
+		[CIRCUT_ALTIMA_VII] = {
+			.name = "ALTIMA VII",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track02/", .start_line_pos = 27, .behind_speed = 300, .spread_base = 80, .spread_factor = 20, .sky_y_offset = -2520},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track03/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 80, .spread_factor = 11, .sky_y_offset = -1930},
+			}
+		},
+		[CIRCUT_KARBONIS_V] = {
+			.name = "KARBONIS V",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track04/", .start_line_pos = 16, .behind_speed = 200, .spread_base = 10, .spread_factor =  8, .sky_y_offset = -5000},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track05/", .start_line_pos = 16, .behind_speed = 500, .spread_base = 10, .spread_factor =  8, .sky_y_offset = -5000},
+			}
+		},
+		[CIRCUT_TERRAMAX] = {
+			.name = "TERRAMAX",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track01/", .start_line_pos = 27, .behind_speed = 350, .spread_base = 60, .spread_factor = 11, .sky_y_offset =  -820},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track06/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 10, .spread_factor =  8, .sky_y_offset =     0},
+			}
+		},
+		[CIRCUT_KORODERA] = {
+			.name = "KORODERA",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track12/", .start_line_pos = 16, .behind_speed = 450, .spread_base = 40, .spread_factor = 11, .sky_y_offset = -2120},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track07/", .start_line_pos = 16, .behind_speed = 500, .spread_base = 30, .spread_factor = 11, .sky_y_offset = -2260},
+			}
+		},
+		[CIRCUT_ARRIDOS_IV] = {
+			.name = "ARRIDOS IV",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track08/", .start_line_pos = 16, .behind_speed = 350, .spread_base = 80, .spread_factor = 15, .sky_y_offset =   -40},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track11/", .start_line_pos = 16, .behind_speed = 450, .spread_base = 30, .spread_factor = 11, .sky_y_offset =  -240},
+			}
+		},
+		[CIRCUT_SILVERSTREAM] = {
+			.name = "SILVERSTREAM",
+			.is_bonus_circut = false,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track09/", .start_line_pos = 16, .behind_speed = 150, .spread_base = 10, .spread_factor =  8, .sky_y_offset = -2700},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track13/", .start_line_pos = 16, .behind_speed = 150, .spread_base = 10, .spread_factor =  8, .sky_y_offset = -2700},
+			}
+		},
+		[CIRCUT_FIRESTAR] = {
+			.name = "FIRESTAR",
+			.is_bonus_circut = true,
+			.settings = {
+				[RACE_CLASS_VENOM]  = {.path = "wipeout/track10/", .start_line_pos = 27, .behind_speed = 200, .spread_base = 40, .spread_factor = 11, .sky_y_offset =     0},
+				[RACE_CLASS_RAPIER] = {.path = "wipeout/track14/", .start_line_pos = 27, .behind_speed = 500, .spread_base = 40, .spread_factor = 11, .sky_y_offset =     0},
+			}
+		},
+	},
+	.music = {
+		{.path = "wipeout/music/track01.qoa", .name = "CAIRODROME"},
+		{.path = "wipeout/music/track02.qoa", .name = "CARDINAL DANCER"},
+		{.path = "wipeout/music/track03.qoa", .name = "COLD COMFORT"},
+		{.path = "wipeout/music/track04.qoa", .name = "DOH T"},
+		{.path = "wipeout/music/track05.qoa", .name = "MESSIJ"},
+		{.path = "wipeout/music/track06.qoa", .name = "OPERATIQUE"},
+		{.path = "wipeout/music/track07.qoa", .name = "TENTATIVE"},
+		{.path = "wipeout/music/track08.qoa", .name = "TRANCEVAAL"},
+		{.path = "wipeout/music/track09.qoa", .name = "AFRO RIDE"},
+		{.path = "wipeout/music/track10.qoa", .name = "CHEMICAL BEATS"},
+		{.path = "wipeout/music/track11.qoa", .name = "WIPEOUT"},
+	},
+	.credits = {
+		"#MANAGING DIRECTORS",
+			"IAN HETHERINGTON",
+			"JONATHAN ELLIS",
+		"#DIRECTOR OF DEVELOPMENT",
+			"JOHN WHITE",
+		"#PRODUCERS",
+			"DOMINIC MALLINSON",
+			"ANDY YELLAND",
+		"#PRODUCT MANAGER",
+			"SUE CAMPBELL",
+		"#GAME DESIGNER",
+			"NICK BURCOMBE",
+			"",
+			"",
+		"#PLAYSTATION VERSION",
+		"#PROGRAMMERS",
+			"DAVE ROSE",
+			"ROB SMITH",
+			"JASON DENTON",
+			"STEWART SOCKETT",
+		"#ORIGINAL ARTISTS",
+			"NICKY CARUS WESTCOTT",
+			"LAURA GRIEVE",
+			"LOUISE SMITH",
+			"DARREN DOUGLAS",
+			"POL SIGERSON",
+		"#INTRO SEQUENCE",
+			"LEE CARUS WESTCOTT",
+		"#CONCEPTUAL ARTIST",
+			"JIM BOWERS",
+		"#ADDITIONAL GRAPHIC DESIGN",
+			"THE DESIGNERS REPUBLIC",
+		"#MUSIC",
+			"ORBITAL",
+			"CHEMICAL BROTHERS",
+			"LEFTFIELD",
+			"COLD STORAGE",
+		"#SOUND EFFECTS",
+			"TIM WRIGHT",
+		"#MANUAL WRITTEN BY",
+			"DAMON FAIRCLOUGH",
+			"NICK BURCOMBE",
+		"#PACKAGING DESIGN",
+			"THE DESIGNERS REPUBLIC",
+			"KEITH HOPWOOD",
+			"",
+			"",
+		"#PC VERSION",
+		"#PROGRAMMERS",
+			"ANDY YELLAND",
+			"ANDY SATTERTHWAITE",
+			"DAVE SMITH",
+			"MARK KELLY",
+			"JED ADAMS",
+			"STEVE WARD",
+			"CHRIS EDEN",
+			"SALIM SIWANI",
+		"#SOUND PROGRAMMING",
+			"ANDY CROWLEY",
+		"#MOVIE PROGRAMMING",
+			"MIKE ANTHONY",
+		"#CONVERSION ARTISTS",
+			"JOHN DWYER",
+			"GARY BURLEY",
+			"",
+			"",
+		"#ATI 3D RAGE VERSION",
+		"#PRODUCER",
+			"BILL ALLEN",
+		"#DEVELOPED BY",
+		"#BROADSWORD INTERACTIVE LTD",
+			"STEPHEN ROSE",
+			"JOHN JONES STEELE",
+			"",
+			"",
+		"#2023 REWRITE",
+			"PHOBOSLAB",
+			"DOMINIC SZABLEWSKI",
+			"",
+			"",
+		"#DEVELOPMENT SECRETARY",
+			"JENNIFER REES",
+			"",
+			"",
+		"#QUALITY ASSURANCE",
+			"STUART ALLEN",
+			"CHRIS GRAHAM",
+			"THOMAS REES",
+			"BRIAN WALSH",
+			"CARL BERRY",
+			"MARK INMAN",
+			"PAUL TWEEDLE",
+			"ANTHONY CROSS",
+			"EDWARD HAY",
+			"ROB WOLFE",
+			"",
+			"",
+		"#SPECIAL THANKS TO",
+			"THE HACKERS TEAM MGM",
+			"SOFTIMAGE",
+			"SGI",
+			"GLEN OCONNELL",
+			"JOANNE GALVIN",
+			"ALL AT PSYGNOSIS",
+	},
+	.congratulations = {
+		.venom = {
+			"#WELL DONE",
+			"",
+			"VENOM CLASS",
+			"",
+			"COMPETENCE ACHIEVED",
+			"",
+			"YOU HAVE NOW QUALIFIED",
+			"",
+			"FOR THE ULTRA FAST",
+			"",
+			"RAPIER CLASS",
+			"",
+			"WE RECOMMEND YOU",
+			"",
+			"SAVE YOUR CURRENT GAME",
+		},
+		.venom_all_circuts = {
+			"#AMAZING",
+			"",
+			"YOU HAVE COMPLETED THE FULL",
+			"",
+			"VENOM CLASS CHAMPIONSHIP",
+			"",
+			"",
+			"WELL DONE",
+			"",
+			"YOU ARE A GREAT PILOT",
+			"",
+			"",
+			"",
+			"NOW TAKE ON THE FULL",
+			"",
+			"RAPIER CLASS CHAMPIONSHIP",
+			"",
+			"",
+			"#KEEP GOING",
+		},
+		.rapier = {
+			"#CONGRATULATIONS",
+			"",
+			"RAPIER CLASS",
+			"",
+			"COMPETENCE ACHIEVED",
+			"",
+			"YOU NOW HAVE ACCESS TO THE",
+			"",
+			"FULL VENOM AND RAPIER",
+			"",
+			"CHAMPIONSHIPS WITH THE ",
+			"",
+			"NEWLY CONSTRUCTED CIRCUIT",
+			"",
+			"FIRESTAR",
+			"",
+			"",
+			"",
+			"WE RECOMMEND YOU",
+			"",
+			"SAVE",
+			"",
+			"YOUR CURRENT GAME",
+			"",
+			"",
+			"#GOOD LUCK",
+		},
+		.rapier_all_circuts = {
+			"#AWESOME",
+			"",
+			"YOU HAVE BEATEN",
+			"#WIPEOUT",
+			"",
+			"YOU ARE A TRULY",
+			"",
+			"AMAZING PILOT",
+			"",
+			"",
+			"",
+			"#CONGRATULATIONS",
+			"",
+			"",
+			"",
+			"",
+			"#A BIG THANKS",
+			"",
+			"FROM ALL OF US ON THE TEAM",
+			"",
+			"LOOK OUT FOR",
+			"#WIPEOUT II",
+			"",
+			"COMING SOON",
+		},
+	}
+};
+
+save_t save = {
+	.magic = SAVE_DATA_MAGIC,
+	.is_dirty = true,
+
+	.sfx_volume = 0.6,
+	.music_volume = 0.5,
+	.ui_scale = 0,
+	.show_fps = false,
+	.fullscreen = false,
+
+	.has_rapier_class = true,  // for testing; should be false in prod
+	.has_bonus_circuts = true, // for testing; should be false in prod
+	.highscores_name = {0,0,0,0},
+
+	.highscores = {
+		[RACE_CLASS_VENOM] = {
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 85.83, .entries = {{"WIP", 254.50},{"EOU", 271.17},{"TPC", 289.50},{"NOT", 294.50},{"PSX", 314.50}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 85.83, .entries = {{"MVE", 254.50},{"ALM", 271.17},{"POL", 289.50},{"NIK", 294.50},{"DAR", 314.50}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 55.33, .entries = {{"AJY", 159.33},{"AJS", 172.67},{"DLS", 191.00},{"MAK", 207.67},{"JED", 219.33}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 55.33, .entries = {{"DAR", 159.33},{"STU", 172.67},{"MOC", 191.00},{"DOM", 207.67},{"NIK", 219.33}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 57.5, .entries = {{ "JD", 171.00},{"AJC", 189.33},{"MSA", 202.67},{ "SD", 219.33},{"TIM", 232.67}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 57.5, .entries = {{"PHO", 171.00},{"ENI", 189.33},{ "XR", 202.67},{"ISI", 219.33},{ "NG", 232.67}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 85.17, .entries = {{"POL", 251.33},{"DAR", 263.00},{"JAS", 283.00},{"ROB", 294.67},{"DJR", 314.82}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 85.17, .entries = {{"DOM", 251.33},{"DJR", 263.00},{"MPI", 283.00},{"GOC", 294.67},{"SUE", 314.82}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 80.17, .entries = {{"NIK", 236.17},{"SAL", 253.17},{"DOM", 262.33},{ "LG", 282.67},{"LNK", 298.17}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 80.17, .entries = {{"NIK", 236.17},{"ROB", 253.17},{ "AM", 262.33},{"JAS", 282.67},{"DAR", 298.17}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 61.67, .entries = {{"HAN", 182.33},{"PER", 196.33},{"FEC", 214.83},{"TPI", 228.83},{"ZZA", 244.33}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 61.67, .entries = {{ "FC", 182.33},{"SUE", 196.33},{"ROB", 214.83},{"JEN", 228.83},{ "NT", 244.33}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 63.83, .entries = {{"CAN", 195.40},{"WEH", 209.23},{"AVE", 227.90},{"ABO", 239.90},{"NUS", 240.73}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 63.83, .entries = {{"DJR", 195.40},{"NIK", 209.23},{"JAS", 227.90},{"NCW", 239.90},{"LOU", 240.73}}},
+			},
+		},
+		[RACE_CLASS_RAPIER] = {
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 69.50, .entries = {{"AJY", 200.67},{"DLS", 213.50},{"AJS", 228.67},{"MAK", 247.67},{"JED", 263.00}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 69.50, .entries = {{"NCW", 200.67},{"LEE", 213.50},{"STU", 228.67},{"JAS", 247.67},{"ROB", 263.00}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 47.33, .entries = {{"BOR", 134.58},{"ING", 147.00},{"HIS", 162.25},{"COR", 183.08},{ "ES", 198.25}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 47.33, .entries = {{"NIK", 134.58},{"POL", 147.00},{"DAR", 162.25},{"STU", 183.08},{"ROB", 198.25}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 47.83, .entries = {{"AJS", 142.08},{"DLS", 159.42},{"MAK", 178.08},{"JED", 190.25},{"AJY", 206.58}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 47.83, .entries = {{"POL", 142.08},{"JIM", 159.42},{"TIM", 178.08},{"MOC", 190.25},{ "PC", 206.58}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 76.75, .entries = {{"DLS", 224.17},{"DJR", 237.00},{"LEE", 257.50},{"MOC", 272.83},{"MPI", 285.17}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 76.75, .entries = {{"TIM", 224.17},{"JIM", 237.00},{"NIK", 257.50},{"JAS", 272.83},{ "LG", 285.17}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 65.75, .entries = {{"MAK", 191.00},{"STU", 203.67},{"JAS", 221.83},{"ROB", 239.00},{"DOM", 254.50}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 65.75, .entries = {{ "LG", 191.00},{"LOU", 203.67},{"JIM", 221.83},{"HAN", 239.00},{ "NT", 254.50}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 59.23, .entries = {{"JED", 156.67},{"NCW", 170.33},{"LOU", 188.83},{"DAR", 201.00},{"POL", 221.50}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 59.23, .entries = {{"STU", 156.67},{"DAV", 170.33},{"DOM", 188.83},{"MOR", 201.00},{"GAN", 221.50}}},
+			},
+			{
+				[HIGHSCORE_TAB_RACE]       = {.lap_record = 55.00, .entries = {{ "PC", 162.42},{"POL", 179.58},{"DAR", 194.75},{"DAR", 208.92},{"MSC", 224.58}}},
+				[HIGHSCORE_TAB_TIME_TRIAL] = {.lap_record = 55.00, .entries = {{"THA", 162.42},{"NKS", 179.58},{"FOR", 194.75},{"PLA", 208.92},{"YIN", 224.58}}},
+			}
+		}
+	}
+};
+
+game_t g = {0};
+
+
+
+struct {
+	void (*init)();
+	void (*update)();
+} game_scenes[] = {
+	[GAME_SCENE_INTRO] = {intro_init, intro_update},
+	[GAME_SCENE_TITLE] = {title_init, title_update},
+	[GAME_SCENE_MAIN_MENU] = {main_menu_init, main_menu_update},
+	[GAME_SCENE_RACE] = {race_init, race_update},
+};
+
+static game_scene_t scene_current = GAME_SCENE_NONE;
+static game_scene_t scene_next = GAME_SCENE_NONE;
+static int global_textures_len = 0;
+static void *global_mem_mark = 0;
+
+void game_init() {
+	srand((int)(platform_now() * 100));
+	
+	ui_load();
+	sfx_load();
+	hud_load();
+	ships_load();
+	droid_load();
+	particles_load();
+	weapons_load();
+
+	global_textures_len = render_textures_len();
+	global_mem_mark = mem_mark();
+
+	sfx_music_mode(SFX_MUSIC_PAUSED);
+	sfx_music_play(rand_int(0, len(def.music)));
+
+
+	// System binds; always fixed
+	// Keyboard
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_UP, A_MENU_UP);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_DOWN, A_MENU_DOWN);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_LEFT, A_MENU_LEFT);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_RIGHT, A_MENU_RIGHT);
+
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_BACKSPACE, A_MENU_BACK);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_C, A_MENU_BACK);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_V, A_MENU_BACK);
+
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_X, A_MENU_SELECT);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_RETURN, A_MENU_START);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_KEY_ESCAPE, A_MENU_START);
+
+	// Gamepad
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_UP, A_MENU_UP);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_DOWN, A_MENU_DOWN);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_LEFT, A_MENU_LEFT);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_DPAD_RIGHT, A_MENU_RIGHT);
+
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_UP, A_MENU_UP);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_DOWN, A_MENU_DOWN);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_LEFT, A_MENU_LEFT);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_L_STICK_RIGHT, A_MENU_RIGHT);
+
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_X, A_MENU_BACK);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_B, A_MENU_BACK);
+
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_A, A_MENU_SELECT);
+	input_bind(INPUT_LAYER_SYSTEM, INPUT_GAMEPAD_START, A_MENU_START);
+
+
+
+	// User defined
+	// TODO: these should be configurable and stored in the save struct
+	// Keyboard
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_UP, A_UP);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_DOWN, A_DOWN);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_LEFT, A_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_RIGHT, A_RIGHT);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_C, A_BRAKE_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_V, A_BRAKE_RIGHT);
+
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_X, A_THRUST);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_Z, A_FIRE);
+	input_bind(INPUT_LAYER_USER, INPUT_KEY_A, A_CHANGE_VIEW);
+
+	// Gamepad
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_DPAD_UP, A_UP);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_DPAD_DOWN, A_DOWN);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_DPAD_LEFT, A_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_DPAD_RIGHT, A_RIGHT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_STICK_UP, A_UP);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_STICK_DOWN, A_DOWN);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_STICK_LEFT, A_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_STICK_RIGHT, A_RIGHT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_TRIGGER, A_BRAKE_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_R_TRIGGER, A_BRAKE_RIGHT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_L_SHOULDER, A_BRAKE_LEFT);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_R_SHOULDER, A_BRAKE_RIGHT);
+
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_A, A_THRUST);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_X, A_FIRE);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_Y, A_CHANGE_VIEW);
+	input_bind(INPUT_LAYER_USER, INPUT_GAMEPAD_SELECT, A_CHANGE_VIEW);
+
+
+	game_set_scene(GAME_SCENE_INTRO);
+
+	if (file_exists("save.dat")) {
+		uint32_t size;
+		save_t *save_file = (save_t *)file_load("save.dat", &size);
+		if (size == sizeof(save_t) && save_file->magic == SAVE_DATA_MAGIC) {
+			printf("load save data success\n");
+			memcpy(&save, save_file, sizeof(save_t));
+		}
+		mem_temp_free(save_file);
+	}
+}
+
+void game_set_scene(game_scene_t scene) {
+	sfx_reset();
+	scene_next = scene;
+}
+
+void game_reset_championship() {
+	for (int i = 0; i < len(g.championship_ranks); i++) {
+		g.championship_ranks[i].points = 0;
+		g.championship_ranks[i].pilot = i;
+	}
+	g.lives = NUM_LIVES;
+}
+
+void game_update() {
+	double frame_start_time = platform_now();
+
+	int sh = render_size().y;
+	int scale = max(1, sh >=  720 ? sh / 360 : sh / 240);
+	if (save.ui_scale && save.ui_scale < scale) {
+		scale = save.ui_scale;
+	}
+	ui_set_scale(scale);
+
+
+	if (scene_next != GAME_SCENE_NONE) {
+		scene_current = scene_next;
+		scene_next = GAME_SCENE_NONE;
+		render_textures_reset(global_textures_len);
+		mem_reset(global_mem_mark);
+		system_reset_cycle_time();
+
+		if (scene_current != GAME_SCENE_NONE) {
+			game_scenes[scene_current].init();
+		}
+	}
+
+	if (scene_current != GAME_SCENE_NONE) {
+		game_scenes[scene_current].update();
+	}
+
+	if (save.is_dirty) {
+		// FIXME: use a text based format?
+		// FIXME: this should probably run async somewhere
+		save.is_dirty = false;
+		file_store("save.dat", &save, sizeof(save_t)); 
+		printf("wrote save.dat\n");
+	}
+
+	double now = platform_now();
+	g.frame_time = now - frame_start_time;
+	if (g.frame_time > 0) {
+		g.frame_rate = ((double)g.frame_rate * 0.95) + (1.0/g.frame_time) * 0.05;
+	}
+}
+
--- /dev/null
+++ b/src/wipeout/game.h
@@ -1,0 +1,264 @@
+#ifndef GAME_H
+#define GAME_H
+
+#include "../types.h"
+
+#include "droid.h"
+#include "ship.h"
+#include "camera.h"
+#include "track.h"
+
+#define NUM_AI_OPPONENTS 7
+#define NUM_PILOTS_PER_TEAM 2
+#define NUM_NON_BONUS_CIRCUTS 6
+#define NUM_MUSIC_TRACKS 11
+#define NUM_HIGHSCORES 5
+
+#define NUM_LAPS 3
+#define NUM_LIVES 3
+#define QUALIFYING_RANK 3
+#define SAVE_DATA_MAGIC 0x64736f77
+
+typedef enum {
+	A_MENU_UP,
+	A_MENU_DOWN,
+	A_MENU_LEFT,
+	A_MENU_RIGHT,
+	A_MENU_BACK,
+	A_MENU_SELECT,
+	A_MENU_START,
+	A_MENU_QUIT,
+
+	A_UP,
+	A_DOWN,
+	A_LEFT,
+	A_RIGHT,
+	A_BRAKE_LEFT,
+	A_BRAKE_RIGHT,
+	A_THRUST,
+	A_FIRE,
+	A_CHANGE_VIEW,
+} action_t;
+
+
+typedef enum {
+	GAME_SCENE_INTRO,
+	GAME_SCENE_TITLE,
+	GAME_SCENE_MAIN_MENU,
+	GAME_SCENE_HIGHSCORES,
+	GAME_SCENE_RACE,
+	GAME_SCENE_NONE,
+	NUM_GAME_SCENES
+} game_scene_t;
+
+enum race_class {
+	RACE_CLASS_VENOM,
+	RACE_CLASS_RAPIER,
+	NUM_RACE_CLASSES
+};
+
+enum race_type {
+	RACE_TYPE_CHAMPIONSHIP,
+	RACE_TYPE_SINGLE,
+	RACE_TYPE_TIME_TRIAL,
+	NUM_RACE_TYPES,
+};
+
+enum highscore_tab {
+	HIGHSCORE_TAB_TIME_TRIAL,
+	HIGHSCORE_TAB_RACE,
+	NUM_HIGHSCORE_TABS
+};
+
+enum pilot {
+	PILOT_JOHN_DEKKA,
+	PILOT_DANIEL_CHANG,
+	PILOT_ARIAL_TETSUO,
+	PILOT_ANASTASIA_CHEROVOSKI,
+	PILOT_KEL_SOLAAR,
+	PILOT_ARIAN_TETSUO,
+	PILOT_SOFIA_DE_LA_RENTE,
+	PILOT_PAUL_JACKSON,
+	NUM_PILOTS
+};
+
+enum team {
+	TEAM_AG_SYSTEMS,
+	TEAM_AURICOM,
+	TEAM_QIREX,
+	TEAM_FEISAR,
+	NUM_TEAMS
+};
+
+enum circut {
+	CIRCUT_ALTIMA_VII,
+	CIRCUT_KARBONIS_V,
+	CIRCUT_TERRAMAX,
+	CIRCUT_KORODERA,
+	CIRCUT_ARRIDOS_IV,
+	CIRCUT_SILVERSTREAM,
+	CIRCUT_FIRESTAR,
+	NUM_CIRCUTS
+};
+
+
+// Game definitions
+
+typedef struct {
+	char *name;
+} race_class_t;
+
+typedef struct {
+	char *name;
+} race_type_t;
+
+typedef struct {
+	char *name;
+	char *portrait;
+	int logo_model;
+	int team;
+} pilot_t;
+
+typedef struct {
+	float thrust_max;
+	float thrust_magnitude;
+	bool fight_back;
+} ai_setting_t;
+
+typedef struct {
+	float mass;
+	float thrust_max;
+	float resistance;
+	float turn_rate;
+	float turn_rate_max;
+	float skid;
+} team_attributes_t;
+
+typedef struct {
+	char *name;
+	int logo_model;
+	int pilots[NUM_PILOTS_PER_TEAM];
+	team_attributes_t attributes[NUM_RACE_CLASSES];
+} team_t;
+
+typedef struct {
+	char *path;
+	float start_line_pos;
+	float behind_speed;
+	float spread_base;
+	float spread_factor;
+	float sky_y_offset;
+} circut_settings_t;
+
+typedef struct {
+	char *name;
+	bool is_bonus_circut;
+	circut_settings_t settings[NUM_RACE_CLASSES];
+} circut_t;
+
+typedef struct {
+	char *path;
+	char *name;
+} music_track_t;
+
+typedef struct {
+	race_class_t race_classes[NUM_RACE_CLASSES];
+	race_type_t race_types[NUM_RACE_TYPES];
+	pilot_t pilots[NUM_PILOTS];
+	team_t teams[NUM_TEAMS];
+	ai_setting_t ai_settings[NUM_RACE_CLASSES][NUM_AI_OPPONENTS];
+	circut_t circuts[NUM_CIRCUTS];
+	int ship_model_to_pilot[NUM_PILOTS];
+	int race_points_for_rank[NUM_PILOTS];
+	music_track_t music[NUM_MUSIC_TRACKS];
+	char *credits[104];
+	struct {
+		char *venom[15];
+		char *venom_all_circuts[19];
+		char *rapier[26];
+		char *rapier_all_circuts[24];
+	} congratulations;
+} game_def_t;
+
+
+
+// Running game data
+
+typedef struct {
+	uint16_t pilot;
+	uint16_t points;
+} pilot_points_t;
+
+typedef struct {
+	float frame_time;
+	float frame_rate;
+	
+	int race_class;
+	int race_type;
+	int highscore_tab;
+	int team;
+	int pilot;
+	int circut;
+	bool is_attract_mode;
+	bool show_credits;
+
+	bool is_new_lap_record;
+	bool is_new_race_record;
+	float best_lap;
+	float race_time;
+	int lives;
+	int race_position;
+	
+	float lap_times[NUM_PILOTS][NUM_LAPS];
+	pilot_points_t race_ranks[NUM_PILOTS];
+	pilot_points_t championship_ranks[NUM_PILOTS];
+
+	camera_t camera;
+	droid_t droid;
+	ship_t ships[NUM_PILOTS];
+	track_t track;
+} game_t;
+
+
+
+// Save Data
+
+typedef struct {
+	char name[4];
+	float time;
+} highscores_entry_t;
+
+typedef struct {
+	highscores_entry_t entries[NUM_HIGHSCORES];
+	float lap_record;
+} highscores_t;
+
+typedef struct {
+	uint32_t magic;
+	bool is_dirty;
+
+	float sfx_volume;
+	float music_volume;
+	uint8_t ui_scale;
+	bool show_fps;
+	bool fullscreen;
+
+	uint32_t has_rapier_class;
+	uint32_t has_bonus_circuts;
+	char highscores_name[4];
+	highscores_t highscores[NUM_RACE_CLASSES][NUM_CIRCUTS][NUM_HIGHSCORE_TABS];
+} save_t;
+
+
+
+
+extern const game_def_t def;
+extern game_t g;
+extern save_t save;
+
+void game_init();
+void game_set_scene(game_scene_t scene);
+void game_reset_championship();
+void game_update();
+
+#endif
--- /dev/null
+++ b/src/wipeout/hud.c
@@ -1,0 +1,244 @@
+#include "../types.h"
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "hud.h"
+#include "droid.h"
+#include "camera.h"
+#include "image.h"
+#include "ship_ai.h"
+#include "game.h"
+#include "ui.h"
+
+static texture_list_t weapon_icon_textures;
+static uint16_t target_reticle;
+
+typedef struct {
+	vec2i_t offset;
+	uint16_t height;
+	rgba_t color;
+} speedo_bar_t;
+
+const struct {
+	uint16_t width;
+	uint16_t skew;
+	speedo_bar_t bars[13];
+} speedo = {
+	.width = 121,
+	.skew = 2,
+	.bars = {
+		{{.x =   6, .y = 12}, .height = 10, .color = rgba( 66,  16,  49, 255)},
+		{{.x =  13, .y = 12}, .height = 10, .color = rgba(115,  33,  90, 255)},
+		{{.x =  20, .y = 12}, .height = 10, .color = rgba(132,  58, 164, 255)},
+		{{.x =  27, .y = 12}, .height = 10, .color = rgba( 99,  90, 197, 255)},
+		{{.x =  34, .y = 12}, .height = 10, .color = rgba( 74, 148, 181, 255)},
+		{{.x =  41, .y = 12}, .height = 10, .color = rgba( 66, 173, 115, 255)},
+		{{.x =  50, .y = 10}, .height = 12, .color = rgba( 99, 206,  58, 255)},
+		{{.x =  59, .y =  8}, .height = 12, .color = rgba(189, 206,  41, 255)},
+		{{.x =  69, .y =  5}, .height = 13, .color = rgba(247, 140,  33, 255)},
+		{{.x =  81, .y =  2}, .height = 15, .color = rgba(255, 197,  49, 255)},
+		{{.x =  95, .y =  1}, .height = 16, .color = rgba(255, 222, 115, 255)},
+		{{.x = 110, .y =  1}, .height = 16, .color = rgba(255, 239, 181, 255)},
+		{{.x = 126, .y =  1}, .height = 16, .color = rgba(255, 255, 255, 255)}
+	}
+};
+
+static uint16_t speedo_facia_texture;
+
+void hud_load() {
+	speedo_facia_texture = image_get_texture("wipeout/textures/speedo.tim");
+	target_reticle = image_get_texture_semi_trans("wipeout/textures/target2.tim");
+	weapon_icon_textures = image_get_compressed_textures("wipeout/common/wicons.cmp");
+}
+
+static void hud_draw_speedo_bar(vec2i_t *pos, const speedo_bar_t *a, const speedo_bar_t *b, float f, rgba_t color_override) {
+	rgba_t left_color, right_color;
+	if (color_override.as_uint32 > 0) {
+		left_color = color_override;
+		right_color = color_override;
+	}
+	else {
+		left_color = a->color;
+		right_color = rgba(
+			lerp(a->color.as_rgba.r, b->color.as_rgba.r, f),
+			lerp(a->color.as_rgba.g, b->color.as_rgba.g, f),
+			lerp(a->color.as_rgba.b, b->color.as_rgba.b, f),
+			lerp(a->color.as_rgba.a, b->color.as_rgba.a, f)
+		);
+	}
+
+	float right_h = lerp(a->height, b->height, f);
+	vec2i_t top_left     = vec2i(a->offset.x + 1, a->offset.y);
+	vec2i_t bottom_left  = vec2i(a->offset.x + 1 - a->height / speedo.skew, a->offset.y + a->height);
+	vec2i_t top_right    = vec2i(lerp(a->offset.x + 1, b->offset.x, f), lerp(a->offset.y, b->offset.y, f));
+	vec2i_t bottom_right = vec2i(top_right.x - right_h / speedo.skew, top_right.y + right_h);
+
+	top_left     = ui_scaled(top_left);
+	bottom_left  = ui_scaled(bottom_left);
+	top_right    = ui_scaled(top_right);
+	bottom_right = ui_scaled(bottom_right);
+
+	render_push_tris((tris_t) {
+		.vertices = {
+			{
+				.pos = {pos->x + bottom_left.x, pos->y + bottom_left.y, 0},
+				.uv = {0, 0},
+				.color = left_color
+			},
+			{
+				.pos = {pos->x + top_right.x, pos->y + top_right.y, 0},
+				.uv = {0, 0},
+				.color = right_color
+			},
+			{
+				.pos = {pos->x + top_left.x, pos->y + top_left.y, 0},
+				.uv = {0, 0},
+				.color = left_color
+			},
+		}
+	}, RENDER_NO_TEXTURE);
+
+	render_push_tris((tris_t) {
+		.vertices = {
+			{
+				.pos = {pos->x + bottom_right.x, pos->y + bottom_right.y, 0},
+				.uv = {0, 0},
+				.color = right_color
+			},
+			{
+				.pos = {pos->x + top_right.x, pos->y + top_right.y, 0},
+				.uv = {0, 0},
+				.color = right_color
+			},
+			{
+				.pos = {pos->x + bottom_left.x, pos->y + bottom_left.y, 0},
+				.uv = {0, 0},
+				.color = left_color
+			},
+		}
+	}, RENDER_NO_TEXTURE);
+}
+
+static void hud_draw_speedo_bars(vec2i_t *pos, float f, rgba_t color_override) {
+	if (f <= 0) {
+		return;
+	}
+
+	if (f - floor(f) > 0.9) {
+		f = ceil(f);
+	}
+	if (f > 13) {
+		f = 13;
+	}
+
+	int bars = f;
+	for (int i = 1; i < bars; i++) {
+		hud_draw_speedo_bar(pos, &speedo.bars[i - 1], &speedo.bars[i], 1, color_override);
+	}
+
+	if (bars > 12) {
+		return;
+	}
+
+	float last_bar_fraction = f - bars + 0.1;
+	if (last_bar_fraction <= 0) {
+		return;
+	}
+
+	if (last_bar_fraction > 1) {
+		last_bar_fraction = 1;
+	}
+	int last_bar = bars == 0 ? 1 : bars;
+	hud_draw_speedo_bar(pos, &speedo.bars[last_bar - 1], &speedo.bars[last_bar], last_bar_fraction, color_override);
+}
+
+static void hud_draw_speedo(int speed, int thrust) {
+	vec2i_t facia_pos = ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-141, -45));
+	vec2i_t bar_pos = ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-141, -40));
+	hud_draw_speedo_bars(&bar_pos, thrust / 65.0, rgba(255, 0, 0, 128));
+	hud_draw_speedo_bars(&bar_pos, speed / 2166.0, rgba(0, 0, 0, 0));
+	render_push_2d(facia_pos, ui_scaled(render_texture_size(speedo_facia_texture)), rgba(128, 128, 128, 255), speedo_facia_texture);
+}
+
+static void hud_draw_target_icon(vec3_t position) {
+	vec2i_t screen_size = render_size();
+	vec2i_t size = ui_scaled(render_texture_size(target_reticle));
+	vec3_t projected = render_transform(position);
+
+	vec2i_t pos = vec2i(
+		(( projected.x + 1.0) / 2.0) * screen_size.x - size.x / 2,
+		((-projected.y + 1.0) / 2.0) * screen_size.y - size.y / 2
+	);
+	render_push_2d(pos, size, rgba(128, 128, 128, 128), target_reticle);
+}
+
+void hud_draw(ship_t *ship) {
+	// Current lap time
+	if (ship->lap >= 0) {
+		ui_draw_time(ship->lap_time, ui_scaled_pos(UI_POS_BOTTOM | UI_POS_LEFT, vec2i(16, -30)), UI_SIZE_16, UI_COLOR_DEFAULT);
+	
+		for (int i = 0; i < ship->lap && i < NUM_LAPS-1; i++) {
+			ui_draw_time(g.lap_times[ship->pilot][i], ui_scaled_pos(UI_POS_BOTTOM | UI_POS_LEFT, vec2i(16, -45 - (10 * i))), UI_SIZE_8, UI_COLOR_ACCENT);
+		}
+	}
+
+	// Current Lap
+	int display_lap = max(0, ship->lap + 1);
+	ui_draw_text("LAP", ui_scaled(vec2i(15, 8)), UI_SIZE_8, UI_COLOR_ACCENT); 
+	ui_draw_number(display_lap, ui_scaled(vec2i(10, 19)), UI_SIZE_16, UI_COLOR_DEFAULT); 
+	int width = ui_char_width('0' + display_lap, UI_SIZE_16);
+	ui_draw_text("OF", ui_scaled(vec2i((10 + width), 27)), UI_SIZE_8, UI_COLOR_ACCENT);
+	ui_draw_number(NUM_LAPS, ui_scaled(vec2i((32 + width), 19)), UI_SIZE_16, UI_COLOR_DEFAULT);
+
+	// Race Position
+	if (g.race_type != RACE_TYPE_TIME_TRIAL) {
+		ui_draw_text("POSITION", ui_scaled_pos(UI_POS_TOP | UI_POS_RIGHT, vec2i(-90, 8)), UI_SIZE_8, UI_COLOR_ACCENT);
+		ui_draw_number(ship->position_rank, ui_scaled_pos(UI_POS_TOP | UI_POS_RIGHT, vec2i(-60, 19)), UI_SIZE_16, UI_COLOR_DEFAULT);
+	}
+
+	// Framerate
+	if (save.show_fps) {
+		ui_draw_text("FPS", ui_scaled(vec2i(16, 78)), UI_SIZE_8, UI_COLOR_ACCENT);
+		ui_draw_number((int)(g.frame_rate), ui_scaled(vec2i(16, 90)), UI_SIZE_8, UI_COLOR_DEFAULT);
+	}
+
+	// Lap Record
+	ui_draw_text("LAP RECORD", ui_scaled(vec2i(15, 43)), UI_SIZE_8, UI_COLOR_ACCENT);
+	ui_draw_time(save.highscores[g.race_class][g.circut][g.highscore_tab].lap_record, ui_scaled(vec2i(15, 55)), UI_SIZE_8, UI_COLOR_DEFAULT);
+
+	// Wrong way
+	if (flags_not(ship->flags, SHIP_DIRECTION_FORWARD)) {
+		ui_draw_text_centered("WRONG WAY", ui_scaled_pos(UI_POS_MIDDLE | UI_POS_CENTER, vec2i(-20, 0)), UI_SIZE_16, UI_COLOR_ACCENT);
+	}
+
+	// Speedo
+	int speedo_speed = (g.camera.update_func == camera_update_attract_internal)
+		? ship->speed * 7
+		: ship->speed;
+	hud_draw_speedo(speedo_speed, ship->thrust_mag);
+
+	// Weapon icon
+	if (ship->weapon_type != WEAPON_TYPE_NONE) {
+		vec2i_t pos = ui_scaled_pos(UI_POS_TOP | UI_POS_CENTER, vec2i(-16, 20));
+		vec2i_t size = ui_scaled(vec2i(32, 32));
+		uint16_t icon = texture_from_list(weapon_icon_textures, ship->weapon_type-1);
+		render_push_2d(pos, size, rgba(128,128,128,255), icon);
+	}
+
+	// Lives
+	if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		for (int i = 0; i < g.lives; i++) {
+			ui_draw_icon(UI_ICON_STAR, ui_scaled_pos(UI_POS_BOTTOM | UI_POS_RIGHT, vec2i(-26 - 13 * i, -50)), UI_COLOR_DEFAULT);
+		}
+	}
+
+	// Weapon target reticle
+	if (ship->weapon_target) {
+		hud_draw_target_icon(ship->weapon_target->position);
+	}
+}
--- /dev/null
+++ b/src/wipeout/hud.h
@@ -1,0 +1,9 @@
+#ifndef HUD_H
+#define HUD_H
+
+#include "ship.h"
+
+void hud_load();
+void hud_draw(ship_t *ship);
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/image.c
@@ -1,0 +1,320 @@
+#include "../types.h"
+#include "../mem.h"
+#include "../utils.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "droid.h"
+#include "camera.h"
+#include "object.h"
+#include "scene.h"
+#include "game.h"
+#include "hud.h"
+#include "image.h"
+
+
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include "../libs/stb_image_write.h"
+
+
+#define TIM_TYPE_PALETTED_4_BPP 0x08
+#define TIM_TYPE_PALETTED_8_BPP 0x09
+#define TIM_TYPE_TRUE_COLOR_16_BPP 0x02
+
+static inline rgba_t tim_16bit_to_rgba(uint16_t c, bool transparent_bit) {
+	return rgba(
+		((c >>  0) & 0x1f) << 3,
+		((c >>  5) & 0x1f) << 3,
+		((c >> 10) & 0x1f) << 3,
+		(c == 0 
+			? 0x00
+			: transparent_bit && (c & 0x7fff) == 0 ? 0x00 : 0xff
+		)
+	);
+}
+
+image_t *image_alloc(uint32_t width, uint32_t height) {
+	image_t *image = mem_temp_alloc(sizeof(image_t) + width * height * sizeof(rgba_t));
+	image->width = width;
+	image->height = height;
+	image->pixels = (rgba_t *)(((uint8_t *)image) + sizeof(image_t));
+	return image;
+}
+
+image_t *image_load_from_bytes(uint8_t *bytes, bool transparent) {
+	uint32_t p = 0;
+
+	uint32_t magic = get_i32_le(bytes, &p);
+	uint32_t type = get_i32_le(bytes, &p);
+	uint16_t *palette = NULL;
+
+	if (
+	    type == TIM_TYPE_PALETTED_4_BPP ||
+	    type == TIM_TYPE_PALETTED_8_BPP
+	) {
+		uint32_t header_length = get_i32_le(bytes, &p);
+		uint16_t palette_x = get_i16_le(bytes, &p);
+		uint16_t palette_y = get_i16_le(bytes, &p);
+		uint16_t palette_colors = get_i16_le(bytes, &p);
+		uint16_t palettes = get_i16_le(bytes, &p);
+		palette = (uint16_t *)(bytes + p);
+		p += palette_colors * 2;
+	}
+
+	uint32_t data_size = get_i32_le(bytes, &p);
+
+	int32_t pixels_per_16bit = 1;
+	if (type == TIM_TYPE_PALETTED_8_BPP) {
+		pixels_per_16bit = 2;
+	}
+	else if (type == TIM_TYPE_PALETTED_4_BPP) {
+		pixels_per_16bit = 4;
+	}
+
+	uint16_t skip_x = get_i16_le(bytes, &p);
+	uint16_t skip_y = get_i16_le(bytes, &p);
+	uint16_t entries_per_row  = get_i16_le(bytes, &p);
+	uint16_t rows = get_i16_le(bytes, &p);
+
+	int32_t width = entries_per_row * pixels_per_16bit;
+	int32_t height = rows;
+	int32_t entries = entries_per_row * rows;
+
+	image_t *image = image_alloc(width, height);
+	int32_t pixel_pos = 0;
+
+	if (type == TIM_TYPE_TRUE_COLOR_16_BPP) {
+		for (int i = 0; i < entries; i++) {
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(get_i16_le(bytes, &p), transparent);
+		}
+	}
+	else if (type == TIM_TYPE_PALETTED_8_BPP) {
+		for (int i = 0; i < entries; i++) {
+			int32_t palette_pos = get_i16_le(bytes, &p);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 0) & 0xff], transparent);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 8) & 0xff], transparent);
+		}
+	}
+	else if (type == TIM_TYPE_PALETTED_4_BPP) {
+		for (int i = 0; i < entries; i++) {
+			int32_t palette_pos = get_i16_le(bytes, &p);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >>  0) & 0xf], transparent);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >>  4) & 0xf], transparent);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >>  8) & 0xf], transparent);
+			image->pixels[pixel_pos++] = tim_16bit_to_rgba(palette[(palette_pos >> 12) & 0xf], transparent);
+		}
+	}
+
+	return image;
+}
+
+#define LZSS_INDEX_BIT_COUNT  13
+#define LZSS_LENGTH_BIT_COUNT 4
+#define LZSS_WINDOW_SIZE      (1 << LZSS_INDEX_BIT_COUNT)
+#define LZSS_BREAK_EVEN       ((1 + LZSS_INDEX_BIT_COUNT + LZSS_LENGTH_BIT_COUNT) / 9)
+#define LZSS_END_OF_STREAM    0
+#define LZSS_MOD_WINDOW(a)    ((a) & (LZSS_WINDOW_SIZE - 1))
+
+void lzss_decompress(uint8_t *in_data, uint8_t *out_data) {
+	int16_t i;
+	int16_t current_position;
+	uint8_t cc;
+	int16_t match_length;
+	int16_t match_position;
+	uint32_t mask;
+	uint32_t return_value;
+	uint8_t in_bfile_mask;
+	int16_t in_bfile_rack;
+	int16_t value;
+	uint8_t window[LZSS_WINDOW_SIZE];
+
+	in_bfile_rack = 0;
+	in_bfile_mask = 0x80;
+
+	current_position = 1;
+	while (true) {
+		if (in_bfile_mask == 0x80) {
+			in_bfile_rack = (int16_t) * in_data++;
+		}
+
+		value = in_bfile_rack & in_bfile_mask;
+		in_bfile_mask >>= 1;
+		if (in_bfile_mask == 0) {
+			in_bfile_mask = 0x80;
+		}
+
+		if (value) {
+			mask = 1L << (8 - 1);
+			return_value = 0;
+			while (mask != 0) {
+				if (in_bfile_mask == 0x80) {
+					in_bfile_rack = (int16_t) * in_data++;
+				}
+
+				if (in_bfile_rack & in_bfile_mask) {
+					return_value |= mask;
+				}
+				mask >>= 1;
+				in_bfile_mask >>= 1;
+
+				if (in_bfile_mask == 0) {
+					in_bfile_mask = 0x80;
+				}
+			}
+			cc = (uint8_t) return_value;
+			*out_data++ = cc;
+			window[ current_position ] = cc;
+			current_position = LZSS_MOD_WINDOW(current_position + 1);
+		}
+		else {
+			mask = 1L << (LZSS_INDEX_BIT_COUNT - 1);
+			return_value = 0;
+			while (mask != 0) {
+				if (in_bfile_mask == 0x80) {
+					in_bfile_rack = (int16_t) * in_data++;
+				}
+
+				if (in_bfile_rack & in_bfile_mask) {
+					return_value |= mask;
+				}
+				mask >>= 1;
+				in_bfile_mask >>= 1;
+
+				if (in_bfile_mask == 0) {
+					in_bfile_mask = 0x80;
+				}
+			}
+			match_position = (int16_t) return_value;
+
+			if (match_position == LZSS_END_OF_STREAM) {
+				break;
+			}
+
+			mask = 1L << (LZSS_LENGTH_BIT_COUNT - 1);
+			return_value = 0;
+			while (mask != 0) {
+				if (in_bfile_mask == 0x80) {
+					in_bfile_rack = (int16_t) * in_data++;
+				}
+
+				if (in_bfile_rack & in_bfile_mask) {
+					return_value |= mask;
+				}
+				mask >>= 1;
+				in_bfile_mask >>= 1;
+
+				if (in_bfile_mask == 0) {
+					in_bfile_mask = 0x80;
+				}
+			}
+			match_length = (int16_t) return_value;
+
+			match_length += LZSS_BREAK_EVEN;
+
+			for (i = 0 ; i <= match_length ; i++) {
+				cc = window[LZSS_MOD_WINDOW(match_position + i)];
+				*out_data++ = cc;
+				window[current_position] = cc;
+				current_position = LZSS_MOD_WINDOW(current_position + 1);
+			}
+		}
+	}
+}
+
+cmp_t *image_load_compressed(char *name) {
+	printf("load cmp %s\n", name);
+	uint32_t compressed_size;
+	uint8_t *compressed_bytes = file_load(name, &compressed_size);
+
+	uint32_t p = 0;
+	int32_t decompressed_size = 0;
+	int32_t image_count = get_i32_le(compressed_bytes, &p);
+
+	// Calculate the total uncompressed size
+	for (int i = 0; i < image_count; i++) {
+		decompressed_size += get_i32_le(compressed_bytes, &p);
+	}
+
+	uint32_t struct_size = sizeof(cmp_t) + sizeof(uint8_t *) * image_count;
+	cmp_t *cmp = mem_temp_alloc(struct_size + decompressed_size);
+	cmp->len = image_count;
+
+	uint8_t *decompressed_bytes = ((uint8_t *)cmp) + struct_size;
+
+	// Rewind and load all offsets
+	p = 4;
+	uint32_t offset = 0;
+	for (int i = 0; i < image_count; i++) {
+		cmp->entries[i] = decompressed_bytes + offset;
+		offset += get_i32_le(compressed_bytes, &p);
+	}
+
+	lzss_decompress(compressed_bytes + p, decompressed_bytes);
+	mem_temp_free(compressed_bytes);
+
+	return cmp;
+}
+
+uint16_t image_get_texture(char *name) {
+	printf("load: %s\n", name);
+	uint32_t size;
+	uint8_t *bytes = file_load(name, &size);
+	image_t *image = image_load_from_bytes(bytes, false);
+	uint32_t texture_index = render_texture_create(image->width, image->height, image->pixels);
+	mem_temp_free(image);
+	mem_temp_free(bytes);
+
+	return texture_index;
+}
+
+uint16_t image_get_texture_semi_trans(char *name) {
+	printf("load: %s\n", name);
+	uint32_t size;
+	uint8_t *bytes = file_load(name, &size);
+	image_t *image = image_load_from_bytes(bytes, true);
+	uint32_t texture_index = render_texture_create(image->width, image->height, image->pixels);
+	mem_temp_free(image);
+	mem_temp_free(bytes);
+
+	return texture_index;
+}
+
+texture_list_t image_get_compressed_textures(char *name) {
+	cmp_t *cmp = image_load_compressed(name);
+	texture_list_t list = {.start = render_textures_len(), .len = cmp->len};
+
+	for (int i = 0; i < cmp->len; i++) {
+		int32_t width, height;
+		image_t *image = image_load_from_bytes(cmp->entries[i], false);
+
+		// char png_name[1024] = {0};
+		// sprintf(png_name, "%s.%d.png", name, i);
+		// stbi_write_png(png_name, image->width, image->height, 4, image->pixels, 0);
+
+		render_texture_create(image->width, image->height, image->pixels);
+		mem_temp_free(image);
+	}
+
+	mem_temp_free(cmp);
+	return list;
+}
+
+uint16_t texture_from_list(texture_list_t tl, uint16_t index) {
+	error_if(index >= tl.len, "Texture %d not in list of len %d", index, tl.len);
+	return tl.start + index;
+}
+
+void image_copy(image_t *src, image_t *dst, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh, uint32_t dx, uint32_t dy) {
+	rgba_t *src_pixels = src->pixels + sy * src->width + sx;
+	rgba_t *dst_pixels = dst->pixels + dy * dst->width + dx;
+	for (uint32_t y = 0; y < sh; y++) {
+		for (uint32_t x = 0; x < sw; x++) {
+			*(dst_pixels++) = *(src_pixels++);
+		}
+		src_pixels += src->width - sw;
+		dst_pixels += dst->width - sw;
+	}
+}
+
--- /dev/null
+++ b/src/wipeout/image.h
@@ -1,0 +1,34 @@
+#ifndef INIT_H
+#define INIT_H
+
+#include "../types.h"
+
+typedef struct {
+	uint16_t start;
+	uint16_t len;
+} texture_list_t;
+
+#define texture_list_empty() ((texture_list_t){0, 0})
+
+typedef struct {
+	uint32_t width;
+	uint32_t height;
+	rgba_t *pixels;
+} image_t;
+
+typedef struct {
+	uint32_t len;
+	uint8_t *entries[];
+} cmp_t;
+
+image_t *image_alloc(uint32_t width, uint32_t height);
+void image_copy(image_t *src, image_t *dst, uint32_t sx, uint32_t sy, uint32_t sw, uint32_t sh, uint32_t dx, uint32_t dy);
+image_t *image_load_from_bytes(uint8_t *bytes, bool transparent);
+cmp_t *image_load_compressed(char *name);
+
+uint16_t image_get_texture(char *name);
+uint16_t image_get_texture_semi_trans(char *name);
+texture_list_t image_get_compressed_textures(char *name);
+uint16_t texture_from_list(texture_list_t tl, uint16_t index);
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/ingame_menus.c
@@ -1,0 +1,486 @@
+#include <string.h>
+
+#include "../input.h"
+#include "../system.h"
+#include "../utils.h"
+#include "../mem.h"
+
+#include "menu.h"
+#include "ingame_menus.h"
+#include "game.h"
+#include "image.h"
+#include "ui.h"
+#include "race.h"
+
+static void page_race_points_init(menu_t * menu);
+static void page_championship_points_init(menu_t * menu);
+static void page_hall_of_fame_init(menu_t * menu);
+
+static texture_list_t pilot_portraits;
+static menu_t *ingame_menu;
+
+void ingame_menus_load() {
+	pilot_portraits = image_get_compressed_textures(def.pilots[g.pilot].portrait);
+	ingame_menu = mem_bump(sizeof(menu_t));
+}
+
+// -----------------------------------------------------------------------------
+// Pause Menu
+
+static void button_continue(menu_t *menu, int data) {
+	race_unpause();
+}
+
+static void button_restart_confirm(menu_t *menu, int data) {
+	if (data) {
+		race_restart();
+	}
+	else {
+		menu_pop(menu);
+	}
+}
+
+static void button_restart_or_quit(menu_t *menu, int data) {
+	if (data) {
+		race_restart();
+	}
+	else {
+		game_set_scene(GAME_SCENE_MAIN_MENU);
+	}
+}
+
+static void button_restart(menu_t *menu, int data) {
+	menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO RESTART", "YES", "NO", button_restart_confirm);
+}
+
+static void button_quit_confirm(menu_t *menu, int data) {
+	if (data) {
+		game_set_scene(GAME_SCENE_MAIN_MENU);
+	}
+	else {
+		menu_pop(menu);
+	}
+}
+
+static void button_quit(menu_t *menu, int data) {
+	menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO QUIT", "YES", "NO", button_quit_confirm);
+}
+
+
+static void button_music_track(menu_t *menu, int data) {
+	sfx_music_play(data);
+	sfx_music_mode(SFX_MUSIC_LOOP);
+}
+
+static void button_music_random(menu_t *menu, int data) {
+	sfx_music_play(rand_int(0, len(def.music)));
+	sfx_music_mode(SFX_MUSIC_RANDOM);
+}
+
+static void button_music(menu_t *menu, int data) {
+	menu_page_t *page = menu_push(menu, "MUSIC", NULL);
+
+	for (int i = 0; i < len(def.music); i++) {
+		menu_page_add_button(page, i, def.music[i].name, button_music_track);
+	}
+	menu_page_add_button(page, 0, "RANDOM", button_music_random);
+}
+
+menu_t *pause_menu_init() {
+	sfx_play(SFX_MENU_SELECT);
+	menu_reset(ingame_menu);
+
+	menu_page_t *page = menu_push(ingame_menu, "PAUSED", NULL);
+	menu_page_add_button(page, 0, "CONTINUE", button_continue);
+	menu_page_add_button(page, 0, "RESTART", button_restart);
+	menu_page_add_button(page, 0, "QUIT", button_quit);
+	menu_page_add_button(page, 0, "MUSIC", button_music);
+	return ingame_menu;
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Game Over
+
+menu_t *game_over_menu_init() {
+	sfx_play(SFX_MENU_SELECT);
+	menu_reset(ingame_menu);
+
+	menu_page_t *page = menu_push(ingame_menu, "GAME OVER", NULL);
+	menu_page_add_button(page, 1, "", button_quit_confirm);
+	return ingame_menu;
+}
+
+
+// -----------------------------------------------------------------------------
+// Race Stats
+
+static void button_qualify_confirm(menu_t *menu, int data) {
+	if (data) {
+		race_restart();
+	}
+	else {
+		game_set_scene(GAME_SCENE_MAIN_MENU);
+	}
+}
+
+static void button_race_stats_continue(menu_t *menu, int data) {
+	if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		if (g.race_position <= QUALIFYING_RANK) {
+			page_race_points_init(menu);
+		}
+		else {
+			menu_page_t *page = menu_confirm(menu, "CONTINUE QUALIFYING OR QUIT", "", "QUALIFY", "QUIT", button_qualify_confirm);
+			page->index = 0;
+		}
+	}
+	else {
+		if (g.is_new_race_record) {
+			page_hall_of_fame_init(menu);
+		}
+		else {
+			menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
+		}
+	}
+}
+
+static void page_race_stats_draw(menu_t *menu, int data) {
+	menu_page_t *page = &menu->pages[menu->index];
+	vec2i_t pos = page->title_pos;
+	pos.x -= 140;
+	pos.y += 32;
+	ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	// Pilot portrait and race position - only for championship or single race
+	if (g.race_type != RACE_TYPE_TIME_TRIAL) {
+		vec2i_t image_pos = ui_scaled_pos(anchor, vec2i(pos.x + 180, pos.y));
+		uint16_t image = texture_from_list(pilot_portraits, g.race_position <= QUALIFYING_RANK ? 1 : 0);
+		render_push_2d(image_pos, ui_scaled(render_texture_size(image)), rgba(0, 0, 0, 128), RENDER_NO_TEXTURE);
+		ui_draw_image(image_pos, image);
+
+		ui_draw_text("RACE POSITION", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+		ui_draw_number(g.race_position, ui_scaled_pos(anchor, vec2i(pos.x + ui_text_width("RACE POSITION", UI_SIZE_8)+8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
+	}
+
+	pos.y += 32;
+
+	ui_draw_text("RACE STATISTICS", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+	pos.y += 16;
+
+	for (int i = 0; i < NUM_LAPS; i++) {
+		ui_draw_text("LAP", ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
+		ui_draw_number(i+1, ui_scaled_pos(anchor, vec2i(pos.x + 50, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
+		ui_draw_time(g.lap_times[g.pilot][i], ui_scaled_pos(anchor, vec2i(pos.x + 72, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
+		pos.y+= 12;
+	}
+	pos.y += 32;
+
+	ui_draw_text("RACE TIME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+	pos.y += 12;
+	ui_draw_time(g.race_time, ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
+	pos.y += 12;
+
+	ui_draw_text("BEST LAP", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+	pos.y += 12;
+	ui_draw_time(g.best_lap, ui_scaled_pos(anchor, vec2i(pos.x + 8, pos.y)), UI_SIZE_8, UI_COLOR_DEFAULT);
+	pos.y += 12;
+}
+
+menu_t *race_stats_menu_init() {
+	sfx_play(SFX_MENU_SELECT);
+	menu_reset(ingame_menu);
+	
+	char *title;
+	if (g.race_type == RACE_TYPE_TIME_TRIAL) {
+		title = "";
+	}
+	else if (g.race_position <= QUALIFYING_RANK) {
+		title = "CONGRATULATIONS";
+	}
+	else {
+		title = "FAILED TO QUALIFY";
+	}
+	menu_page_t *page = menu_push(ingame_menu, title, page_race_stats_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->title_pos = vec2i(0, -100);
+	menu_page_add_button(page, 1, "", button_race_stats_continue);
+	return ingame_menu;
+}
+
+
+// -----------------------------------------------------------------------------
+// Race Table
+
+static void button_race_points_continue(menu_t *menu, int data) {
+	if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		page_championship_points_init(menu);
+	}
+	else if (g.is_new_race_record) {
+		page_hall_of_fame_init(menu);
+	}
+	else {
+		menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
+	}
+}
+
+static void page_race_points_draw(menu_t *menu, int data) {
+	menu_page_t *page = &menu->pages[menu->index];
+	vec2i_t pos = page->title_pos;
+	pos.x -= 140;
+	pos.y += 32;
+	ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	ui_draw_text("PILOT NAME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+	ui_draw_text("POINTS", ui_scaled_pos(anchor, vec2i(pos.x + 222, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
+
+	pos.y += 24;
+
+	for (int i = 0; i < len(g.race_ranks); i++) {
+		rgba_t color = g.race_ranks[i].pilot == g.pilot ? UI_COLOR_ACCENT : UI_COLOR_DEFAULT;
+		ui_draw_text(def.pilots[g.race_ranks[i].pilot].name, ui_scaled_pos(anchor, pos), UI_SIZE_8, color);
+		int w = ui_number_width(g.race_ranks[i].points, UI_SIZE_8);
+		ui_draw_number(g.race_ranks[i].points, ui_scaled_pos(anchor, vec2i(pos.x + 280 - w, pos.y)), UI_SIZE_8, color);
+		pos.y += 12;
+	}
+}
+
+static void page_race_points_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "RACE POINTS", page_race_points_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->title_pos = vec2i(0, -100);
+	menu_page_add_button(page, 1, "", button_race_points_continue);
+}
+
+
+// -----------------------------------------------------------------------------
+// Championship Table
+
+static void button_championship_points_continue(menu_t *menu, int data) {
+	if (g.is_new_race_record) {
+		page_hall_of_fame_init(menu);
+	}
+	else if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		race_next();
+	}
+	else {
+		menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_quit_confirm);
+	}
+}
+
+static void page_championship_points_draw(menu_t *menu, int data) {
+	menu_page_t *page = &menu->pages[menu->index];
+	vec2i_t pos = page->title_pos;
+	pos.x -= 140;
+	pos.y += 32;
+	ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	ui_draw_text("PILOT NAME", ui_scaled_pos(anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+	ui_draw_text("POINTS", ui_scaled_pos(anchor, vec2i(pos.x + 222, pos.y)), UI_SIZE_8, UI_COLOR_ACCENT);
+
+	pos.y += 24;
+
+	for (int i = 0; i < len(g.championship_ranks); i++) {
+		rgba_t color = g.championship_ranks[i].pilot == g.pilot ? UI_COLOR_ACCENT : UI_COLOR_DEFAULT;
+		ui_draw_text(def.pilots[g.championship_ranks[i].pilot].name, ui_scaled_pos(anchor, pos), UI_SIZE_8, color);
+		int w = ui_number_width(g.championship_ranks[i].points, UI_SIZE_8);
+		ui_draw_number(g.championship_ranks[i].points, ui_scaled_pos(anchor, vec2i(pos.x + 280 - w, pos.y)), UI_SIZE_8, color);
+		pos.y += 12;
+	}
+}
+
+static void page_championship_points_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "CHAMPIONSHIP TABLE", page_championship_points_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->title_pos = vec2i(0, -100);
+	menu_page_add_button(page, 1, "", button_championship_points_continue);
+}
+
+
+// -----------------------------------------------------------------------------
+// Hall of Fame
+
+static highscores_entry_t hs_new_entry = {
+	.time = 0,
+	.name = ""
+};
+static const char *hs_charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+static int hs_char_index = 0;
+static bool hs_entry_complete = false;
+
+static void hall_of_fame_draw_name_entry(menu_t *menu, ui_pos_t anchor, vec2i_t pos) {
+	int entry_len = strlen(hs_new_entry.name);
+	int entry_width = ui_text_width(hs_new_entry.name, UI_SIZE_16);
+
+	vec2i_t c_pos = ui_scaled_pos(anchor, vec2i(pos.x + entry_width, pos.y));
+	int c_first = 0;
+	int c_last = 38;
+	if (entry_len == 0) {
+		c_last = 37;
+	}
+	else if (entry_len == 3) {
+		c_first = 36;
+	}
+
+	if (input_pressed(A_MENU_UP)) {
+		hs_char_index++;
+	}
+	else if (input_pressed(A_MENU_DOWN)) {
+		hs_char_index--;
+	}
+
+	if (hs_char_index < c_first) {
+		hs_char_index = c_last-1;
+	}
+	if (hs_char_index >= c_last) {
+		hs_char_index = c_first;
+	}
+
+	// DEL
+	if (hs_char_index == 36) {
+		ui_draw_icon(UI_ICON_DEL, c_pos, UI_COLOR_ACCENT);
+		if (input_pressed(A_MENU_SELECT)) {
+			sfx_play(SFX_MENU_SELECT);
+			if (entry_len > 0) {
+				hs_new_entry.name[entry_len-1] = '\0';
+			}
+		}
+	}
+
+	// END
+	else if (hs_char_index == 37) {
+		ui_draw_icon(UI_ICON_END, c_pos, UI_COLOR_ACCENT);
+		if (input_pressed(A_MENU_SELECT)) {
+			hs_entry_complete = true;
+		}
+	}
+
+	// A-Z, 0-9
+	else {
+		char selector[2] = {hs_charset[hs_char_index], '\0'};
+		ui_draw_text(selector, c_pos, UI_SIZE_16, UI_COLOR_ACCENT);
+
+		if (input_pressed(A_MENU_SELECT)) {
+			sfx_play(SFX_MENU_SELECT);
+			hs_new_entry.name[entry_len] = hs_charset[hs_char_index];
+			hs_new_entry.name[entry_len+1] = '\0';
+		}
+	}
+
+	ui_draw_text(hs_new_entry.name, ui_scaled_pos(anchor, pos), UI_SIZE_16, UI_COLOR_ACCENT);
+}
+
+static void page_hall_of_fame_draw(menu_t *menu, int data) {
+	// FIXME: doing this all in the draw() function leads to all kinds of
+	// complications
+
+	highscores_t *hs = &save.highscores[g.race_class][g.circut][g.highscore_tab];
+	
+	if (hs_entry_complete) {
+		sfx_play(SFX_MENU_SELECT);
+		strncpy(save.highscores_name, hs_new_entry.name, 4);
+		save.is_dirty = true;
+		
+		// Insert new highscore entry into the save struct
+		highscores_entry_t temp_entry = hs->entries[0];
+		for (int i = 0; i < NUM_HIGHSCORES; i++) {
+			if (hs_new_entry.time < hs->entries[i].time) {
+				for (int j = NUM_HIGHSCORES - 2; j >= i; j--) {
+					hs->entries[j+1] = hs->entries[j];
+				}
+				hs->entries[i] = hs_new_entry;
+				break;
+			}
+		}
+		save.is_dirty = true;
+
+		if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+			race_next();
+		}
+		else {
+			menu_reset(menu); // Can't go back!
+			menu_confirm(menu, "", "RESTART RACE", "RESTART", "QUIT", button_restart_or_quit);
+		}
+		return;
+	}
+
+	menu_page_t *page = &menu->pages[menu->index];
+	vec2i_t pos = page->title_pos;
+	pos.x -= 120;
+	pos.y += 48;
+	ui_pos_t anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	bool has_shown_new_entry = false;
+	for (int i = 0, j = 0; i < NUM_HIGHSCORES; i++, j++) {
+		if (!has_shown_new_entry && hs_new_entry.time < hs->entries[i].time) {
+			hall_of_fame_draw_name_entry(menu, anchor, pos);
+			ui_draw_time(hs_new_entry.time, ui_scaled_pos(anchor, vec2i(pos.x + 120, pos.y)), UI_SIZE_16, UI_COLOR_DEFAULT);
+			has_shown_new_entry = true;
+			j--;
+		}
+		else {
+			ui_draw_text(hs->entries[j].name, ui_scaled_pos(anchor, pos), UI_SIZE_16, UI_COLOR_DEFAULT);
+			ui_draw_time(hs->entries[j].time, ui_scaled_pos(anchor, vec2i(pos.x + 120, pos.y)), UI_SIZE_16, UI_COLOR_DEFAULT);
+		}
+		pos.y += 24;
+	}
+}
+
+static void page_hall_of_fame_init(menu_t *menu) {
+	menu_reset(menu); // Can't go back!
+	menu_page_t *page = menu_push(menu, "HALL OF FAME", page_hall_of_fame_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->title_pos = vec2i(0, -100);
+
+	hs_new_entry.time = g.race_time;
+	strncpy(hs_new_entry.name, save.highscores_name, 4);
+	hs_char_index = 0;
+	hs_entry_complete = false;
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Text scroller
+
+static char * const *text_scroll_lines;
+static int text_scroll_lines_len;
+static double text_scroll_start_time;
+
+static void text_scroll_menu_draw(menu_t *menu, int data) {
+	double time = system_time() - text_scroll_start_time;
+	int scale = ui_get_scale();
+	int speed = 32;
+	vec2i_t screen = render_size();
+	vec2i_t pos = vec2i(screen.x / 2, screen.y - time * scale * speed);
+
+	for (int i = 0; i < text_scroll_lines_len; i++) {
+		const char *line = text_scroll_lines[i];
+
+		if (line[0] == '#') {
+			pos.y += 48 * scale;
+			ui_draw_text_centered(line + 1, pos, UI_SIZE_16, UI_COLOR_ACCENT);
+			pos.y += 32 * scale;
+		}
+		else {
+			ui_draw_text_centered(line, pos, UI_SIZE_8, UI_COLOR_DEFAULT);	
+			pos.y += 12 * scale;
+		}
+	}
+}
+
+menu_t *text_scroll_menu_init(char * const *lines, int len) {
+	text_scroll_lines = lines;
+	text_scroll_lines_len = len;
+	text_scroll_start_time = system_time();
+
+	menu_reset(ingame_menu);
+
+	menu_page_t *page = menu_push(ingame_menu, "", text_scroll_menu_draw);
+	menu_page_add_button(page, 1, "", button_quit_confirm);
+	return ingame_menu;
+}
--- /dev/null
+++ b/src/wipeout/ingame_menus.h
@@ -1,0 +1,13 @@
+#ifndef PAUSE_MENU_H
+#define PAUSE_MENU_H
+
+#include "menu.h"
+
+void ingame_menus_load();
+
+menu_t *pause_menu_init();
+menu_t *game_over_menu_init();
+menu_t *race_stats_menu_init();
+menu_t *text_scroll_menu_init(char * const *lines, int len);
+
+#endif
--- /dev/null
+++ b/src/wipeout/intro.c
@@ -1,0 +1,105 @@
+#include "../system.h"
+#include "../input.h"
+#include "../utils.h"
+#include "../types.h"
+#include "../mem.h"
+
+#include "intro.h"
+#include "ui.h"
+#include "image.h"
+#include "game.h"
+
+void free_dummmy(void *p) {}
+void *realloc_dummmy(void *p, size_t sz) {
+	die("pl_mpeg needed to realloc. Not implemented. Maybe increase PLM_BUFFER_DEFAULT_SIZE");
+	return NULL;
+}
+
+#define PL_MPEG_IMPLEMENTATION
+#define PLM_MALLOC mem_bump
+#define PLM_FREE free_dummmy
+#define PLM_REALLOC realloc_dummmy
+#include "../libs/pl_mpeg.h"
+
+#define INTRO_AUDIO_BUFFER_LEN (64 * 1024)
+
+static plm_t *plm;
+static rgba_t *frame_buffer;
+static int16_t texture;
+static float *audio_buffer;
+static int audio_buffer_read_pos;
+static int audio_buffer_write_pos;
+
+static void video_cb(plm_t *plm, plm_frame_t *frame, void *user);
+static void audio_cb(plm_t *plm, plm_samples_t *samples, void *user);
+static void audio_mix(float *samples, uint32_t len);
+static void intro_end();
+
+void intro_init() {
+	plm = plm_create_with_filename("wipeout/intro.mpeg");
+	if (!plm) {
+		intro_end();
+		return;
+	}
+	plm_set_video_decode_callback(plm, video_cb, NULL);
+	plm_set_audio_decode_callback(plm, audio_cb, NULL);
+	
+	plm_set_loop(plm, false);
+	plm_set_audio_enabled(plm, true);
+	plm_set_audio_stream(plm, 0);
+
+	int w = plm_get_width(plm);
+	int h = plm_get_height(plm);
+	frame_buffer = mem_bump(w * h * sizeof(rgba_t));
+	for (int i = 0; i < w * h; i++) {
+		frame_buffer[i] = rgba(0, 0, 0, 255);
+	}
+	texture = render_texture_create(w, h, frame_buffer);
+
+	sfx_set_external_mix_cb(audio_mix);
+	audio_buffer = mem_bump(INTRO_AUDIO_BUFFER_LEN * sizeof(float) * 2);
+	audio_buffer_read_pos = 0;
+	audio_buffer_write_pos = 0;
+}
+
+static void intro_end() {
+	sfx_set_external_mix_cb(NULL);
+	game_set_scene(GAME_SCENE_TITLE);
+}
+
+void intro_update() {
+	if (!plm) {
+		return;
+	}
+	plm_decode(plm, system_tick());
+	render_set_view_2d();
+	render_push_2d(vec2i(0,0), render_size(), rgba(128, 128, 128, 255), texture);
+	if (plm_has_ended(plm) || input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
+		intro_end();
+	}
+}
+
+static void audio_cb(plm_t *plm, plm_samples_t *samples, void *user) {
+	int len = samples->count * 2;
+	for (int i = 0; i < len; i++) {
+		audio_buffer[audio_buffer_write_pos % INTRO_AUDIO_BUFFER_LEN] = samples->interleaved[i];
+		audio_buffer_write_pos++;
+	}
+}
+
+static void audio_mix(float *samples, uint32_t len) {
+	int i;
+	for (i = 0; i < len && audio_buffer_read_pos < audio_buffer_write_pos; i++) {
+		samples[i] = audio_buffer[audio_buffer_read_pos % INTRO_AUDIO_BUFFER_LEN];
+		audio_buffer_read_pos++;
+	}
+	for (; i < len; i++) {
+		samples[i] = 0;
+	}
+}
+
+static void video_cb(plm_t *plm, plm_frame_t *frame, void *user) {
+	plm_frame_to_rgba(frame, (uint8_t *)frame_buffer, plm_get_width(plm) * sizeof(rgba_t));
+	render_texture_replace_pixels(texture, frame_buffer);
+}
+
--- /dev/null
+++ b/src/wipeout/intro.h
@@ -1,0 +1,8 @@
+#ifndef INTRO_H
+#define INTRO_H
+
+void intro_init();
+void intro_update();
+void intro_cleanup();
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/main_menu.c
@@ -1,0 +1,410 @@
+#include "../utils.h"
+#include "../system.h"
+#include "../mem.h"
+#include "../platform.h"
+
+#include "menu.h"
+#include "main_menu.h"
+#include "game.h"
+#include "image.h"
+#include "ui.h"
+
+static void page_main_init(menu_t *menu);
+static void page_options_init(menu_t *menu);
+static void page_race_class_init(menu_t *menu);
+static void page_race_type_init(menu_t *menu);
+static void page_team_init(menu_t *menu);
+static void page_pilot_init(menu_t *menu);
+static void page_circut_init(menu_t *menu);
+static void page_options_controls_init(menu_t *menu);
+static void page_options_video_init(menu_t *menu);
+static void page_options_audio_init(menu_t *menu);
+
+static uint16_t background;
+static texture_list_t track_images;
+static menu_t *main_menu;
+
+static struct {
+	Object *race_classes[2];
+	Object *teams[4];
+	Object *pilots[8];
+	struct { Object *stopwatch, *save, *load, *headphones, *cd; } options;
+	struct { Object *championship, *msdos, *single_race, *options; } misc;
+	Object *rescue;
+	Object *controller;
+} models;
+
+static void draw_model(Object *model, vec2_t offset, vec3_t pos, float rotation) {
+	render_set_view(vec3(0,0,0), vec3(0, -M_PI, -M_PI));
+	render_set_screen_position(offset);
+	mat4_t mat = mat4_identity();
+	mat4_set_translation(&mat, pos);
+	mat4_set_yaw_pitch_roll(&mat, vec3(0, rotation, M_PI));
+	object_draw(model, &mat);
+	render_set_screen_position(vec2(0, 0));
+}
+
+// -----------------------------------------------------------------------------
+// Main Menu
+
+static void button_start_game(menu_t *menu, int data) {
+	page_race_class_init(menu);
+}
+
+static void button_options(menu_t *menu, int data) {
+	page_options_init(menu);
+}
+
+static void button_quit_confirm(menu_t *menu, int data) {
+	if (data) {
+		system_exit();
+	}
+	else {
+		menu_pop(menu);
+	}
+}
+
+static void button_quit(menu_t *menu, int data) {
+	menu_confirm(menu, "ARE YOU SURE YOU", "WANT TO QUIT", "YES", "NO", button_quit_confirm);
+}
+
+static void page_main_draw(menu_t *menu, int data) {
+	switch (data) {
+		case 0: draw_model(g.ships[0].model, vec2(0, -0.1), vec3(0, 0, -700), system_cycle_time()); break;
+		case 1: draw_model(models.misc.options, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break;
+		case 2: draw_model(models.misc.msdos, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break;
+	}
+}
+
+static void page_main_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "OPTIONS", page_main_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+
+	menu_page_add_button(page, 0, "START GAME", button_start_game);
+	menu_page_add_button(page, 1, "OPTIONS", button_options);
+
+	#ifndef __EMSCRIPTEN__
+		menu_page_add_button(page, 2, "QUIT", button_quit);
+	#endif
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Options
+
+static void button_controls(menu_t *menu, int data) {
+	page_options_controls_init(menu);
+}
+
+static void button_video(menu_t *menu, int data) {
+	page_options_video_init(menu);
+}
+
+static void button_audio(menu_t *menu, int data) {
+	page_options_audio_init(menu);
+}
+
+static void page_options_draw(menu_t *menu, int data) {
+	switch (data) {
+		case 0: draw_model(models.controller, vec2(0, -0.1), vec3(0, 0, -6000), system_cycle_time()); break;
+		case 1: draw_model(models.rescue, vec2(0, -0.2), vec3(0, 0, -700), system_cycle_time()); break; // TODO: needs better model
+		case 2: draw_model(models.options.headphones, vec2(0, -0.2), vec3(0, 0, -300), system_cycle_time()); break;
+	}
+}
+
+static void page_options_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "OPTIONS", page_options_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	menu_page_add_button(page, 0, "CONTROLS", button_controls);
+	menu_page_add_button(page, 1, "VIDEO", button_video);
+	menu_page_add_button(page, 2, "AUDIO", button_audio);
+}
+
+
+// -----------------------------------------------------------------------------
+// Options Controls
+
+static void page_options_controls_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "TODO", page_options_draw);
+}
+
+// -----------------------------------------------------------------------------
+// Options Video
+
+static void toggle_fullscreen(menu_t *menu, int data) {
+	save.fullscreen = data;
+	save.is_dirty = true;
+	platform_set_fullscreen(save.fullscreen);
+}
+
+static void toggle_show_fps(menu_t *menu, int data) {
+	save.show_fps = data;
+	save.is_dirty = true;
+}
+
+static void toggle_ui_scale(menu_t *menu, int data) {
+	save.ui_scale = data;
+	save.is_dirty = true;
+}
+
+static const char *opts_off_on[] = {"OFF", "ON"};
+static const char *opts_ui_sizes[] = {"AUTO", "1X", "2X", "3X", "4X"};
+
+static void page_options_video_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "VIDEO OPTIONS", NULL);
+	flags_set(page->layout_flags, MENU_VERTICAL | MENU_FIXED);
+	page->title_pos = vec2i(-160, -100);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->items_pos = vec2i(-160, -80);
+	page->block_width = 320;
+	page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	#ifndef __EMSCRIPTEN__
+		menu_page_add_toggle(page, save.fullscreen, "FULLSCREEN", opts_off_on, len(opts_off_on), toggle_fullscreen);
+	#endif
+	menu_page_add_toggle(page, save.ui_scale, "UI SCALE", opts_ui_sizes, len(opts_ui_sizes), toggle_ui_scale);
+	menu_page_add_toggle(page, save.show_fps, "SHOW FPS", opts_off_on, len(opts_off_on), toggle_show_fps);
+}
+
+// -----------------------------------------------------------------------------
+// Options Audio
+
+static void toggle_music_volume(menu_t *menu, int data) {
+	save.music_volume = (float)data * 0.1;
+	save.is_dirty = true;
+}
+
+static void toggle_sfx_volume(menu_t *menu, int data) {
+	save.sfx_volume = (float)data * 0.1;	
+	save.is_dirty = true;
+}
+
+static const char *opts_volume[] = {"0", "10", "20", "30", "40", "50", "60", "70", "80", "90", "100"};
+
+static void page_options_audio_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "AUDIO OPTIONS", NULL);
+
+	flags_set(page->layout_flags, MENU_VERTICAL | MENU_FIXED);
+	page->title_pos = vec2i(-160, -100);
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->items_pos = vec2i(-160, -80);
+	page->block_width = 320;
+	page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+
+	menu_page_add_toggle(page, save.music_volume * 10, "MUSIC VOLUME", opts_volume, len(opts_volume), toggle_music_volume);
+	menu_page_add_toggle(page, save.sfx_volume * 10, "SOUND EFFECTS VOLUME", opts_volume, len(opts_volume), toggle_sfx_volume);
+}
+
+
+
+
+
+
+
+
+// -----------------------------------------------------------------------------
+// Racing class
+
+static void button_race_class_select(menu_t *menu, int data) {
+	if (!save.has_rapier_class && data == RACE_CLASS_RAPIER) {
+		return;
+	}
+	g.race_class = data;
+	page_race_type_init(menu);
+}
+
+static void page_race_class_draw(menu_t *menu, int data) {
+	menu_page_t *page = &menu->pages[menu->index];
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	draw_model(models.race_classes[data], vec2(0, -0.2), vec3(0, 0, -350), system_cycle_time());
+
+	if (!save.has_rapier_class && data == RACE_CLASS_RAPIER) {
+		render_set_view_2d();
+		vec2i_t pos = vec2i(page->items_pos.x, page->items_pos.y + 32);
+		ui_draw_text_centered("NOT AVAILABLE", ui_scaled_pos(page->items_anchor, pos), UI_SIZE_12, UI_COLOR_ACCENT);
+	}
+}
+
+static void page_race_class_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "SELECT RACING CLASS", page_race_class_draw);
+	for (int i = 0; i < len(def.race_classes); i++) {
+		menu_page_add_button(page, i, def.race_classes[i].name, button_race_class_select);
+	}
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Race Type
+
+static void button_race_type_select(menu_t *menu, int data) {
+	g.race_type = data;
+	g.highscore_tab = g.race_type == RACE_TYPE_TIME_TRIAL ? HIGHSCORE_TAB_TIME_TRIAL : HIGHSCORE_TAB_RACE;
+	page_team_init(menu);
+}
+
+static void page_race_type_draw(menu_t *menu, int data) {
+	switch (data) {
+		case 0: draw_model(models.misc.championship, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
+		case 1: draw_model(models.misc.single_race, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
+		case 2: draw_model(models.options.stopwatch, vec2(0, -0.2), vec3(0, 0, -400), system_cycle_time()); break;
+	}
+}
+
+static void page_race_type_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "SELECT RACE TYPE", page_race_type_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	for (int i = 0; i < len(def.race_types); i++) {
+		menu_page_add_button(page, i, def.race_types[i].name, button_race_type_select);
+	}
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Team
+
+static void button_team_select(menu_t *menu, int data) {
+	g.team = data;
+	page_pilot_init(menu);
+}
+
+static void page_team_draw(menu_t *menu, int data) {
+	draw_model(models.teams[data], vec2(0, -0.2), vec3(0, 0, -10000), system_cycle_time());
+	draw_model(g.ships[def.teams[data].pilots[0]].model, vec2(0, -0.3), vec3(-700, -800, -1300), system_cycle_time()*1.1);
+	draw_model(g.ships[def.teams[data].pilots[1]].model, vec2(0, -0.3), vec3( 700, -800, -1300), system_cycle_time()*1.2);
+}
+
+static void page_team_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "SELECT YOUR TEAM", page_team_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	for (int i = 0; i < len(def.teams); i++) {
+		menu_page_add_button(page, i, def.teams[i].name, button_team_select);
+	}
+}
+
+
+
+// -----------------------------------------------------------------------------
+// Pilot
+
+static void button_pilot_select(menu_t *menu, int data) {
+	g.pilot = data;
+	if (g.race_type != RACE_TYPE_CHAMPIONSHIP) {
+		page_circut_init(menu);
+	}
+	else {
+		g.circut = 0;
+		game_reset_championship();
+		game_set_scene(GAME_SCENE_RACE);
+	}
+}
+
+static void page_pilot_draw(menu_t *menu, int data) {
+	draw_model(models.pilots[data], vec2(0, -0.2), vec3(0, 0, -10000), system_cycle_time());
+}
+
+static void page_pilot_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "CHOOSE YOUR PILOT", page_pilot_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -110);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	for (int i = 0; i < len(def.teams[g.team].pilots); i++) {
+		menu_page_add_button(page, def.teams[g.team].pilots[i], def.pilots[def.teams[g.team].pilots[i]].name, button_pilot_select);
+	}
+}
+
+
+// -----------------------------------------------------------------------------
+// Circut
+
+static void button_circut_select(menu_t *menu, int data) {
+	g.circut = data;
+	game_set_scene(GAME_SCENE_RACE);
+}
+
+static void page_circut_draw(menu_t *menu, int data) {
+	vec2i_t pos = vec2i(0, -25);
+	vec2i_t size = vec2i(128, 74);
+	vec2i_t scaled_size = ui_scaled(size);
+	vec2i_t scaled_pos = ui_scaled_pos(UI_POS_MIDDLE | UI_POS_CENTER, vec2i(pos.x - size.x/2, pos.y - size.y/2));
+	render_push_2d(scaled_pos, scaled_size, rgba(128, 128, 128, 255), texture_from_list(track_images, data));
+}
+
+static void page_circut_init(menu_t *menu) {
+	menu_page_t *page = menu_push(menu, "SELECT RACING CIRCUT", page_circut_draw);
+	flags_add(page->layout_flags, MENU_FIXED);
+	page->title_pos = vec2i(0, 30);
+	page->title_anchor = UI_POS_TOP | UI_POS_CENTER;
+	page->items_pos = vec2i(0, -100);
+	page->items_anchor = UI_POS_BOTTOM | UI_POS_CENTER;
+	for (int i = 0; i < len(def.circuts); i++) {
+		if (!def.circuts[i].is_bonus_circut || save.has_bonus_circuts) {
+			menu_page_add_button(page, i, def.circuts[i].name, button_circut_select);
+		}
+	}
+}
+
+#define objects_unpack(DEST, SRC) \
+	objects_unpack_imp((Object **)&DEST, sizeof(DEST)/sizeof(Object*), SRC)
+
+static void objects_unpack_imp(Object **dest_array, int len, Object *src) {
+	int i;
+	for (i = 0; src && i < len; i++) {
+		dest_array[i] = src;
+		src = src->next;
+	}
+	error_if(i != len, "expected %d models got %d", len, i)
+}
+
+
+void main_menu_init() {
+	g.is_attract_mode = false;
+
+	main_menu = mem_bump(sizeof(menu_t));
+
+	background = image_get_texture("wipeout/textures/wipeout1.tim");
+	track_images = image_get_compressed_textures("wipeout/textures/track.cmp");
+
+	objects_unpack(models.race_classes, objects_load("wipeout/common/leeg.prm", image_get_compressed_textures("wipeout/common/leeg.cmp")));
+	objects_unpack(models.teams, objects_load("wipeout/common/teams.prm", texture_list_empty()));
+	objects_unpack(models.pilots, objects_load("wipeout/common/pilot.prm", image_get_compressed_textures("wipeout/common/pilot.cmp")));
+	objects_unpack(models.options, objects_load("wipeout/common/alopt.prm", image_get_compressed_textures("wipeout/common/alopt.cmp")));
+	objects_unpack(models.rescue, objects_load("wipeout/common/rescu.prm", image_get_compressed_textures("wipeout/common/rescu.cmp")));
+	objects_unpack(models.controller, objects_load("wipeout/common/pad1.prm", image_get_compressed_textures("wipeout/common/pad1.cmp")));
+	objects_unpack(models.misc, objects_load("wipeout/common/msdos.prm", image_get_compressed_textures("wipeout/common/msdos.cmp")));
+
+	menu_reset(main_menu);
+	page_main_init(main_menu);
+}
+
+void main_menu_update() {
+	render_set_view_2d();
+	render_push_2d(vec2i(0, 0), render_size(), rgba(128, 128, 128, 255), background);
+
+	menu_update(main_menu);
+}
+
--- /dev/null
+++ b/src/wipeout/main_menu.h
@@ -1,0 +1,7 @@
+#ifndef MAIN_MENU_H
+#define MAIN_MENU_H
+
+void main_menu_init();
+void main_menu_update();
+
+#endif
--- /dev/null
+++ b/src/wipeout/menu.c
@@ -1,0 +1,240 @@
+#include "../system.h"
+#include "../input.h"
+#include "../utils.h"
+
+#include "game.h"
+#include "menu.h"
+#include "ui.h"
+#include "sfx.h"
+
+void menu_reset(menu_t *menu) {
+	menu->index = -1;
+}
+
+menu_page_t *menu_push(menu_t *menu, char *title, void(*draw_func)(menu_t *, int)) {
+	error_if(menu->index >= MENU_PAGES_MAX-1, "MENU_PAGES_MAX exceeded");
+	menu_page_t *page = &menu->pages[++menu->index];
+	page->layout_flags = MENU_VERTICAL | MENU_ALIGN_CENTER;
+	page->block_width = 320;
+	page->title = title;
+	page->subtitle = NULL;
+	page->draw_func = draw_func;
+	page->entries_len = 0;
+	page->index = 0;
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	return page;
+}
+
+menu_page_t *menu_confirm(menu_t *menu, char *title, char *subtitle, char *yes, char *no, void(*confirm_func)(menu_t *, int)) {
+	error_if(menu->index >= MENU_PAGES_MAX-1, "MENU_PAGES_MAX exceeded");
+	menu_page_t *page = &menu->pages[++menu->index];
+	page->layout_flags = MENU_HORIZONTAL;
+	page->title = title;
+	page->subtitle = subtitle;
+	page->draw_func = NULL;
+	page->entries_len = 0;
+	menu_page_add_button(page, 1, yes, confirm_func);
+	menu_page_add_button(page, 0, no, confirm_func);
+	page->index = 1;
+	page->title_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	page->items_anchor = UI_POS_MIDDLE | UI_POS_CENTER;
+	return page;
+}
+
+void menu_pop(menu_t *menu) {
+	if (menu->index == 0) {
+		return;
+	}
+	menu->index--;
+}
+
+void menu_page_add_button(menu_page_t *page, int data, char *text, void(*select_func)(menu_t *, int)) {
+	error_if(page->entries_len >= MENU_ENTRIES_MAX-1, "MENU_ENTRIES_MAX exceeded");
+	menu_entry_t *entry = &page->entries[page->entries_len++];
+	entry->data = data;
+	entry->text = text;
+	entry->select_func = select_func;
+	entry->type = MENU_ENTRY_BUTTON;
+}
+
+void menu_page_add_toggle(menu_page_t *page, int data, char *text, const char **options, int len, void(*select_func)(menu_t *, int)) {
+	error_if(page->entries_len >= MENU_ENTRIES_MAX-1, "MENU_ENTRIES_MAX exceeded");
+	menu_entry_t *entry = &page->entries[page->entries_len++];
+	entry->data = data;
+	entry->text = text;
+	entry->select_func = select_func;
+	entry->type = MENU_ENTRY_TOGGLE;
+	entry->options = options;
+	entry->options_len = len;
+}
+
+
+void menu_update(menu_t *menu) {
+	render_set_view_2d();
+	
+	error_if(menu->index < 0, "Attempt to update menu without a page");
+	menu_page_t *page = &menu->pages[menu->index];
+
+	// Handle menu entry selecting
+	int last_index = page->index;
+	int selected_data = 0;
+	if (page->entries_len > 0) {
+		if (flags_is(page->layout_flags, MENU_HORIZONTAL)) {
+			if (input_pressed(A_MENU_LEFT)) {
+				page->index--;
+			}
+			else if (input_pressed(A_MENU_RIGHT)) {
+				page->index++;
+			}
+		}
+		else {
+			if (input_pressed(A_MENU_UP)) {
+				page->index--;
+			}
+			if (input_pressed(A_MENU_DOWN)) {
+				page->index++;
+			}
+		}
+
+		if (page->index >= page->entries_len) {
+			page->index = 0;
+		}
+		if (page->index < 0) {
+			page->index = page->entries_len - 1;
+		}
+
+		if (last_index != page->index) {
+			sfx_play(SFX_MENU_MOVE);
+		}
+		selected_data = page->entries[page->index].data;
+	}
+
+	if (page->draw_func) {
+		page->draw_func(menu, selected_data);
+	}
+
+	render_set_view_2d();
+
+	// Draw Horizontal (confirm)
+	if (flags_is(page->layout_flags, MENU_HORIZONTAL)) {
+		vec2i_t pos = vec2i(0, -20);
+		ui_draw_text_centered(page->title, ui_scaled_pos(page->title_anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+		if (page->subtitle) {
+			pos.y += 12;
+			ui_draw_text_centered(page->subtitle, ui_scaled_pos(page->title_anchor, pos), UI_SIZE_8, UI_COLOR_ACCENT);
+		}
+		pos.y += 16;
+
+		page = &menu->pages[menu->index];
+		pos.x = -50;
+		for (int i = 0; i < page->entries_len; i++) {
+			menu_entry_t *entry = &page->entries[i];
+			rgba_t text_color;
+			if (i == page->index) {
+				text_color = UI_COLOR_ACCENT;
+			}
+			else {
+				text_color = UI_COLOR_DEFAULT;
+			}
+			ui_draw_text_centered(entry->text, ui_scaled_pos(page->items_anchor, pos), UI_SIZE_16, text_color);
+			pos.x = 60;
+		}
+	}
+
+	// Draw Vertical
+	else {
+		vec2i_t title_pos, items_pos;
+		if (flags_not(page->layout_flags, MENU_FIXED)) {
+			int height = 20 + page->entries_len * 12;
+			title_pos = vec2i(0, -height/2);
+			items_pos = vec2i(0, -height/2 + 20);
+		}
+		else {
+			title_pos = page->title_pos;
+			items_pos = page->items_pos;
+		}
+		if (flags_is(page->layout_flags, MENU_ALIGN_CENTER)) {
+			ui_draw_text_centered(page->title, ui_scaled_pos(page->title_anchor, title_pos), UI_SIZE_12, UI_COLOR_ACCENT);
+		}
+		else {
+			ui_draw_text(page->title, ui_scaled_pos(page->title_anchor, title_pos), UI_SIZE_12, UI_COLOR_ACCENT);	
+		}
+
+		page = &menu->pages[menu->index];
+		for (int i = 0; i < page->entries_len; i++) {
+			menu_entry_t *entry = &page->entries[i];
+			rgba_t text_color;
+			if (i == page->index) {
+				text_color = UI_COLOR_ACCENT;
+			}
+			else {
+				text_color = UI_COLOR_DEFAULT;
+			}
+
+			if (flags_is(page->layout_flags, MENU_ALIGN_CENTER)) {
+				ui_draw_text_centered(entry->text, ui_scaled_pos(page->items_anchor, items_pos), UI_SIZE_8, text_color);
+			}
+			else {
+				ui_draw_text(entry->text, ui_scaled_pos(page->items_anchor, items_pos), UI_SIZE_8, text_color);
+			}
+
+			if (entry->type == MENU_ENTRY_TOGGLE) {
+				vec2i_t toggle_pos = items_pos;
+				toggle_pos.x += page->block_width - ui_text_width(entry->options[entry->data], UI_SIZE_8);
+				ui_draw_text(entry->options[entry->data], ui_scaled_pos(page->items_anchor, toggle_pos), UI_SIZE_8, text_color);	
+			}
+			items_pos.y += 12;
+		}
+	}
+
+	// Handle back buttons
+	if (input_pressed(A_MENU_BACK)) {
+		if (menu->index != 0) {
+			menu_pop(menu);
+			sfx_play(SFX_MENU_SELECT);
+		}
+		return;
+	}
+
+	if (page->entries_len == 0) {
+		return;
+	}
+
+
+	// Handle toggle entries
+	menu_entry_t *entry = &page->entries[page->index];
+
+	if (entry->type == MENU_ENTRY_TOGGLE) {
+		if (input_pressed(A_MENU_LEFT)) {
+			sfx_play(SFX_MENU_SELECT);
+			entry->data--;
+			if (entry->data < 0) {
+				entry->data = entry->options_len-1;
+			}
+			if (entry->select_func) {
+				entry->select_func(menu, entry->data);
+			}
+		}
+		else if (input_pressed(A_MENU_RIGHT) || input_pressed(A_MENU_SELECT)) {
+			sfx_play(SFX_MENU_SELECT);
+			entry->data = (entry->data + 1) % entry->options_len;
+			if (entry->select_func) {
+				entry->select_func(menu, entry->data);
+			}
+		}
+	}
+
+	// Handle buttons
+	else {
+		if (input_pressed(A_MENU_SELECT)) {
+			if (entry->select_func) {
+				sfx_play(SFX_MENU_SELECT);
+				if (entry->type == MENU_ENTRY_TOGGLE) {
+					entry->data = (entry->data + 1) % entry->options_len;
+				}
+				entry->select_func(menu, entry->data);
+			}
+		}
+	}
+}
--- /dev/null
+++ b/src/wipeout/menu.h
@@ -1,0 +1,65 @@
+#ifndef MENU_H
+#define MENU_H
+
+#include "../types.h"
+#include "ui.h"
+
+#define MENU_PAGES_MAX 8
+#define MENU_ENTRIES_MAX 16
+
+typedef enum {
+	MENU_ENTRY_BUTTON,
+	MENU_ENTRY_TOGGLE
+} menu_entry_type_t;
+
+typedef enum {
+	MENU_VERTICAL     = (1<<0),
+	MENU_HORIZONTAL   = (1<<1),
+	MENU_FIXED        = (1<<2),
+	MENU_ALIGN_CENTER = (1<<3),
+	MENU_ALIGN_BLOCK  = (1<<4)
+} menu_page_layout_t;
+
+typedef struct menu_t menu_t;
+typedef struct menu_page_t menu_page_t;
+typedef struct menu_entry_t menu_entry_t;
+typedef struct menu_entry_options_t menu_entry_options_t;
+
+struct menu_entry_t {
+	menu_entry_type_t type;
+	int data;
+	char *text;
+	void (*select_func)(menu_t *, int);
+	const char **options;
+	int options_len;
+};
+
+struct menu_page_t {
+	char *title, *subtitle;
+	menu_page_layout_t layout_flags;
+	void (*draw_func)(menu_t *, int);
+	menu_entry_t entries[MENU_ENTRIES_MAX];
+	int entries_len;
+	int index;
+	int block_width;
+	vec2i_t title_pos;
+	ui_pos_t title_anchor;
+	vec2i_t items_pos;
+	ui_pos_t items_anchor;
+};
+
+struct menu_t {
+	menu_page_t pages[MENU_PAGES_MAX];
+	int index;
+};
+
+
+void menu_reset(menu_t *menu);
+menu_page_t *menu_push(menu_t *menu, char *title, void(*draw_func)(menu_t *, int));
+menu_page_t *menu_confirm(menu_t *menu, char *title, char *subtitle, char *yes, char *no, void(*confirm_func)(menu_t *, int));
+void menu_pop(menu_t *menu);
+void menu_page_add_button(menu_page_t *page, int data, char *text, void(*select_func)(menu_t *, int));
+void menu_page_add_toggle(menu_page_t *page, int data, char *text, const char **options, int len, void(*select_func)(menu_t *, int));
+void menu_update(menu_t *menu);
+
+#endif
--- /dev/null
+++ b/src/wipeout/object.c
@@ -1,0 +1,778 @@
+#include "../types.h"
+#include "../mem.h"
+#include "../render.h"
+#include "../utils.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "droid.h"
+#include "camera.h"
+#include "object.h"
+#include "scene.h"
+#include "hud.h"
+#include "object.h"
+
+
+static rgba_t int32_to_rgba(uint32_t v) {
+	return rgba(
+		((v >> 24) & 0xff),
+		((v >> 16) & 0xff),
+		((v >> 8) & 0xff),
+		255
+	);
+}
+
+Object *objects_load(char *name, texture_list_t tl) {
+	uint32_t length = 0;
+	uint8_t *bytes = file_load(name, &length);
+	if (!bytes) {
+		die("Failed to load file %s\n", name);
+	}
+	printf("load: %s\n", name);
+
+	Object *objectList = mem_mark();
+	Object *prevObject = NULL;
+	uint32_t p = 0;
+
+	while (p < length) {
+		Object *object = mem_bump(sizeof(Object));
+		if (prevObject) {
+			prevObject->next = object;
+		}
+		prevObject = object;
+
+		for (int i = 0; i < 16; i++) {
+			object->name[i] = get_i8(bytes, &p);
+		}
+		
+		object->mat = mat4_identity();
+		object->vertices_len = get_i16(bytes, &p); p += 2;
+		object->vertices = NULL; get_i32(bytes, &p);
+		object->normals_len = get_i16(bytes, &p); p += 2;
+		object->normals = NULL; get_i32(bytes, &p);
+		object->primitives_len = get_i16(bytes, &p); p += 2;
+		object->primitives = NULL; get_i32(bytes, &p);
+		get_i32(bytes, &p);
+		get_i32(bytes, &p);
+		get_i32(bytes, &p); // Skeleton ref
+		object->extent = get_i32(bytes, &p);
+		object->flags = get_i16(bytes, &p); p += 2;
+		object->next = NULL; get_i32(bytes, &p);
+
+		p += 3 * 3 * 2; // relative rot matrix
+		p += 2; // padding
+
+		object->origin.x = get_i32(bytes, &p);
+		object->origin.y = get_i32(bytes, &p);
+		object->origin.z = get_i32(bytes, &p);
+
+		p += 3 * 3 * 2; // absolute rot matrix
+		p += 2; // padding
+		p += 3 * 4; // absolute translation matrix
+		p += 2; // skeleton update flag
+		p += 2; // padding
+		p += 4; // skeleton super
+		p += 4; // skeleton sub
+		p += 4; // skeleton next
+
+		object->vertices = mem_bump(object->vertices_len * sizeof(vec3_t));
+		for (int i = 0; i < object->vertices_len; i++) {
+			object->vertices[i].x = get_i16(bytes, &p);
+			object->vertices[i].y = get_i16(bytes, &p);
+			object->vertices[i].z = get_i16(bytes, &p);
+			p += 2; // padding
+		}
+
+		object->normals = mem_bump(object->normals_len * sizeof(vec3_t));
+		for (int i = 0; i < object->normals_len; i++) {
+			object->normals[i].x = get_i16(bytes, &p);
+			object->normals[i].y = get_i16(bytes, &p);
+			object->normals[i].z = get_i16(bytes, &p);
+			p += 2; // padding
+		}
+
+		object->primitives = mem_mark();
+		for (int i = 0; i < object->primitives_len; i++) {
+			Prm prm;
+			int16_t prm_type = get_i16(bytes, &p);
+			int16_t prm_flag = get_i16(bytes, &p);
+
+			switch (prm_type) {
+			case PRM_TYPE_F3:
+				prm.ptr = mem_bump(sizeof(F3));
+				prm.f3->coords[0] = get_i16(bytes, &p);
+				prm.f3->coords[1] = get_i16(bytes, &p);
+				prm.f3->coords[2] = get_i16(bytes, &p);
+				prm.f3->pad1 = get_i16(bytes, &p);
+				prm.f3->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_F4:
+				prm.ptr = mem_bump(sizeof(F4));
+				prm.f4->coords[0] = get_i16(bytes, &p);
+				prm.f4->coords[1] = get_i16(bytes, &p);
+				prm.f4->coords[2] = get_i16(bytes, &p);
+				prm.f4->coords[3] = get_i16(bytes, &p);
+				prm.f4->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_FT3:
+				prm.ptr = mem_bump(sizeof(FT3));
+				prm.ft3->coords[0] = get_i16(bytes, &p);
+				prm.ft3->coords[1] = get_i16(bytes, &p);
+				prm.ft3->coords[2] = get_i16(bytes, &p);
+
+				prm.ft3->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.ft3->cba = get_i16(bytes, &p);
+				prm.ft3->tsb = get_i16(bytes, &p);
+				prm.ft3->u0 = get_i8(bytes, &p);
+				prm.ft3->v0 = get_i8(bytes, &p);
+				prm.ft3->u1 = get_i8(bytes, &p);
+				prm.ft3->v1 = get_i8(bytes, &p);
+				prm.ft3->u2 = get_i8(bytes, &p);
+				prm.ft3->v2 = get_i8(bytes, &p);
+
+				prm.ft3->pad1 = get_i16(bytes, &p);
+				prm.ft3->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_FT4:
+				prm.ptr = mem_bump(sizeof(FT4));
+				prm.ft4->coords[0] = get_i16(bytes, &p);
+				prm.ft4->coords[1] = get_i16(bytes, &p);
+				prm.ft4->coords[2] = get_i16(bytes, &p);
+				prm.ft4->coords[3] = get_i16(bytes, &p);
+
+				prm.ft4->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.ft4->cba = get_i16(bytes, &p);
+				prm.ft4->tsb = get_i16(bytes, &p);
+				prm.ft4->u0 = get_i8(bytes, &p);
+				prm.ft4->v0 = get_i8(bytes, &p);
+				prm.ft4->u1 = get_i8(bytes, &p);
+				prm.ft4->v1 = get_i8(bytes, &p);
+				prm.ft4->u2 = get_i8(bytes, &p);
+				prm.ft4->v2 = get_i8(bytes, &p);
+				prm.ft4->u3 = get_i8(bytes, &p);
+				prm.ft4->v3 = get_i8(bytes, &p);
+				prm.ft4->pad1 = get_i16(bytes, &p);
+				prm.ft4->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_G3:
+				prm.ptr = mem_bump(sizeof(G3));
+				prm.g3->coords[0] = get_i16(bytes, &p);
+				prm.g3->coords[1] = get_i16(bytes, &p);
+				prm.g3->coords[2] = get_i16(bytes, &p);
+				prm.g3->pad1 = get_i16(bytes, &p);
+				prm.g3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.g3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.g3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_G4:
+				prm.ptr = mem_bump(sizeof(G4));
+				prm.g4->coords[0] = get_i16(bytes, &p);
+				prm.g4->coords[1] = get_i16(bytes, &p);
+				prm.g4->coords[2] = get_i16(bytes, &p);
+				prm.g4->coords[3] = get_i16(bytes, &p);
+				prm.g4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.g4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.g4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				prm.g4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_GT3:
+				prm.ptr = mem_bump(sizeof(GT3));
+				prm.gt3->coords[0] = get_i16(bytes, &p);
+				prm.gt3->coords[1] = get_i16(bytes, &p);
+				prm.gt3->coords[2] = get_i16(bytes, &p);
+
+				prm.gt3->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.gt3->cba = get_i16(bytes, &p);
+				prm.gt3->tsb = get_i16(bytes, &p);
+				prm.gt3->u0 = get_i8(bytes, &p);
+				prm.gt3->v0 = get_i8(bytes, &p);
+				prm.gt3->u1 = get_i8(bytes, &p);
+				prm.gt3->v1 = get_i8(bytes, &p);
+				prm.gt3->u2 = get_i8(bytes, &p);
+				prm.gt3->v2 = get_i8(bytes, &p);
+				prm.gt3->pad1 = get_i16(bytes, &p);
+				prm.gt3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.gt3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.gt3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_GT4:
+				prm.ptr = mem_bump(sizeof(GT4));
+				prm.gt4->coords[0] = get_i16(bytes, &p);
+				prm.gt4->coords[1] = get_i16(bytes, &p);
+				prm.gt4->coords[2] = get_i16(bytes, &p);
+				prm.gt4->coords[3] = get_i16(bytes, &p);
+
+				prm.gt4->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.gt4->cba = get_i16(bytes, &p);
+				prm.gt4->tsb = get_i16(bytes, &p);
+				prm.gt4->u0 = get_i8(bytes, &p);
+				prm.gt4->v0 = get_i8(bytes, &p);
+				prm.gt4->u1 = get_i8(bytes, &p);
+				prm.gt4->v1 = get_i8(bytes, &p);
+				prm.gt4->u2 = get_i8(bytes, &p);
+				prm.gt4->v2 = get_i8(bytes, &p);
+				prm.gt4->u3 = get_i8(bytes, &p);
+				prm.gt4->v3 = get_i8(bytes, &p);
+				prm.gt4->pad1 = get_i16(bytes, &p);
+				prm.gt4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.gt4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.gt4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				prm.gt4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+
+			case PRM_TYPE_LSF3:
+				prm.ptr = mem_bump(sizeof(LSF3));
+				prm.lsf3->coords[0] = get_i16(bytes, &p);
+				prm.lsf3->coords[1] = get_i16(bytes, &p);
+				prm.lsf3->coords[2] = get_i16(bytes, &p);
+				prm.lsf3->normal = get_i16(bytes, &p);
+				prm.lsf3->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSF4:
+				prm.ptr = mem_bump(sizeof(LSF4));
+				prm.lsf4->coords[0] = get_i16(bytes, &p);
+				prm.lsf4->coords[1] = get_i16(bytes, &p);
+				prm.lsf4->coords[2] = get_i16(bytes, &p);
+				prm.lsf4->coords[3] = get_i16(bytes, &p);
+				prm.lsf4->normal = get_i16(bytes, &p);
+				prm.lsf4->pad1 = get_i16(bytes, &p);
+				prm.lsf4->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSFT3:
+				prm.ptr = mem_bump(sizeof(LSFT3));
+				prm.lsft3->coords[0] = get_i16(bytes, &p);
+				prm.lsft3->coords[1] = get_i16(bytes, &p);
+				prm.lsft3->coords[2] = get_i16(bytes, &p);
+				prm.lsft3->normal = get_i16(bytes, &p);
+
+				prm.lsft3->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.lsft3->cba = get_i16(bytes, &p);
+				prm.lsft3->tsb = get_i16(bytes, &p);
+				prm.lsft3->u0 = get_i8(bytes, &p);
+				prm.lsft3->v0 = get_i8(bytes, &p);
+				prm.lsft3->u1 = get_i8(bytes, &p);
+				prm.lsft3->v1 = get_i8(bytes, &p);
+				prm.lsft3->u2 = get_i8(bytes, &p);
+				prm.lsft3->v2 = get_i8(bytes, &p);
+				prm.lsft3->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSFT4:
+				prm.ptr = mem_bump(sizeof(LSFT4));
+				prm.lsft4->coords[0] = get_i16(bytes, &p);
+				prm.lsft4->coords[1] = get_i16(bytes, &p);
+				prm.lsft4->coords[2] = get_i16(bytes, &p);
+				prm.lsft4->coords[3] = get_i16(bytes, &p);
+				prm.lsft4->normal = get_i16(bytes, &p);
+
+				prm.lsft4->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.lsft4->cba = get_i16(bytes, &p);
+				prm.lsft4->tsb = get_i16(bytes, &p);
+				prm.lsft4->u0 = get_i8(bytes, &p);
+				prm.lsft4->v0 = get_i8(bytes, &p);
+				prm.lsft4->u1 = get_i8(bytes, &p);
+				prm.lsft4->v1 = get_i8(bytes, &p);
+				prm.lsft4->u2 = get_i8(bytes, &p);
+				prm.lsft4->v2 = get_i8(bytes, &p);
+				prm.lsft4->u3 = get_i8(bytes, &p);
+				prm.lsft4->v3 = get_i8(bytes, &p);
+				prm.lsft4->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSG3:
+				prm.ptr = mem_bump(sizeof(LSG3));
+				prm.lsg3->coords[0] = get_i16(bytes, &p);
+				prm.lsg3->coords[1] = get_i16(bytes, &p);
+				prm.lsg3->coords[2] = get_i16(bytes, &p);
+				prm.lsg3->normals[0] = get_i16(bytes, &p);
+				prm.lsg3->normals[1] = get_i16(bytes, &p);
+				prm.lsg3->normals[2] = get_i16(bytes, &p);
+				prm.lsg3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsg3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsg3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSG4:
+				prm.ptr = mem_bump(sizeof(LSG4));
+				prm.lsg4->coords[0] = get_i16(bytes, &p);
+				prm.lsg4->coords[1] = get_i16(bytes, &p);
+				prm.lsg4->coords[2] = get_i16(bytes, &p);
+				prm.lsg4->coords[3] = get_i16(bytes, &p);
+				prm.lsg4->normals[0] = get_i16(bytes, &p);
+				prm.lsg4->normals[1] = get_i16(bytes, &p);
+				prm.lsg4->normals[2] = get_i16(bytes, &p);
+				prm.lsg4->normals[3] = get_i16(bytes, &p);
+				prm.lsg4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsg4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsg4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsg4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSGT3:
+				prm.ptr = mem_bump(sizeof(LSGT3));
+				prm.lsgt3->coords[0] = get_i16(bytes, &p);
+				prm.lsgt3->coords[1] = get_i16(bytes, &p);
+				prm.lsgt3->coords[2] = get_i16(bytes, &p);
+				prm.lsgt3->normals[0] = get_i16(bytes, &p);
+				prm.lsgt3->normals[1] = get_i16(bytes, &p);
+				prm.lsgt3->normals[2] = get_i16(bytes, &p);
+
+				prm.lsgt3->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.lsgt3->cba = get_i16(bytes, &p);
+				prm.lsgt3->tsb = get_i16(bytes, &p);
+				prm.lsgt3->u0 = get_i8(bytes, &p);
+				prm.lsgt3->v0 = get_i8(bytes, &p);
+				prm.lsgt3->u1 = get_i8(bytes, &p);
+				prm.lsgt3->v1 = get_i8(bytes, &p);
+				prm.lsgt3->u2 = get_i8(bytes, &p);
+				prm.lsgt3->v2 = get_i8(bytes, &p);
+				prm.lsgt3->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsgt3->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsgt3->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_LSGT4:
+				prm.ptr = mem_bump(sizeof(LSGT4));
+				prm.lsgt4->coords[0] = get_i16(bytes, &p);
+				prm.lsgt4->coords[1] = get_i16(bytes, &p);
+				prm.lsgt4->coords[2] = get_i16(bytes, &p);
+				prm.lsgt4->coords[3] = get_i16(bytes, &p);
+				prm.lsgt4->normals[0] = get_i16(bytes, &p);
+				prm.lsgt4->normals[1] = get_i16(bytes, &p);
+				prm.lsgt4->normals[2] = get_i16(bytes, &p);
+				prm.lsgt4->normals[3] = get_i16(bytes, &p);
+
+				prm.lsgt4->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.lsgt4->cba = get_i16(bytes, &p);
+				prm.lsgt4->tsb = get_i16(bytes, &p);
+				prm.lsgt4->u0 = get_i8(bytes, &p);
+				prm.lsgt4->v0 = get_i8(bytes, &p);
+				prm.lsgt4->u1 = get_i8(bytes, &p);
+				prm.lsgt4->v1 = get_i8(bytes, &p);
+				prm.lsgt4->u2 = get_i8(bytes, &p);
+				prm.lsgt4->v2 = get_i8(bytes, &p);
+				prm.lsgt4->pad1 = get_i16(bytes, &p);
+				prm.lsgt4->colour[0] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsgt4->colour[1] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsgt4->colour[2] = int32_to_rgba(get_i32(bytes, &p));
+				prm.lsgt4->colour[3] = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+
+			case PRM_TYPE_TSPR:
+			case PRM_TYPE_BSPR:
+				prm.ptr = mem_bump(sizeof(SPR));
+				prm.spr->coord = get_i16(bytes, &p);
+				prm.spr->width = get_i16(bytes, &p);
+				prm.spr->height = get_i16(bytes, &p);
+				prm.spr->texture = texture_from_list(tl, get_i16(bytes, &p));
+				prm.spr->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_SPLINE:
+				prm.ptr = mem_bump(sizeof(Spline));
+				prm.spline->control1.x = get_i32(bytes, &p);
+				prm.spline->control1.y = get_i32(bytes, &p);
+				prm.spline->control1.z = get_i32(bytes, &p);
+				p += 4; // padding
+				prm.spline->position.x = get_i32(bytes, &p);
+				prm.spline->position.y = get_i32(bytes, &p);
+				prm.spline->position.z = get_i32(bytes, &p);
+				p += 4; // padding
+				prm.spline->control2.x = get_i32(bytes, &p);
+				prm.spline->control2.y = get_i32(bytes, &p);
+				prm.spline->control2.z = get_i32(bytes, &p);
+				p += 4; // padding
+				prm.spline->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+			case PRM_TYPE_POINT_LIGHT:
+				prm.ptr = mem_bump(sizeof(PointLight));
+				prm.pointLight->position.x = get_i32(bytes, &p);
+				prm.pointLight->position.y = get_i32(bytes, &p);
+				prm.pointLight->position.z = get_i32(bytes, &p);
+				p += 4; // padding
+				prm.pointLight->colour = int32_to_rgba(get_i32(bytes, &p));
+				prm.pointLight->startFalloff = get_i16(bytes, &p);
+				prm.pointLight->endFalloff = get_i16(bytes, &p);
+				break;
+
+			case PRM_TYPE_SPOT_LIGHT:
+				prm.ptr = mem_bump(sizeof(SpotLight));
+				prm.spotLight->position.x = get_i32(bytes, &p);
+				prm.spotLight->position.y = get_i32(bytes, &p);
+				prm.spotLight->position.z = get_i32(bytes, &p);
+				p += 4; // padding
+				prm.spotLight->direction.x = get_i16(bytes, &p);
+				prm.spotLight->direction.y = get_i16(bytes, &p);
+				prm.spotLight->direction.z = get_i16(bytes, &p);
+				p += 2; // padding
+				prm.spotLight->colour = int32_to_rgba(get_i32(bytes, &p));
+				prm.spotLight->startFalloff = get_i16(bytes, &p);
+				prm.spotLight->endFalloff = get_i16(bytes, &p);
+				prm.spotLight->coneAngle = get_i16(bytes, &p);
+				prm.spotLight->spreadAngle = get_i16(bytes, &p);
+				break;
+
+			case PRM_TYPE_INFINITE_LIGHT:
+				prm.ptr = mem_bump(sizeof(InfiniteLight));
+				prm.infiniteLight->direction.x = get_i16(bytes, &p);
+				prm.infiniteLight->direction.y = get_i16(bytes, &p);
+				prm.infiniteLight->direction.z = get_i16(bytes, &p);
+				p += 2; // padding
+				prm.infiniteLight->colour = int32_to_rgba(get_i32(bytes, &p));
+				break;
+
+
+			default:
+				die("bad primitive type %x \n", prm_type);
+			} // switch
+
+			prm.f3->type = prm_type;
+			prm.f3->flag = prm_flag;
+		} // each prim
+	} // each object
+
+	mem_temp_free(bytes);
+	return objectList;
+}
+
+
+void object_draw(Object *object, mat4_t *mat) {
+	vec3_t *vertex = object->vertices;
+
+	Prm poly = {.primitive = object->primitives};
+	int primitives_len = object->primitives_len;
+
+	render_set_model_mat(mat);
+
+	// TODO: check for PRM_SINGLE_SIDED
+
+	for (int i = 0; i < primitives_len; i++) {
+		int coord0;
+		int coord1;
+		int coord2;
+		int coord3;
+		switch (poly.primitive->type) {
+		case PRM_TYPE_GT3:
+			coord0 = poly.gt3->coords[0];
+			coord1 = poly.gt3->coords[1];
+			coord2 = poly.gt3->coords[2];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.gt3->u2, poly.gt3->v2},
+						.color = poly.gt3->colour[2]
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.gt3->u1, poly.gt3->v1},
+						.color = poly.gt3->colour[1]
+					},
+					{
+						.pos = vertex[coord0],
+						.uv = {poly.gt3->u0, poly.gt3->v0},
+						.color = poly.gt3->colour[0]
+					},
+				}
+			}, poly.gt3->texture);
+
+			poly.gt3 += 1;
+			break;
+
+		case PRM_TYPE_GT4:
+			coord0 = poly.gt4->coords[0];
+			coord1 = poly.gt4->coords[1];
+			coord2 = poly.gt4->coords[2];
+			coord3 = poly.gt4->coords[3];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.gt4->u2, poly.gt4->v2},
+						.color = poly.gt4->colour[2]
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.gt4->u1, poly.gt4->v1},
+						.color = poly.gt4->colour[1]
+					},
+					{
+						.pos = vertex[coord0],
+						.uv = {poly.gt4->u0, poly.gt4->v0},
+						.color = poly.gt4->colour[0]
+					},
+				}
+			}, poly.gt4->texture);
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.gt4->u2, poly.gt4->v2},
+						.color = poly.gt4->colour[2]
+					},
+					{
+						.pos = vertex[coord3],
+						.uv = {poly.gt4->u3, poly.gt4->v3},
+						.color = poly.gt4->colour[3]
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.gt4->u1, poly.gt4->v1},
+						.color = poly.gt4->colour[1]
+					},
+				}
+			}, poly.gt4->texture);
+
+			poly.gt4 += 1;
+			break;
+
+		case PRM_TYPE_FT3:
+			coord0 = poly.ft3->coords[0];
+			coord1 = poly.ft3->coords[1];
+			coord2 = poly.ft3->coords[2];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.ft3->u2, poly.ft3->v2},
+						.color = poly.ft3->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.ft3->u1, poly.ft3->v1},
+						.color = poly.ft3->colour
+					},
+					{
+						.pos = vertex[coord0],
+						.uv = {poly.ft3->u0, poly.ft3->v0},
+						.color = poly.ft3->colour
+					},
+				}
+			}, poly.ft3->texture);
+
+			poly.ft3 += 1;
+			break;
+
+		case PRM_TYPE_FT4:
+			coord0 = poly.ft4->coords[0];
+			coord1 = poly.ft4->coords[1];
+			coord2 = poly.ft4->coords[2];
+			coord3 = poly.ft4->coords[3];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.ft4->u2, poly.ft4->v2},
+						.color = poly.ft4->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.ft4->u1, poly.ft4->v1},
+						.color = poly.ft4->colour
+					},
+					{
+						.pos = vertex[coord0],
+						.uv = {poly.ft4->u0, poly.ft4->v0},
+						.color = poly.ft4->colour
+					},
+				}
+			}, poly.ft4->texture);
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.uv = {poly.ft4->u2, poly.ft4->v2},
+						.color = poly.ft4->colour
+					},
+					{
+						.pos = vertex[coord3],
+						.uv = {poly.ft4->u3, poly.ft4->v3},
+						.color = poly.ft4->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.uv = {poly.ft4->u1, poly.ft4->v1},
+						.color = poly.ft4->colour
+					},
+				}
+			}, poly.ft4->texture);
+
+			poly.ft4 += 1;
+			break;
+
+		case PRM_TYPE_G3:
+			coord0 = poly.g3->coords[0];
+			coord1 = poly.g3->coords[1];
+			coord2 = poly.g3->coords[2];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.g3->colour[2]
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.g3->colour[1]
+					},
+					{
+						.pos = vertex[coord0],
+						.color = poly.g3->colour[0]
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+
+			poly.g3 += 1;
+			break;
+
+		case PRM_TYPE_G4:
+			coord0 = poly.g4->coords[0];
+			coord1 = poly.g4->coords[1];
+			coord2 = poly.g4->coords[2];
+			coord3 = poly.g4->coords[3];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.g4->colour[2]
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.g4->colour[1]
+					},
+					{
+						.pos = vertex[coord0],
+						.color = poly.g4->colour[0]
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.g4->colour[2]
+					},
+					{
+						.pos = vertex[coord3],
+						.color = poly.g4->colour[3]
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.g4->colour[1]
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+
+			poly.g4 += 1;
+			break;
+
+		case PRM_TYPE_F3:
+			coord0 = poly.f3->coords[0];
+			coord1 = poly.f3->coords[1];
+			coord2 = poly.f3->coords[2];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.f3->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.f3->colour
+					},
+					{
+						.pos = vertex[coord0],
+						.color = poly.f3->colour
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+
+			poly.f3 += 1;
+			break;
+
+		case PRM_TYPE_F4:
+			coord0 = poly.f4->coords[0];
+			coord1 = poly.f4->coords[1];
+			coord2 = poly.f4->coords[2];
+			coord3 = poly.f4->coords[3];
+
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.f4->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.f4->colour
+					},
+					{
+						.pos = vertex[coord0],
+						.color = poly.f4->colour
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+			render_push_tris((tris_t) {
+				.vertices = {
+					{
+						.pos = vertex[coord2],
+						.color = poly.f4->colour
+					},
+					{
+						.pos = vertex[coord3],
+						.color = poly.f4->colour
+					},
+					{
+						.pos = vertex[coord1],
+						.color = poly.f4->colour
+					},
+				}
+			}, RENDER_NO_TEXTURE);
+
+			poly.f4 += 1;
+			break;
+
+		case PRM_TYPE_TSPR:
+		case PRM_TYPE_BSPR:
+			coord0 = poly.spr->coord;
+
+			render_push_sprite(
+				vec3(
+					vertex[coord0].x,
+					vertex[coord0].y + ((poly.primitive->type == PRM_TYPE_TSPR ? poly.spr->height : -poly.spr->height) >> 1),
+					vertex[coord0].z
+				),
+				vec2i(poly.spr->width, poly.spr->height),
+				poly.spr->colour,
+				poly.spr->texture
+			);
+
+			poly.spr += 1;
+			break;
+
+		default:
+			break;
+
+		}
+	}
+}
--- /dev/null
+++ b/src/wipeout/object.h
@@ -1,0 +1,383 @@
+#ifndef OBJECT_H
+#define OBJECT_H
+
+#include "../types.h"
+#include "../render.h"
+#include "../utils.h"
+#include "image.h"
+
+// Primitive Structure Stub ( Structure varies with primitive type )
+
+typedef struct Primitive {
+	int16_t type; // Type of Primitive
+} Primitive;
+
+
+typedef struct F3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t pad1;
+	rgba_t colour;
+} F3;
+
+typedef struct FT3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	int16_t pad1;
+	rgba_t colour;
+} FT3;
+
+typedef struct F4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	rgba_t colour;
+} F4;
+
+typedef struct FT4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	uint8_t u3;
+	uint8_t v3;
+	int16_t pad1;
+	rgba_t colour;
+} FT4;
+
+typedef struct G3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t pad1;
+	rgba_t colour[3];
+} G3;
+
+typedef struct GT3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	int16_t pad1;
+	rgba_t colour[3];
+} GT3;
+
+typedef struct G4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	rgba_t colour[4];
+} G4;
+
+typedef struct GT4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	uint8_t u3;
+	uint8_t v3;
+	int16_t pad1;
+	rgba_t colour[4];
+} GT4;
+
+
+
+
+/* LIGHT SOURCED POLYGONS
+*/
+
+typedef struct LSF3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t normal; // Indices of the normals
+	rgba_t colour;
+} LSF3;
+
+typedef struct LSFT3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t normal; // Indices of the normals
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	rgba_t colour;
+} LSFT3;
+
+typedef struct LSF4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t normal; // Indices of the normals
+	int16_t pad1;
+	rgba_t colour;
+} LSF4;
+
+typedef struct LSFT4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t normal; // Indices of the normals
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	uint8_t u3;
+	uint8_t v3;
+	rgba_t colour;
+} LSFT4;
+
+typedef struct LSG3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t normals[3]; // Indices of the normals
+	rgba_t colour[3];
+} LSG3;
+
+typedef struct LSGT3 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[3]; // Indices of the coords
+	int16_t normals[3]; // Indices of the normals
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	rgba_t colour[3];
+} LSGT3;
+
+typedef struct LSG4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t normals[4]; // Indices of the normals
+	rgba_t colour[4];
+} LSG4;
+
+typedef struct LSGT4 {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	int16_t coords[4]; // Indices of the coords
+	int16_t normals[4]; // Indices of the normals
+	int16_t texture;
+	int16_t cba;
+	int16_t tsb;
+	uint8_t u0;
+	uint8_t v0;
+	uint8_t u1;
+	uint8_t v1;
+	uint8_t u2;
+	uint8_t v2;
+	uint8_t u3;
+	uint8_t v3;
+	int16_t pad1;
+	rgba_t colour[4];
+} LSGT4;
+
+
+
+
+
+
+/* OTHER PRIMITIVE TYPES
+*/
+typedef struct SPR {
+	int16_t type;
+	int16_t flag;
+	int16_t coord;
+	int16_t width;
+	int16_t height;
+	int16_t texture;
+	rgba_t colour;
+} SPR;
+
+
+typedef struct Spline {
+	int16_t type; // Type of primitive
+	int16_t flag;
+	vec3_t control1;
+	vec3_t position;
+	vec3_t control2;
+	rgba_t colour;
+} Spline;
+
+
+typedef struct PointLight {
+	int16_t type;
+	int16_t flag;
+	vec3_t position;
+	rgba_t colour;
+	int16_t startFalloff;
+	int16_t endFalloff;
+} PointLight;
+
+
+typedef struct SpotLight {
+	int16_t type;
+	int16_t flag;
+	vec3_t position;
+	vec3_t direction;
+	rgba_t colour;
+	int16_t startFalloff;
+	int16_t endFalloff;
+	int16_t coneAngle;
+	int16_t spreadAngle;
+} SpotLight;
+
+
+typedef struct InfiniteLight {
+	int16_t type;
+	int16_t flag;
+	vec3_t direction;
+	rgba_t colour;
+} InfiniteLight;
+
+
+
+
+
+
+// PRIMITIVE FLAGS
+
+#define PRM_SINGLE_SIDED 0x0001
+#define PRM_SHIP_ENGINE  0x0002
+#define PRM_TRANSLUCENT  0x0004
+
+
+
+#define PRM_TYPE_F3               1
+#define PRM_TYPE_FT3              2
+#define PRM_TYPE_F4               3
+#define PRM_TYPE_FT4              4
+#define PRM_TYPE_G3               5
+#define PRM_TYPE_GT3              6
+#define PRM_TYPE_G4               7
+#define PRM_TYPE_GT4              8
+
+#define PRM_TYPE_LF2              9
+#define PRM_TYPE_TSPR             10
+#define PRM_TYPE_BSPR             11
+
+#define PRM_TYPE_LSF3             12
+#define PRM_TYPE_LSFT3            13
+#define PRM_TYPE_LSF4             14
+#define PRM_TYPE_LSFT4            15
+#define PRM_TYPE_LSG3             16
+#define PRM_TYPE_LSGT3            17
+#define PRM_TYPE_LSG4             18
+#define PRM_TYPE_LSGT4            19
+
+#define PRM_TYPE_SPLINE           20
+
+#define PRM_TYPE_INFINITE_LIGHT    21
+#define PRM_TYPE_POINT_LIGHT       22
+#define PRM_TYPE_SPOT_LIGHT        23
+
+
+typedef struct Object {
+	char name[16];
+
+	mat4_t mat;
+	int16_t vertices_len; // Number of Vertices
+	vec3_t *vertices; // Pointer to 3D Points
+
+	int16_t normals_len; // Number of Normals
+	vec3_t *normals; // Pointer to 3D Normals
+
+	int16_t primitives_len; // Number of Primitives
+	Primitive *primitives; // Pointer to Z Sort Primitives
+
+	vec3_t origin;
+	int32_t extent; // Flags for object characteristics
+	int16_t flags; // Next object in list
+	struct Object *next; // Next object in list
+} Object;
+
+typedef union Prm {
+	uint8_t *ptr;
+	int16_t *sptr;
+	int32_t *lptr;
+	Object *object;
+	Primitive        *primitive;
+
+	F3               *f3;
+	FT3              *ft3;
+	F4               *f4;
+	FT4              *ft4;
+	G3               *g3;
+	GT3              *gt3;
+	G4               *g4;
+	GT4              *gt4;
+	SPR              *spr;
+	Spline           *spline;
+	PointLight       *pointLight;
+	SpotLight        *spotLight;
+	InfiniteLight    *infiniteLight;
+
+	LSF3             *lsf3;
+	LSFT3            *lsft3;
+	LSF4             *lsf4;
+	LSFT4            *lsft4;
+	LSG3             *lsg3;
+	LSGT3            *lsgt3;
+	LSG4             *lsg4;
+	LSGT4            *lsgt4;
+} Prm;
+
+Object *objects_load(char *name, texture_list_t tl);
+void object_draw(Object *object, mat4_t *mat);
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/particle.c
@@ -1,0 +1,70 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+#include "../render.h"
+
+#include "particle.h"
+#include "image.h"
+
+static particle_t *particles;
+static int particles_active = 0;
+static texture_list_t particle_textures;
+
+void particles_load() {
+	particles = mem_bump(sizeof(particle_t) * PARTICLES_MAX);
+	particle_textures = image_get_compressed_textures("wipeout/common/effects.cmp");
+	particles_init();
+}
+
+void particles_init() {
+	particles_active = 0;
+}
+
+void particles_update() {
+	for (int i = 0; i < particles_active; i++) {
+		particle_t *p = &particles[i];
+
+		p->timer -= system_tick();
+		p->position = vec3_add(p->position, vec3_mulf(p->velocity, system_tick()));
+		if (p->timer < 0) {
+			particles[i--] = particles[--particles_active];
+			continue;
+		}
+	}
+}
+
+void particles_draw() {
+	if (particles_active == 0) {
+		return;
+	}
+
+	render_set_model_mat(&mat4_identity());
+	render_set_depth_write(false);
+	render_set_blend_mode(RENDER_BLEND_LIGHTER);
+	render_set_depth_offset(-32.0);
+
+	for (int i = 0; i < particles_active; i++) {
+		particle_t *p = &particles[i];
+		render_push_sprite(p->position, p->size, p->color, p->texture);
+	}
+
+	render_set_depth_offset(0.0);
+	render_set_depth_write(true);
+	render_set_blend_mode(RENDER_BLEND_NORMAL);
+}
+
+void particles_spawn(vec3_t position, uint16_t type, vec3_t velocity, int size) {
+	if (particles_active == PARTICLES_MAX) {
+		return;
+	}
+
+	particle_t *p = &particles[particles_active++];
+	p->color = rgba(128, 128, 128, 128);
+	p->texture = texture_from_list(particle_textures, type);
+	p->position = position;
+	p->velocity = velocity;
+	p->timer = rand_float(0.75, 1.0);
+	p->size.x = size;
+	p->size.y = size;
+}
+
--- /dev/null
+++ b/src/wipeout/particle.h
@@ -1,0 +1,32 @@
+#ifndef PARTICLE_H
+#define PARTICLE_H
+
+#include "../types.h"
+
+#define PARTICLES_MAX 1024
+
+#define PARTICLE_TYPE_NONE -1
+#define PARTICLE_TYPE_FIRE 0
+#define PARTICLE_TYPE_FIRE_WHITE 1
+#define PARTICLE_TYPE_SMOKE 2
+#define PARTICLE_TYPE_EBOLT 3
+#define PARTICLE_TYPE_HALO 4
+#define PARTICLE_TYPE_GREENY 5
+
+typedef struct particle_t {
+	vec3_t position;
+	vec3_t velocity;
+	vec2i_t size;
+	rgba_t color;
+	float timer;
+	uint16_t type;
+	uint16_t texture;
+} particle_t;
+
+void particles_load();
+void particles_init();
+void particles_spawn(vec3_t position, uint16_t type, vec3_t velocity, int size);
+void particles_draw();
+void particles_update();
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/race.c
@@ -1,0 +1,269 @@
+#include "../mem.h"
+#include "../input.h"
+#include "../platform.h"
+#include "../system.h"
+#include "../utils.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "droid.h"
+#include "camera.h"
+#include "object.h"
+#include "scene.h"
+#include "game.h"
+#include "hud.h"
+#include "sfx.h"
+#include "race.h"
+#include "particle.h"
+#include "menu.h"
+#include "ship_ai.h"
+#include "ingame_menus.h"
+
+#define ATTRACT_DURATION 60.0
+
+static bool is_paused = false;
+static bool menu_is_scroll_text = false;
+static bool has_show_credits = false;
+static float attract_start_time;
+static menu_t *active_menu = NULL;
+
+void race_init() {
+	ingame_menus_load();
+	menu_is_scroll_text = false;
+
+	const circut_settings_t *cs = &def.circuts[g.circut].settings[g.race_class];
+	track_load(cs->path);
+	scene_load(cs->path, cs->sky_y_offset);
+	
+	if (g.circut == CIRCUT_SILVERSTREAM && g.race_class == RACE_CLASS_RAPIER) {
+		scene_init_aurora_borealis();	
+	} 
+
+	race_start();
+	// render_textures_dump("texture_atlas.png");
+
+	if (g.is_attract_mode) {
+		attract_start_time = system_time();
+		for (int i = 0; i < len(g.ships); i++) {
+			// FIXME: this is needed to initializes the engine sound. Should 
+			// maybe be done in a separate step?
+			ship_ai_update_intro(&g.ships[i]); 
+
+			g.ships[i].update_func = ship_ai_update_race;
+			flags_rm(g.ships[i].flags, SHIP_VIEW_INTERNAL);
+			flags_rm(g.ships[i].flags, SHIP_RACING);
+		}
+		g.pilot = rand_int(0, len(def.pilots));
+		g.camera.update_func = camera_update_attract_random;
+		if (!has_show_credits || rand_int(0, 10) == 0) {
+			active_menu = text_scroll_menu_init(def.credits, len(def.credits));
+			menu_is_scroll_text = true;
+			has_show_credits = true;
+		}
+	}
+
+	is_paused = false;
+}
+
+void race_update() {
+	if (!is_paused) {
+		ships_update();
+		droid_update(&g.droid, &g.ships[g.pilot]);
+		camera_update(&g.camera, &g.ships[g.pilot], &g.droid);
+		weapons_update();
+		particles_update();
+		scene_update();
+		if (g.race_type != RACE_TYPE_TIME_TRIAL) {
+			track_cycle_pickups();
+		}
+
+		if (g.is_attract_mode) {
+			if (input_pressed(A_MENU_START) || input_pressed(A_MENU_SELECT)) {
+				game_set_scene(GAME_SCENE_MAIN_MENU);
+			}
+			float duration = system_time() - attract_start_time;
+			if ((!active_menu && duration > 30) || duration > 120) {
+				game_set_scene(GAME_SCENE_TITLE);
+			}
+		}
+		else if (active_menu == NULL && input_pressed(A_MENU_START)) {
+			race_pause();
+
+		}
+	}
+	else if (input_pressed(A_MENU_START)) {
+		race_unpause();
+	}
+
+
+	// Draw 3D
+	render_set_view(g.camera.position, g.camera.angle);
+
+	render_set_cull_backface(false);
+	scene_draw(&g.camera);	
+	track_draw(&g.camera);
+	render_set_cull_backface(true);
+
+	ships_draw();
+	droid_draw(&g.droid);
+	weapons_draw();
+	particles_draw();
+
+	// Draw 2d
+	render_set_view_2d();
+
+	if (flags_is(g.ships[g.pilot].flags, SHIP_RACING)) {
+		hud_draw(&g.ships[g.pilot]);
+	}
+
+	if (active_menu) {
+		if (!menu_is_scroll_text) {
+			vec2i_t size = render_size();
+			render_push_2d(vec2i(0, 0), size, rgba(0, 0, 0, 128), RENDER_NO_TEXTURE);
+		}
+		menu_update(active_menu);
+	}
+}
+
+void race_start() {
+	active_menu = NULL;
+	sfx_reset();
+	scene_init();
+	camera_init(&g.camera, g.track.sections);
+	g.camera.update_func = camera_update_race_intro;
+	ships_init(g.track.sections);
+	droid_init(&g.droid, &g.ships[g.pilot]);
+	particles_init();
+	weapons_init();
+
+	for (int i = 0; i < len(g.race_ranks); i++) {
+		g.race_ranks[i].points = 0;
+		g.race_ranks[i].pilot = i;
+	}
+	for (int i = 0; i < len(g.lap_times); i++) {
+		for (int j = 0; j < len(g.lap_times[i]); j++) {
+			g.lap_times[i][j] = 0;
+		}
+	}
+	g.is_new_race_record = false;
+	g.is_new_lap_record = false;
+	g.best_lap = 0;
+	g.race_time = 0;
+}
+
+void race_restart() {
+	race_unpause();
+
+	if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		g.lives--;
+		if (g.lives == 0) {
+			race_release_control();
+			active_menu = game_over_menu_init();
+			return;
+		}
+	}
+
+	race_start();
+}
+
+static bool sort_points_compare(pilot_points_t *pa, pilot_points_t *pb) {
+	return (pa->points < pb->points);
+}
+
+void race_end() {
+	race_release_control();
+
+	g.race_position = g.ships[g.pilot].position_rank;
+
+	g.race_time = 0;
+	g.best_lap = g.lap_times[g.pilot][0];
+	for (int i = 0; i < NUM_LAPS; i++) {
+		g.race_time += g.lap_times[g.pilot][i];
+		if (g.lap_times[g.pilot][i] < g.best_lap) {
+			g.best_lap = g.lap_times[g.pilot][i];
+		}
+	}
+
+	highscores_t *hs = &save.highscores[g.race_class][g.circut][g.highscore_tab];
+	if (g.best_lap < hs->lap_record) {
+		hs->lap_record = g.best_lap;
+		g.is_new_lap_record = true;
+		save.is_dirty = true;
+	}
+
+	for (int i = 0; i < NUM_HIGHSCORES; i++) {
+		if (g.race_time < hs->entries[i].time) {
+			g.is_new_race_record = true;
+			break;
+		}
+	}
+
+	if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		for (int i = 0; i < len(def.race_points_for_rank); i++) {
+			g.race_ranks[i].points = def.race_points_for_rank[i];
+			g.championship_ranks[g.race_ranks[i].pilot].points += def.race_points_for_rank[i];
+		}
+		sort(g.championship_ranks, len(g.championship_ranks), sort_points_compare);
+	}
+
+	active_menu = race_stats_menu_init();
+}
+
+void race_next() {
+	int next_circut = g.circut + 1;
+
+	// Championship complete
+	if (
+		(save.has_bonus_circuts && next_circut >= NUM_CIRCUTS) ||
+		(!save.has_bonus_circuts && next_circut >= NUM_NON_BONUS_CIRCUTS)
+	) {
+		if (g.race_class == RACE_CLASS_RAPIER) {
+			if (save.has_bonus_circuts) {
+				active_menu = text_scroll_menu_init(def.congratulations.rapier_all_circuts, len(def.congratulations.rapier_all_circuts));
+			}
+			else {
+				save.has_bonus_circuts = true;
+				active_menu = text_scroll_menu_init(def.congratulations.rapier, len(def.congratulations.rapier));
+			}
+		}
+		else {
+			save.has_rapier_class = true;
+			if (save.has_bonus_circuts) {
+				active_menu = text_scroll_menu_init(def.congratulations.venom_all_circuts, len(def.congratulations.venom_all_circuts));
+			}
+			else {
+				active_menu = text_scroll_menu_init(def.congratulations.venom, len(def.congratulations.venom));
+			}
+		}
+		save.is_dirty = true;
+		menu_is_scroll_text = true;
+	}
+
+	// Next track
+	else {
+		g.circut = next_circut;
+		game_set_scene(GAME_SCENE_RACE);
+	}
+}
+
+void race_release_control() {
+	flags_rm(g.ships[g.pilot].flags, SHIP_RACING);
+	g.ships[g.pilot].remote_thrust_max = 3160;
+	g.ships[g.pilot].remote_thrust_mag = 32;
+	g.ships[g.pilot].speed = 3160;
+	g.camera.update_func = camera_update_attract_random;
+}
+
+void race_pause() {
+	sfx_pause();
+	is_paused = true;
+	active_menu = pause_menu_init();
+}
+
+void race_unpause() {
+	sfx_unpause();
+	is_paused = false;
+	active_menu = NULL;
+}
--- /dev/null
+++ b/src/wipeout/race.h
@@ -1,0 +1,14 @@
+#ifndef RACE_H
+#define RACE_H
+
+void race_init();
+void race_update();
+void race_start();
+void race_restart();
+void race_pause();
+void race_unpause();
+void race_end();
+void race_next();
+void race_release_control();
+
+#endif
--- /dev/null
+++ b/src/wipeout/scene.c
@@ -1,0 +1,261 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "scene.h"
+#include "droid.h"
+#include "camera.h"
+#include "object.h"
+#include "game.h"
+
+
+#define SCENE_START_BOOMS_MAX 4
+#define SCENE_OIL_PUMPS_MAX 2
+#define SCENE_RED_LIGHTS_MAX 4
+#define SCENE_STANDS_MAX 20
+
+static Object *scene_objects;
+static Object *sky_object;
+static vec3_t sky_offset;
+
+static Object *start_booms[SCENE_START_BOOMS_MAX];
+static int start_booms_len;
+
+static Object *oil_pumps[SCENE_OIL_PUMPS_MAX];
+static int oil_pumps_len;
+
+static Object *red_lights[SCENE_RED_LIGHTS_MAX];
+static int red_lights_len;
+
+typedef struct {
+	sfx_t *sfx;
+	vec3_t pos;
+} scene_stand_t;
+static scene_stand_t stands[SCENE_STANDS_MAX];
+static int stands_len;
+
+static struct {
+	bool enabled;
+	GT4	*primitives[80];
+	int16_t *coords[80];
+	int16_t grey_coords[80];	
+} aurora_borealis;
+
+void scene_pulsate_red_light(Object *obj);
+void scene_move_oil_pump(Object *obj);
+void scene_update_aurora_borealis();
+
+void scene_load(const char *base_path, float sky_y_offset) {
+	texture_list_t scene_textures = image_get_compressed_textures(get_path(base_path, "scene.cmp"));
+	scene_objects = objects_load(get_path(base_path, "scene.prm"), scene_textures);
+	
+	texture_list_t sky_textures = image_get_compressed_textures(get_path(base_path, "sky.cmp"));
+	sky_object = objects_load(get_path(base_path, "sky.prm") , sky_textures);
+	sky_offset = vec3(0, sky_y_offset, 0);
+
+	// Collect all objects that need to be updated each frame
+	start_booms_len = 0;
+	oil_pumps_len = 0;
+	red_lights_len = 0;
+	stands_len = 0;
+
+	Object *obj = scene_objects;
+	while (obj) {
+		mat4_set_translation(&obj->mat, obj->origin);
+
+		if (str_starts_with(obj->name, "start")) {
+			error_if(start_booms_len >= SCENE_START_BOOMS_MAX, "SCENE_START_BOOMS_MAX reached");
+			start_booms[start_booms_len++] = obj;
+		}
+		else if (str_starts_with(obj->name, "redl")) {
+			error_if(red_lights_len >= SCENE_RED_LIGHTS_MAX, "SCENE_RED_LIGHTS_MAX reached");
+			red_lights[red_lights_len++] = obj;
+		}
+		else if (str_starts_with(obj->name, "donkey")) {
+			error_if(oil_pumps_len >= SCENE_OIL_PUMPS_MAX, "SCENE_OIL_PUMPS_MAX reached");
+			oil_pumps[oil_pumps_len++] = obj;
+		}
+		else if (
+			str_starts_with(obj->name, "lostad") || 
+			str_starts_with(obj->name, "stad_") ||
+			str_starts_with(obj->name, "newstad_")
+		) {
+			error_if(stands_len >= SCENE_STANDS_MAX, "SCENE_STANDS_MAX reached");
+			stands[stands_len++] = (scene_stand_t){.sfx = NULL, .pos = obj->origin};
+		}
+		obj = obj->next;
+	}
+
+	aurora_borealis.enabled = false;
+}
+
+void scene_init() {
+	scene_set_start_booms(0);
+	for (int i = 0; i < stands_len; i++) {
+		stands[i].sfx = sfx_reserve_loop(SFX_CROWD);
+	}
+}
+
+void scene_update() {
+	for (int i = 0; i < red_lights_len; i++) {
+		scene_pulsate_red_light(red_lights[i]);
+	}
+	for (int i = 0; i < oil_pumps_len; i++) {
+		scene_move_oil_pump(oil_pumps[i]);
+	}
+	for (int i = 0; i < stands_len; i++) {
+		sfx_set_position(stands[i].sfx, stands[i].pos, vec3(0, 0, 0), 0.4);
+	}
+
+	if (aurora_borealis.enabled) {
+		scene_update_aurora_borealis();
+	}
+}
+
+void scene_draw(camera_t *camera) {
+	// Sky
+	render_set_depth_write(false);
+	mat4_set_translation(&sky_object->mat, vec3_add(camera->position, sky_offset));
+	object_draw(sky_object, &sky_object->mat);
+	render_set_depth_write(true);
+
+	// Nearby objects
+	vec3_t cam_pos = camera->position;
+	Object *object = scene_objects;
+	float max_dist_sq = RENDER_FADEOUT_FAR * RENDER_FADEOUT_FAR;
+	while (object) {
+		vec3_t d = vec3_sub(cam_pos, object->origin);
+		float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
+
+		if (dist_sq < max_dist_sq) {
+			object_draw(object, &object->mat);
+		}
+		
+		object = object->next;
+	}
+
+}
+
+void scene_set_start_booms(int light_index) {
+	
+	int lights_len = 1;
+	rgba_t color = rgba(0, 0, 0, 0);
+
+	if (light_index == 0) { // reset all 3
+		lights_len = 3;
+		color = rgba(0x20, 0x20, 0x20, 0xff);
+	}
+	else if (light_index == 1) {
+		color = rgba(0xff, 0x00, 0x00, 0xff);
+	}
+	else if (light_index == 2) {
+		color = rgba(0xff, 0x80, 0x00, 0xff);
+	}
+	else if (light_index == 3) {
+		color = rgba(0x00, 0xff, 0x00, 0xff);
+	}
+
+	for (int i = 0; i < start_booms_len; i++) {
+		Prm libPoly = {.primitive = start_booms[i]->primitives};
+
+		for (int j = 1; j < light_index; j++) {
+			libPoly.gt4 += 1;
+		}
+
+		for (int j = 0; j < lights_len; j++) {
+			for (int v = 0; v < 4; v++) {
+				libPoly.gt4->colour[v].as_rgba.r = color.as_rgba.r;
+				libPoly.gt4->colour[v].as_rgba.g = color.as_rgba.g;
+				libPoly.gt4->colour[v].as_rgba.b = color.as_rgba.b;
+			}
+			libPoly.gt4 += 1;
+		}
+	}
+}
+
+
+void scene_pulsate_red_light(Object *obj) {
+	uint8_t r = clamp(sin(system_cycle_time() * M_PI * 2) * 128 + 128, 0, 255);
+	Prm libPoly = {.primitive = obj->primitives};
+
+	for (int v = 0; v < 4; v++) {
+		libPoly.gt4->colour[v].as_rgba.r = r;
+		libPoly.gt4->colour[v].as_rgba.g = 0x00;
+		libPoly.gt4->colour[v].as_rgba.b = 0x00;
+	}
+}
+
+void scene_move_oil_pump(Object *pump) {
+	mat4_set_yaw_pitch_roll(&pump->mat, vec3(sin(system_cycle_time() * 0.125 * M_PI * 2), 0, 0));
+}
+
+void scene_init_aurora_borealis() {
+	aurora_borealis.enabled = true;
+	clear(aurora_borealis.grey_coords);
+
+	int count = 0;
+	int16_t *coords;
+	float y;
+
+	Prm poly = {.primitive = sky_object->primitives};
+	for (int i = 0; i < sky_object->primitives_len; i++) {
+		switch (poly.primitive->type) {
+		case PRM_TYPE_GT3:
+			poly.gt3 += 1;
+			break;
+		case PRM_TYPE_GT4:
+			coords = poly.gt4->coords;
+			y = sky_object->vertices[coords[0]].y;
+			if (y < -6000) { // -8000
+				aurora_borealis.primitives[count] = poly.gt4;
+				if (y > -6800) {
+					aurora_borealis.coords[count] = poly.gt4->coords;
+					aurora_borealis.grey_coords[count] = -1;
+				}
+				else if (y < -11000) {
+					aurora_borealis.coords[count] = poly.gt4->coords;
+					aurora_borealis.grey_coords[count] = -2;
+				}
+				else {
+					aurora_borealis.coords[count] = poly.gt4->coords;
+				}
+				count++;
+			}
+			poly.gt4 += 1;
+			break;
+		}
+	}
+}
+
+void scene_update_aurora_borealis() {
+	float phase = system_time() / 30.0;
+	for (int i = 0; i < 80; i++) {
+		int16_t *coords = aurora_borealis.coords[i];
+
+		if (aurora_borealis.grey_coords[i] != -2) {
+			aurora_borealis.primitives[i]->colour[0].as_rgba.r = (sin(coords[0] * phase) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[0].as_rgba.g = (sin(coords[0] * (phase + 0.054)) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[0].as_rgba.b = (sin(coords[0] * (phase + 0.039)) * 64.0) + 190;
+		}
+		if (aurora_borealis.grey_coords[i] != -2) {
+			aurora_borealis.primitives[i]->colour[1].as_rgba.r = (sin(coords[1] * phase) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[1].as_rgba.g = (sin(coords[1] * (phase + 0.054)) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[1].as_rgba.b = (sin(coords[1] * (phase + 0.039)) * 64.0) + 190;
+		}
+		if (aurora_borealis.grey_coords[i] != -1) {
+			aurora_borealis.primitives[i]->colour[2].as_rgba.r = (sin(coords[2] * phase) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[2].as_rgba.g = (sin(coords[2] * (phase + 0.054)) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[2].as_rgba.b = (sin(coords[2] * (phase + 0.039)) * 64.0) + 190;
+		}
+
+		if (aurora_borealis.grey_coords[i] != -1) {
+			aurora_borealis.primitives[i]->colour[3].as_rgba.r = (sin(coords[3] * phase) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[3].as_rgba.g = (sin(coords[3] * (phase + 0.054)) * 64.0) + 190;
+			aurora_borealis.primitives[i]->colour[3].as_rgba.b = (sin(coords[3] * (phase + 0.039)) * 64.0) + 190;
+		}
+	}
+}
--- /dev/null
+++ b/src/wipeout/scene.h
@@ -1,0 +1,15 @@
+#ifndef SCENE_H
+#define SCENE_H
+
+#include "image.h"
+#include "camera.h"
+
+void scene_load(const char *path, float sky_y_offset);
+void scene_draw(camera_t *camera);
+void scene_init();
+void scene_set_start_booms(int num_lights);
+void scene_init_aurora_borealis();
+void scene_update();
+void scene_draw();
+
+#endif
--- /dev/null
+++ b/src/wipeout/sfx.c
@@ -1,0 +1,396 @@
+#include "../utils.h"
+#include "../mem.h"
+#include "../platform.h"
+
+#include "sfx.h"
+#include "game.h"
+
+#define QOA_IMPLEMENTATION
+#define QOA_NO_STDIO
+#include "../libs/qoa.h"
+
+typedef struct {
+	int16_t *samples;
+	uint32_t len;
+} sfx_data_t;
+
+typedef struct {
+	qoa_desc qoa;
+	FILE *file;
+
+	uint32_t track_index;
+	uint32_t first_frame_pos;
+
+	uint32_t buffer_len;
+	uint8_t *buffer;
+
+	uint32_t sample_data_pos;
+	uint32_t sample_data_len;
+	short *sample_data;
+	sfx_music_mode_t mode;
+} music_decoder_t;
+
+enum {
+	VAG_REGION_START = 1,
+	VAG_REGION = 2,
+	VAG_REGION_END = 4
+};
+
+static const int32_t vag_tab[5][2] = {
+	{    0,      0}, // {         0.0,          0.0}, << 14
+	{15360,      0}, // { 60.0 / 64.0,          0.0}, << 14
+	{29440, -13312}, // {115.0 / 64.0, -52.0 / 64.0}, << 14
+	{25088, -14080}, // { 98.0 / 64.0, -55.0 / 64.0}, << 14
+	{31232, -15360}, // {122.0 / 64.0, -60.0 / 64.0}, << 14
+};
+
+static sfx_data_t *sources;
+static uint32_t num_sources;
+static sfx_t *nodes;
+static music_decoder_t *music;
+static void (*external_mix_cb)(float *, uint32_t len) = NULL;
+
+void sfx_load() {
+	// Init decode buffer for music
+	uint32_t channels = 2;
+	music = mem_bump(sizeof(music_decoder_t));
+	music->buffer = mem_bump(QOA_FRAME_SIZE(channels, QOA_SLICES_PER_FRAME));
+	music->sample_data = mem_bump(channels * QOA_FRAME_LEN * sizeof(short) * 2);
+	music->qoa.channels = channels;
+	music->mode = SFX_MUSIC_RANDOM;
+	music->file = NULL;
+	music->track_index = -1;
+
+
+	// Load SFX samples
+	nodes = mem_bump(SFX_MAX * sizeof(sfx_t));
+
+	// 16 byte blocks: 2 byte header, 14 bytes with 2x4bit samples each
+	uint32_t vb_size;
+	uint8_t *vb = file_load("wipeout/sound/wipeout.vb", &vb_size);
+	uint32_t num_samples = (vb_size / 16) * 28;
+
+	int16_t *sample_buffer = mem_bump(num_samples * sizeof(int16_t));
+	sources = mem_mark();
+	num_sources = 0;
+
+	uint32_t sample_index = 0;
+	int32_t history[2] = {0, 0};
+	for (int p = 0; p < vb_size;) {
+		uint8_t header = vb[p++];
+		uint8_t flags = vb[p++];
+		uint8_t shift = header & 0x0f;
+		uint8_t predictor = clamp(header >> 4, 0, 4);
+
+		if (flags_is(flags, VAG_REGION_END)) {
+			mem_bump(sizeof(sfx_data_t));
+			sources[num_sources].samples = &sample_buffer[sample_index];
+		}
+
+		for (uint32_t bs = 0; bs < 14; bs++) {
+			int32_t nibbles[2] = {
+				(vb[p] & 0x0f) << 12,
+				(vb[p] & 0xf0) <<  8
+			};
+			p++;
+
+			for (int ni = 0; ni < 2; ni++) {
+				int32_t sample = nibbles[ni];
+				if (sample & 0x8000) {
+					sample |= 0xffff0000;
+				}
+				sample >>= shift;
+				sample += (history[0] * vag_tab[predictor][0] + history[1] * vag_tab[predictor][1]) >> 14;
+				history[1] = history[0];
+				history[0] = sample;
+				sample_buffer[sample_index++] = clamp(sample, -32768, 32767);
+			}
+		}
+
+		if (flags_is(flags, VAG_REGION_START)) {
+			error_if(sources[num_sources].samples == NULL, "VAG_REGION_START without VAG_REGION_END");
+			sources[num_sources].len = &sample_buffer[sample_index] - sources[num_sources].samples;
+			num_sources++;
+		}
+	}
+
+	mem_temp_free(vb);
+	platform_set_audio_mix_cb(sfx_stero_mix);
+}
+
+void sfx_reset() {
+	for (int i = 0; i < SFX_MAX; i++) {
+		if (flags_is(nodes[i].flags, SFX_LOOP)) {
+			flags_set(nodes[i].flags, SFX_NONE);
+		}
+	}
+}
+
+void sfx_unpause() {
+	for (int i = 0; i < SFX_MAX; i++) {
+		if (flags_is(nodes[i].flags, SFX_LOOP_PAUSE)) {
+			flags_rm(nodes[i].flags, SFX_LOOP_PAUSE);
+			flags_add(nodes[i].flags, SFX_PLAY);
+		}
+	}
+}
+
+void sfx_pause() {
+	for (int i = 0; i < SFX_MAX; i++) {
+		if (flags_is(nodes[i].flags, SFX_PLAY | SFX_LOOP)) {
+			flags_rm(nodes[i].flags, SFX_PLAY);
+			flags_add(nodes[i].flags, SFX_LOOP_PAUSE);
+		}
+	}
+}
+
+
+
+// Sound effects
+
+sfx_t *sfx_get_node(sfx_source_t source_index) {
+	error_if(source_index < 0 || source_index > num_sources, "Invalid audio source");
+
+	sfx_t *sfx = NULL;
+	for (int i = 0; i < SFX_MAX; i++) {
+		if (flags_none(nodes[i].flags, SFX_PLAY | SFX_RESERVE)){
+			sfx = &nodes[i];
+			break;
+		}
+	}
+	if (!sfx) {
+		for (int i = 0; i < SFX_MAX; i++) {
+			if (flags_not(nodes[i].flags, SFX_RESERVE)) {
+				sfx = &nodes[i];
+				break;
+			}
+		}
+	}
+
+	error_if(!sfx, "All audio nodes reserved");
+
+	flags_set(sfx->flags, SFX_NONE);
+	sfx->source = source_index;
+	sfx->volume = 1;
+	sfx->current_volume = 1;
+	sfx->pan = 0;
+	sfx->current_pan = 0;
+	sfx->position = 0;
+
+	// Set default pitch. All voice samples are 44khz, 
+	// other effects 22khz
+	sfx->pitch = source_index >= SFX_VOICE_MINES ? 1.0 : 0.5;
+
+	return sfx;
+}
+
+sfx_t *sfx_play(sfx_source_t source_index) {
+	sfx_t *sfx = sfx_get_node(source_index);
+	flags_set(sfx->flags, SFX_PLAY);
+	return sfx;
+}
+
+sfx_t *sfx_play_at(sfx_source_t source_index, vec3_t pos, vec3_t vel, float volume) {
+	sfx_t *sfx = sfx_get_node(source_index);
+	sfx_set_position(sfx, pos, vel, volume);
+	if (sfx->volume > 0) {
+		flags_set(sfx->flags, SFX_PLAY);
+	}
+	return sfx;
+}
+
+sfx_t *sfx_reserve_loop(sfx_source_t source_index) {
+	sfx_t *sfx = sfx_get_node(source_index);
+	flags_set(sfx->flags, SFX_RESERVE | SFX_LOOP | SFX_PLAY);
+	sfx->volume = 0;
+	sfx->current_volume = 0;
+	sfx->current_pan = 0;
+	sfx->position = rand_float(0, sources[source_index].len);
+	return sfx;
+}
+
+void sfx_set_position(sfx_t *sfx, vec3_t pos, vec3_t vel, float volume) {
+	vec3_t relative_position = vec3_sub(g.camera.position, pos);
+	vec3_t relative_velocity = vec3_sub(g.camera.real_velocity, vel);
+	float distance = vec3_len(relative_position);
+
+	sfx->volume = clamp(scale(distance, 512, 32768, 1, 0), 0, 1) * volume;
+	sfx->pan = -sin(atan2(g.camera.position.x - pos.x, g.camera.position.z - pos.z)+g.camera.angle.y);
+
+	// Doppler effect
+	float away = vec3_dot(relative_velocity, relative_position) / distance;
+	sfx->pitch = (262144.0 - away) / 524288.0;
+}
+
+
+
+
+// Music
+
+uint32_t sfx_music_decode_frame() {
+	if (!music->file) {
+		return 0;
+	}
+	music->buffer_len = fread(music->buffer, 1, qoa_max_frame_size(&music->qoa), music->file);
+
+	uint32_t frame_len;
+	qoa_decode_frame(music->buffer, music->buffer_len, &music->qoa, music->sample_data, &frame_len);
+	music->sample_data_pos = 0;
+	music->sample_data_len = frame_len;
+	return frame_len;
+}
+
+void sfx_music_rewind() {
+	fseek(music->file, music->first_frame_pos, SEEK_SET);
+	music->sample_data_len = 0;
+	music->sample_data_pos = 0;
+}
+
+void sfx_music_open(char *path) {
+	if (music->file) {
+		fclose(music->file);
+		music->file = NULL;
+	}
+
+	FILE *file = fopen(path, "rb");
+	if (!file) {
+		return;
+	}
+
+	uint8_t header[QOA_MIN_FILESIZE];
+	int read = fread(header, QOA_MIN_FILESIZE, 1, file);
+	if (!read) {
+		fclose(file);
+		return;
+	}
+
+	qoa_desc qoa;
+	uint32_t first_frame_pos = qoa_decode_header(header, QOA_MIN_FILESIZE, &qoa);
+	if (!first_frame_pos) {
+		fclose(file);
+		return;
+	}
+
+	fseek(file, first_frame_pos, SEEK_SET);
+
+	if (qoa.channels != music->qoa.channels) {
+		fclose(file);
+		return;
+	}
+	music->qoa = qoa;
+	music->first_frame_pos = first_frame_pos;
+	music->file = file;
+	music->sample_data_len = 0;
+	music->sample_data_pos = 0;
+}
+
+void sfx_music_play(uint32_t index) {
+	error_if(index > len(def.music), "Invalid music index");
+	if (index == music->track_index) {
+		return;
+	}
+
+	printf("open music track %d\n", index);
+
+	music->track_index = index;
+	sfx_music_open(def.music[index].path);
+}
+
+void sfx_music_mode(sfx_music_mode_t mode) {
+	music->mode = mode;
+}
+
+
+
+
+
+// Mixing
+
+void sfx_set_external_mix_cb(void (*cb)(float *, uint32_t len)) {
+	external_mix_cb = cb;
+}
+
+void sfx_stero_mix(float *buffer, uint32_t len) {
+	if (external_mix_cb) {
+		external_mix_cb(buffer, len);
+		return;
+	}
+
+	// Find currently active nodes: those that play and have volume > 0
+	sfx_t *active_nodes[SFX_MAX_ACTIVE];
+	int active_nodes_len = 0;
+	int total_on = 0;
+	for (int n = 0; n < SFX_MAX; n++) {
+		sfx_t *sfx = &nodes[n];
+		if (flags_is(sfx->flags, SFX_PLAY) && (sfx->volume > 0 || sfx->current_volume > 0.01)) {
+			active_nodes[active_nodes_len++] = sfx;
+			if (active_nodes_len >= SFX_MAX_ACTIVE) {
+				break;
+			}
+		}
+		if (flags_is(sfx->flags, SFX_PLAY)) {
+			total_on++;
+		}
+	}
+
+	uint32_t music_src_index = music->sample_data_pos * music->qoa.channels;
+
+	for (int i = 0; i < len; i += 2) {
+		float left = 0;
+		float right = 0;
+
+		// Fill buffer with all active nodes
+		for (int n = 0; n < active_nodes_len; n++) {
+			sfx_t *sfx = active_nodes[n];
+			if (flags_not(sfx->flags, SFX_PLAY)) {
+				continue;
+			}
+
+			sfx->current_volume = sfx->current_volume * 0.999 + sfx->volume * 0.001;
+			sfx->current_pan = sfx->current_pan * 0.999 + sfx->pan * 0.001;
+
+			sfx_data_t *source = &sources[sfx->source];
+			float sample = (float)source->samples[(int)sfx->position] / 32768.0;
+			left += sample * sfx->current_volume * clamp(1.0 - sfx->current_pan, 0, 1);
+			right += sample * sfx->current_volume * clamp(1.0 + sfx->current_pan, 0, 1);
+
+			sfx->position += sfx->pitch;
+			if (sfx->position >= source->len) {
+				if (flags_is(sfx->flags, SFX_LOOP)) {
+					sfx->position = fmod(sfx->position, source->len);
+				}
+				else {
+					flags_rm(sfx->flags, SFX_PLAY);
+				}
+			}
+		}
+
+		left *= save.sfx_volume;
+		right *= save.sfx_volume;
+
+		// Mix in music
+		if (music->mode != SFX_MUSIC_PAUSED && music->file) {
+			if (music->sample_data_len - music->sample_data_pos == 0) {
+				if (!sfx_music_decode_frame()) {
+					if (music->mode == SFX_MUSIC_RANDOM) {
+						sfx_music_play(rand_int(0, len(def.music)));
+					}
+					else if (music->mode == SFX_MUSIC_SEQUENTIAL) {
+						sfx_music_play((music->track_index + 1) % len(def.music));
+					}
+					else if (music->mode == SFX_MUSIC_LOOP) {
+						sfx_music_rewind();
+					}
+					sfx_music_decode_frame();
+				}
+				music_src_index = 0;
+			}
+			left += (music->sample_data[music_src_index++] / 32768.0) * save.music_volume;
+			right += (music->sample_data[music_src_index++] / 32768.0) * save.music_volume;
+			music->sample_data_pos++;
+		}
+
+		buffer[i+0] = left;
+		buffer[i+1] = right;
+	}
+}
--- /dev/null
+++ b/src/wipeout/sfx.h
@@ -1,0 +1,85 @@
+#ifndef SFX_H
+#define SFX_H
+
+#include "../types.h"
+
+typedef enum {
+	SFX_CRUNCH,
+	SFX_EBOLT,
+	SFX_ENGINE_INTAKE,
+	SFX_ENGINE_RUMBLE,
+	SFX_ENGINE_THRUST,
+	SFX_EXPLOSION_1,
+	SFX_EXPLOSION_2,
+	SFX_IMPACT,
+	SFX_MENU_MOVE,
+	SFX_MENU_SELECT,
+	SFX_MENU_TRANSITION,
+	SFX_MINE_DROP,
+	SFX_MISSILE_FIRE,
+	SFX_ENGINE_REMOTE,
+	SFX_POWERUP,
+	SFX_SHIELD,
+	SFX_SIREN,
+	SFX_TRACTOR,
+	SFX_TURBULENCE,
+	SFX_CROWD,
+	SFX_VOICE_MINES,
+	SFX_VOICE_MISSILE,
+	SFX_VOICE_ROCKETS,
+	SFX_VOICE_REVCON,
+	SFX_VOICE_SHOCKWAVE,
+	SFX_VOICE_SPECIAL,
+	SFX_VOICE_COUNT_3,
+	SFX_VOICE_COUNT_2,
+	SFX_VOICE_COUNT_1,
+	SFX_VOICE_COUNT_GO,
+} sfx_source_t;
+
+typedef enum {
+	SFX_NONE       = 0,
+	SFX_PLAY       = (1<<0),
+	SFX_RESERVE    = (1<<1),
+	SFX_LOOP       = (1<<2),
+	SFX_LOOP_PAUSE = (1<<3),
+} sfx_flags_t;
+
+typedef struct {
+	sfx_source_t source;
+	sfx_flags_t flags;
+	float pan;
+	float current_pan;
+	float volume;
+	float current_volume;
+	float pitch;
+	float position;
+} sfx_t;
+
+#define SFX_MAX 64
+#define SFX_MAX_ACTIVE 16
+
+void sfx_load();
+void sfx_stero_mix(float *buffer, uint32_t len);
+void sfx_set_external_mix_cb(void (*cb)(float *, uint32_t len));
+void sfx_reset();
+void sfx_pause();
+void sfx_unpause();
+
+sfx_t *sfx_play(sfx_source_t source_index);
+sfx_t *sfx_play_at(sfx_source_t source_index, vec3_t pos, vec3_t vel, float volume);
+sfx_t *sfx_reserve_loop(sfx_source_t source_index);
+void sfx_set_position(sfx_t *sfx, vec3_t pos, vec3_t vel, float volume);
+
+typedef enum {
+	SFX_MUSIC_PAUSED,
+	SFX_MUSIC_RANDOM,
+	SFX_MUSIC_SEQUENTIAL,
+	SFX_MUSIC_LOOP
+} sfx_music_mode_t;
+
+void sfx_music_next();
+void sfx_music_play(uint32_t index);
+void sfx_music_mode(sfx_music_mode_t);
+void sfx_music_pause();
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/ship.c
@@ -1,0 +1,1006 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+
+#include "object.h"
+#include "scene.h"
+#include "track.h"
+#include "weapon.h"
+#include "camera.h"
+#include "image.h"
+#include "object.h"
+#include "ship.h"
+#include "ship_ai.h"
+#include "ship_player.h"
+#include "game.h"
+#include "race.h"
+#include "sfx.h"
+
+void ships_load() {
+	texture_list_t ship_textures = image_get_compressed_textures("wipeout/common/allsh.cmp");
+	Object *ship_models = objects_load("wipeout/common/allsh.prm", ship_textures);
+
+	texture_list_t collision_textures = image_get_compressed_textures("wipeout/common/alcol.cmp");
+	Object *collision_models = objects_load("wipeout/common/alcol.prm", collision_textures);
+
+	int object_index;
+	Object *ship_model = ship_models;
+	Object *collision_model = collision_models;
+
+	for (object_index = 0; object_index < len(g.ships) && ship_model && collision_model; object_index++) {
+		int ship_index = def.ship_model_to_pilot[object_index];
+		g.ships[ship_index].model = ship_model;
+		g.ships[ship_index].collision_model = collision_model;
+
+		ship_model = ship_model->next;
+		collision_model = collision_model->next;
+
+		ship_init_exhaust_plume(&g.ships[ship_index]);
+	}
+
+	error_if(object_index != len(g.ships), "Expected %ld ship models, got %d", len(g.ships), object_index);
+
+	uint16_t shadow_textures_start = render_textures_len();
+	image_get_texture_semi_trans("wipeout/textures/shad1.tim");
+	image_get_texture_semi_trans("wipeout/textures/shad2.tim");
+	image_get_texture_semi_trans("wipeout/textures/shad3.tim");
+	image_get_texture_semi_trans("wipeout/textures/shad4.tim");
+
+	for (int i = 0; i < len(g.ships); i++) {
+		g.ships[i].shadow_texture = shadow_textures_start + (i >> 1);
+	}
+}
+
+
+void ships_init(section_t *section) {
+	section_t *start_sections[len(g.ships)];
+
+	int ranks_to_pilots[NUM_PILOTS];
+
+	// Initialize ranks with all pilots in order
+	for (int i = 0; i < len(g.ships); i++) {
+		ranks_to_pilots[i] = i;
+	}
+
+	// Randomize order for single race or new championship
+	if (g.race_type != RACE_TYPE_CHAMPIONSHIP || g.circut == CIRCUT_ALTIMA_VII) {
+		shuffle(ranks_to_pilots, len(ranks_to_pilots));
+	}
+
+	// Randomize some tiers in an ongoing championship
+	else if (g.race_type == RACE_TYPE_CHAMPIONSHIP) {
+		// Initialize with current championship order
+		for (int i = 0; i < len(g.ships); i++) {
+			ranks_to_pilots[i] = g.championship_ranks[i].pilot;
+		}		
+		shuffle(ranks_to_pilots, 2); // shuffle 0..1
+		shuffle(ranks_to_pilots + 4, len(ranks_to_pilots)-5); // shuffle 4..len-1
+	}
+
+	// player is always last
+	for (int i = 0; i < len(ranks_to_pilots)-1; i++) {
+		if (ranks_to_pilots[i] == g.pilot) {
+			swap(ranks_to_pilots[i], ranks_to_pilots[i+1]);
+		}
+	}
+
+
+	int start_line_pos = def.circuts[g.circut].settings[g.race_class].start_line_pos;
+	for (int i = 0; i < start_line_pos - 15; i++) {
+		section = section->next;
+	}
+	for (int i = 0; i < len(g.ships); i++) {
+		start_sections[i] = section;
+		section = section->next;
+		if ((i % 2) == 0) {
+			section = section->next;
+		}
+	}
+
+	for (int i = 0; i < len(ranks_to_pilots); i++) {
+		int rank_inv = (len(g.ships)-1) - i;
+		int pilot = ranks_to_pilots[i];
+		ship_init(&g.ships[pilot], start_sections[rank_inv], pilot, rank_inv);
+	}
+}
+
+static inline bool sort_rank_compare(pilot_points_t *pa, pilot_points_t *pb) {
+	ship_t *a = &g.ships[pa->pilot];
+	ship_t *b = &g.ships[pb->pilot];
+	if (a->total_section_num == b->total_section_num) {
+		vec3_t c0 = a->section->center;
+		vec3_t c1 = a->section->next->center;
+		vec3_t dir = vec3_sub(c1, c0);
+		float pos_a = vec3_dot(vec3_sub(a->position, c0), dir);
+		float pos_b = vec3_dot(vec3_sub(b->position, c0), dir);
+		return (pos_a < pos_b);
+	}
+	else {
+		return a->total_section_num < b->total_section_num;
+	}
+}
+
+void ships_update() {
+	if (g.race_type == RACE_TYPE_TIME_TRIAL) {
+		ship_update(&g.ships[g.pilot]);
+	}
+	else {
+		for (int i = 0; i < len(g.ships); i++) {
+			ship_update(&g.ships[i]);
+		}
+		for (int j = 0; j < (len(g.ships) - 1); j++) {
+			for (int i = j + 1; i < len(g.ships); i++) {
+				ship_collide_with_ship(&g.ships[i], &g.ships[j]);
+			}
+		}
+
+		if (flags_is(g.ships[g.pilot].flags, SHIP_RACING)) {
+			sort(g.race_ranks, len(g.race_ranks), sort_rank_compare);
+			for (int32_t i = 0; i < len(g.ships); i++) {
+				g.ships[g.race_ranks[i].pilot].position_rank = i + 1;
+			}
+		}
+	}
+}
+
+
+
+void ships_draw() {
+	// Ship models
+	for (int i = 0; i < len(g.ships); i++) {
+		if (
+			flags_is(g.ships[i].flags, SHIP_VIEW_INTERNAL) ||
+			(g.race_type == RACE_TYPE_TIME_TRIAL && i != g.pilot)
+		) {
+			continue;
+		}
+
+		ship_draw(&g.ships[i]);
+	}
+
+
+	// Shadows
+	render_set_model_mat(&mat4_identity());
+
+	render_set_depth_write(false);
+	render_set_depth_offset(-32.0);
+
+	for (int i = 0; i < len(g.ships); i++) {
+		if (
+			(g.race_type == RACE_TYPE_TIME_TRIAL && i != g.pilot) ||
+			flags_not(g.ships[i].flags, SHIP_VISIBLE) || 
+			flags_is(g.ships[i].flags, SHIP_FLYING)
+		) {
+			continue;
+		}
+
+		ship_draw_shadow(&g.ships[i]);
+	}
+
+	render_set_depth_offset(0.0);
+	render_set_depth_write(true);
+}
+
+
+
+
+
+
+
+void ship_init(ship_t *self, section_t *section, int pilot, int inv_start_rank) {
+	self->pilot = pilot;
+	self->velocity = vec3(0, 0, 0);
+	self->acceleration = vec3(0, 0, 0);
+	self->angle = vec3(0, 0, 0);
+	self->angular_velocity = vec3(0, 0, 0);
+	self->turn_rate = 0;
+	self->thrust_mag = 0;
+	self->current_thrust_max = 0;
+	self->turn_rate_from_hit = 0;
+	self->brake_right = 0;
+	self->brake_left = 0;
+	self->flags = SHIP_RACING | SHIP_VISIBLE | SHIP_DIRECTION_FORWARD;
+	self->weapon_type = WEAPON_TYPE_NONE;
+	self->lap = -1;
+	self->max_lap = -1;
+	self->speed = 0;
+	self->ebolt_timer = 0;
+	self->revcon_timer = 0;
+	self->special_timer = 0;
+	self->mat = mat4_identity();
+
+	self->update_timer = 0;
+	self->last_impact_time = 0;
+
+	int team = def.pilots[pilot].team;
+	self->mass =          def.teams[team].attributes[g.race_class].mass;
+	self->thrust_max =    def.teams[team].attributes[g.race_class].thrust_max;
+	self->skid =          def.teams[team].attributes[g.race_class].skid;
+	self->turn_rate =     def.teams[team].attributes[g.race_class].turn_rate;
+	self->turn_rate_max = def.teams[team].attributes[g.race_class].turn_rate_max;
+	self->resistance =    def.teams[team].attributes[g.race_class].resistance;
+	self->lap_time = 0;
+
+	self->update_timer = UPDATE_TIME_INITIAL;
+	self->position_rank = NUM_PILOTS - inv_start_rank;
+
+	if (pilot == g.pilot) {
+		self->update_func = ship_player_update_intro;
+		self->remote_thrust_max = 2900;
+		self->remote_thrust_mag = 46;
+		self->fight_back = 0;
+	}
+	else {
+		self->update_func = ship_ai_update_intro;
+		self->remote_thrust_max = def.ai_settings[g.race_class][inv_start_rank-1].thrust_max;
+		self->remote_thrust_mag = def.ai_settings[g.race_class][inv_start_rank-1].thrust_magnitude;
+		self->fight_back = def.ai_settings[g.race_class][inv_start_rank-1].fight_back;
+	}
+
+	self->section = section;
+	self->prev_section = section;
+	float spread_base = def.circuts[g.circut].settings[g.race_class].spread_base;
+	float spread_factor = def.circuts[g.circut].settings[g.race_class].spread_factor;
+	int p = inv_start_rank - 1;
+	self->start_accelerate_timer = p * (spread_base + (p * spread_factor)) * (1.0/30.0);
+
+	track_face_t *face = g.track.faces + section->face_start;
+	face++;
+	if ((inv_start_rank % 2) != 0) {
+		face++;
+	}
+	
+	vec3_t face_point = vec3_mulf(vec3_add(face->tris[0].vertices[0].pos, face->tris[0].vertices[2].pos), 0.5);
+	self->position = vec3_add(face_point, vec3_mulf(face->normal, 200));
+
+	self->section_num = section->num;
+	self->prev_section_num = section->num;
+	self->total_section_num = section->num;
+
+	section_t *next = section->next;
+	vec3_t direction = vec3_sub(next->center, section->center);
+	self->angle.y = -atan2(direction.x, direction.z);
+}
+
+void ship_init_exhaust_plume(ship_t *self) {
+	int16_t indices[64];
+	int16_t indices_len = 0;
+
+	Prm prm = {.primitive = self->model->primitives};
+
+	for (int i = 0; i < self->model->primitives_len; i++) {
+		switch (prm.f3->type) {
+		case PRM_TYPE_F3 :
+			if (flags_is(prm.f3->flag, PRM_SHIP_ENGINE)) {
+				die("F3 ::SE marked polys should be ft3's");
+			}
+			prm.f3 += 1;
+			break;
+
+		case PRM_TYPE_F4 :
+			if (flags_is(prm.f4->flag, PRM_SHIP_ENGINE)) {
+				die("F4 ::SE marked polys should be ft3's");
+			}
+			prm.f4 += 1;
+			break;
+
+		case PRM_TYPE_FT3 :
+			if (flags_is(prm.ft3->flag, PRM_SHIP_ENGINE)) {
+				indices[indices_len++] = prm.ft3->coords[0];
+				indices[indices_len++] = prm.ft3->coords[1];
+				indices[indices_len++] = prm.ft3->coords[2];
+
+				flags_add(prm.ft3->flag, PRM_TRANSLUCENT);
+				prm.ft3->colour.as_rgba.r = 180;
+				prm.ft3->colour.as_rgba.g = 97 ;
+				prm.ft3->colour.as_rgba.b = 120;
+				prm.ft3->colour.as_rgba.a = 140;
+			}
+			prm.ft3 += 1;
+			break;
+
+		case PRM_TYPE_FT4 :
+			if (flags_is(prm.ft4->flag, PRM_SHIP_ENGINE)) {
+				die("FT4 ::SE marked polys should be ft3's");
+			}
+			prm.ft4 += 1;
+			break;
+
+		case PRM_TYPE_G3 :
+			if (flags_is(prm.g3->flag, PRM_SHIP_ENGINE)) {
+				die("G3 ::SE marked polys should be ft3's");
+			}
+			prm.g3 += 1;
+			break;
+
+		case PRM_TYPE_G4 :
+			if (flags_is(prm.g4->flag, PRM_SHIP_ENGINE)) {
+				die("G4 ::SE marked polys should be ft3's");
+			}
+			prm.g4 += 1;
+			break;
+
+		case PRM_TYPE_GT3 :
+			if (flags_is(prm.gt3->flag, PRM_SHIP_ENGINE)) {
+				indices[indices_len++] = prm.gt3->coords[0];
+				indices[indices_len++] = prm.gt3->coords[1];
+				indices[indices_len++] = prm.gt3->coords[2];
+
+				flags_add(prm.gt3->flag, PRM_TRANSLUCENT);
+				for (int j = 0; j < 3; j++) {
+					prm.gt3->colour[j].as_rgba.r = 180;
+					prm.gt3->colour[j].as_rgba.g = 97 ;
+					prm.gt3->colour[j].as_rgba.b = 120;
+					prm.gt3->colour[j].as_rgba.a = 140;
+				}
+			}
+			prm.gt3 += 1;
+			break;
+
+		case PRM_TYPE_GT4 :
+			if (flags_is(prm.gt4->flag, PRM_SHIP_ENGINE)) {
+				die("GT4 ::SE marked polys should be ft3's");
+			}
+			prm.gt4 += 1;
+			break;
+
+		default :
+			die("cone.c::InitCone:Bad primitive type %x\n", prm.f3->type);
+			break;
+		}
+	}
+
+
+	// get out the center vertex
+
+	self->exhaust_plume[0].v = NULL;
+	self->exhaust_plume[1].v = NULL;
+	self->exhaust_plume[2].v = NULL;
+
+	int shared[3] = {-1, -1, -1};
+	int booster = 0;
+	for (int i = 0; (i < indices_len) && (booster < 3); i++) {
+		int similar = 0;
+		for (int j = 0; j < indices_len; j++) {
+			if (indices[i] == indices[j]) {
+				similar++;
+				if (similar > 3) {
+					int found = 0;
+					for (int k = 0; k < 3; k++) {
+						if (shared[k] == indices[i]) {
+							found = 1;
+						}
+					}
+
+					if (!found) {
+						shared[booster] = indices[i];
+						booster++;
+					}
+				}
+			}
+		}
+	}
+
+	for (int j = 0; j < 3; j++) {
+		if (shared[j] != -1) {
+			self->exhaust_plume[j].v = &self->model->vertices[shared[j]];
+			self->exhaust_plume[j].initial = self->model->vertices[shared[j]];
+		}
+	}
+}
+
+
+void ship_draw(ship_t *self) {
+	object_draw(self->model, &self->mat);
+}
+
+void ship_draw_shadow(ship_t *self) {	
+	track_face_t *face = track_section_get_base_face(self->section);
+
+	vec3_t face_point = face->tris[0].vertices[0].pos;
+	vec3_t nose = vec3_add(self->position, vec3_mulf(self->dir_forward, 384));
+	vec3_t wngl = vec3_sub(vec3_sub(self->position, vec3_mulf(self->dir_right, 256)), vec3_mulf(self->dir_forward, 384));
+	vec3_t wngr = vec3_sub(vec3_add(self->position, vec3_mulf(self->dir_right, 256)), vec3_mulf(self->dir_forward, 384));
+
+	nose = vec3_sub(nose, vec3_mulf(face->normal, vec3_distance_to_plane(nose, face_point, face->normal)));
+	wngl = vec3_sub(wngl, vec3_mulf(face->normal, vec3_distance_to_plane(wngl, face_point, face->normal)));
+	wngr = vec3_sub(wngr, vec3_mulf(face->normal, vec3_distance_to_plane(wngr, face_point, face->normal)));
+	
+	rgba_t color = rgba(0 , 0 , 0, 128);
+	render_push_tris((tris_t) {
+		.vertices = {
+			{
+				.pos = {wngl.x, wngl.y, wngl.z},
+				.uv = {0, 256},
+				.color = color,
+			},
+			{
+				.pos = {wngr.x, wngr.y, wngr.z},
+				.uv = {128, 256},
+				.color = color
+			},
+			{
+				.pos = {nose.x, nose.y, nose.z},
+				.uv = {64, 0},
+				.color = color
+			},
+		}
+	}, self->shadow_texture);
+}
+
+void ship_update(ship_t *self) {
+
+	// Set Unit vectors of this ship
+	float sx = sin(self->angle.x);
+	float cx = cos(self->angle.x);
+	float sy = sin(self->angle.y);
+	float cy = cos(self->angle.y);
+	float sz = sin(self->angle.z);
+	float cz = cos(self->angle.z);
+
+	self->dir_forward.x = -(sy * cx);
+	self->dir_forward.y = - sx;
+	self->dir_forward.z =  (cy * cx);
+
+	self->dir_right.x =  (cy * cz) + (sy * sz * sx);
+	self->dir_right.y = -(sz * cx);
+	self->dir_right.z =  (sy * cz) - (cy * sx * sz);
+
+	self->dir_up.x = (cy * sz) - (sy * sx * cz);
+	self->dir_up.y = -(cx * cz);
+	self->dir_up.z = (sy * sz) + (cy * sx * cz);
+
+	self->prev_section = self->section;
+	float distance;
+	self->section = track_nearest_section(self->position, self->section, &distance);
+	if (distance > 3700) {
+		flags_add(self->flags, SHIP_FLYING);
+	}
+	else {
+		flags_rm(self->flags, SHIP_FLYING);
+	}
+
+	self->prev_section_num = self->prev_section->num;
+	self->section_num = self->section->num;
+
+
+	// Figure out which side of the track the ship is on
+	track_face_t *face = track_section_get_base_face(self->section);
+
+	vec3_t to_face_vector = vec3_sub(
+		face->tris[0].vertices[0].pos,
+		face->tris[0].vertices[1].pos
+	);
+
+	vec3_t direction = vec3_sub(self->section->center, self->position);
+
+	if (vec3_dot(direction, to_face_vector) > 0) {
+		flags_add(self->flags, SHIP_LEFT_SIDE);
+	}
+	else {
+		flags_rm(self->flags, SHIP_LEFT_SIDE);
+		face++;
+	}
+
+	// Collect powerup
+	if (
+		flags_is(face->flags, FACE_PICKUP_ACTIVE) &&
+		flags_not(self->flags, SHIP_SPECIALED) &&
+		self->weapon_type == WEAPON_TYPE_NONE &&
+		track_collect_pickups(face)
+	) {
+		if (self->pilot == g.pilot) {
+			sfx_play(SFX_POWERUP);
+			if (flags_is(self->flags, SHIP_SHIELDED)) {
+				self->weapon_type = weapon_get_random_type(WEAPON_CLASS_PROJECTILE);
+			}
+			else {
+				self->weapon_type = weapon_get_random_type(WEAPON_CLASS_ANY);
+			}
+		}
+		else {
+			self->weapon_type = 1;
+		}
+	}
+
+	self->last_impact_time += system_tick();
+	
+	// Call the active player/ai update function
+	(self->update_func)(self);
+
+
+	// Animate the exhaust plume
+
+	int exhaust_len;
+
+	if (self->pilot == g.pilot) {
+		// get the z exhaust_len related to speed or thrust
+		exhaust_len = self->thrust_mag * 0.0625;
+		exhaust_len += self->speed * 0.00390625;
+	}
+	else {
+		// for remote ships the z exhaust_len is a constant
+		exhaust_len = 150;
+	}
+
+	for (int i = 0; i < 3; i++) {
+		if (self->exhaust_plume[i].v != NULL) {
+			self->exhaust_plume[i].v->z = self->exhaust_plume[i].initial.z - exhaust_len + (rand_int(-16383, 16383) >> 9);
+			self->exhaust_plume[i].v->x = self->exhaust_plume[i].initial.x + (rand_int(-16383, 16383) >> 11);
+			self->exhaust_plume[i].v->y = self->exhaust_plume[i].initial.y + (rand_int(-16383, 16383) >> 11);
+		}
+	}
+
+	mat4_set_translation(&self->mat, self->position);
+	mat4_set_yaw_pitch_roll(&self->mat, self->angle);
+
+
+
+	// Race position and lap times
+	
+	self->lap_time += system_tick();
+
+	int start_line_pos = def.circuts[g.circut].settings[g.race_class].start_line_pos;
+
+	// Crossed line backwards
+	if (self->prev_section_num == start_line_pos + 1 && self->section_num <= start_line_pos) {
+		self->lap--;
+	}
+
+	// Crossed line forwards
+	else if (self->prev_section_num == start_line_pos && self->section_num > start_line_pos) {
+		self->lap++;
+
+		// Is it the first time we're crossing the line for this lap?
+		if (self->lap > self->max_lap) {
+			self->max_lap = self->lap;
+
+			if (self->lap > 0 && self->lap <= NUM_LAPS) {
+				g.lap_times[self->pilot][self->lap-1] = self->lap_time;
+			}
+			self->lap_time = 0;
+
+			if (g.race_type == RACE_TYPE_TIME_TRIAL) {
+				self->weapon_type = WEAPON_TYPE_TURBO;
+			}
+
+			if (self->lap == NUM_LAPS && self->pilot == g.pilot) {
+				race_end();
+			}
+		}
+	}
+
+	int section_num_from_line = self->section_num - (start_line_pos + 1);
+	if (section_num_from_line < 0) {
+		section_num_from_line += g.track.section_count;
+	}
+	self->total_section_num = self->lap * g.track.section_count + section_num_from_line;
+}
+
+vec3_t ship_cockpit(ship_t *self) {
+	return vec3_add(self->position, vec3_mulf(self->dir_up, 128));
+}
+
+vec3_t ship_nose(ship_t *self) {
+	return vec3_add(self->position, vec3_mulf(self->dir_forward, 512));
+	
+}
+
+vec3_t ship_wing_left(ship_t *self) {
+	return vec3_sub(vec3_sub(self->position, vec3_mulf(self->dir_right, 256)), vec3_mulf(self->dir_forward, 256));
+}
+
+vec3_t ship_wing_right(ship_t *self) {
+	return vec3_sub(vec3_add(self->position, vec3_mulf(self->dir_right, 256)), vec3_mulf(self->dir_forward, 256));
+}
+
+static bool vec3_is_on_face(vec3_t pos, track_face_t *face, float alpha) {
+	vec3_t plane_point = vec3_sub(pos, vec3_mulf(face->normal, alpha));
+	vec3_t vec0 = vec3_sub(plane_point, face->tris[0].vertices[1].pos);
+	vec3_t vec1 = vec3_sub(plane_point, face->tris[0].vertices[2].pos);
+	vec3_t vec2 = vec3_sub(plane_point, face->tris[0].vertices[0].pos);
+	vec3_t vec3 = vec3_sub(plane_point, face->tris[1].vertices[0].pos);
+
+	float angle = 
+		vec3_angle(vec0, vec2) +
+		vec3_angle(vec2, vec3) +
+		vec3_angle(vec3, vec1) +
+		vec3_angle(vec1, vec0);
+
+	return (angle > M_PI * 2 - 0.01);
+}
+
+void ship_resolve_wing_collision(ship_t *self, track_face_t *face, float direction) {
+	vec3_t collision_vector = vec3_sub(self->section->center, face->tris[0].vertices[2].pos);
+	float angle = vec3_angle(collision_vector, self->dir_forward);
+	self->velocity = vec3_reflect(self->velocity, face->normal, 2);
+	self->position = vec3_sub(self->position, vec3_mulf(self->velocity, 0.015625)); // system_tick?
+	self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.5));
+	self->velocity = vec3_add(self->velocity, vec3_mulf(face->normal, 4096)); // div by 4096?
+
+	float magnitude = (fabsf(angle) * self->speed) * M_PI / (4096 * 16.0); // (6 velocity shift, 12 angle shift?)
+
+	vec3_t wing_pos;
+	if (direction > 0) {
+		self->angular_velocity.z += magnitude;
+		wing_pos = vec3_add(self->position, vec3_mulf(vec3_sub(self->dir_right, self->dir_forward), 256)); // >> 4??
+	}
+	else {
+		self->angular_velocity.z -= magnitude;	
+		wing_pos = vec3_sub(self->position, vec3_mulf(vec3_sub(self->dir_right, self->dir_forward), 256)); // >> 4??
+	}
+
+	if (self->last_impact_time > 0.2) {
+		self->last_impact_time = 0;
+		sfx_play_at(SFX_IMPACT, wing_pos, vec3(0, 0, 0), 1);
+	}
+}
+
+
+void ship_resolve_nose_collision(ship_t *self, track_face_t *face, float direction) {
+	vec3_t collision_vector = vec3_sub(self->section->center, face->tris[0].vertices[2].pos);
+	float angle = vec3_angle(collision_vector, self->dir_forward);
+	self->velocity = vec3_reflect(self->velocity, face->normal, 2);
+	self->position = vec3_sub(self->position, vec3_mulf(self->velocity, 0.015625)); // system_tick?
+	self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.5));
+	self->velocity = vec3_add(self->velocity, vec3_mulf(face->normal, 4096)); // div by 4096?
+
+	float magnitude = ((self->speed * 0.0625) + 400) * M_PI / (4096.0 * 64.0);
+	if (direction > 0) {
+		self->angular_velocity.y += magnitude;
+	}
+	else { 
+		self->angular_velocity.y -= magnitude;
+	}
+
+	if (self->last_impact_time > 0.2) {
+		self->last_impact_time = 0;
+		sfx_play_at(SFX_IMPACT, ship_nose(self), vec3(0, 0, 0), 1);
+	}
+}
+
+
+void ship_collide_with_track(ship_t *self, track_face_t *face) {
+	float alpha;
+	section_t 	*trackPtr;
+	bool collide;
+	track_face_t *face2;
+
+	trackPtr = self->section->next;
+	vec3_t direction = vec3_sub(trackPtr->center, self->section->center);
+	float down_track = vec3_dot(direction, self->dir_forward);
+
+	if (down_track < 0) {
+		flags_rm(self->flags, SHIP_DIRECTION_FORWARD);
+	}
+	else {
+		flags_add(self->flags, SHIP_DIRECTION_FORWARD);
+	}
+
+	vec3_t to_face_vector = vec3_sub(face->tris[0].vertices[0].pos, face->tris[0].vertices[1].pos);
+	direction = vec3_sub(self->section->center, self->position);
+	float to_face = vec3_dot(direction, to_face_vector);
+
+	face--;
+
+	// Check against left hand side of track
+	
+	// FIXME: the collision checks in junctions are very flakey and often select
+	// the wrong face to test for a collision.
+	// Instead of this whole mess here, there should just be a function 
+	// `track_get_nearest_face(section, pos)` that we call with the nose and 
+	// wing positions and then just resolve against this face.
+
+	if (to_face > 0) {
+		flags_add(self->flags, SHIP_LEFT_SIDE);
+		
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+
+		alpha = vec3_distance_to_plane(ship_nose(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (flags_is(self->section->flags, SECTION_JUNCTION_START)) {
+				collide = vec3_is_on_face(ship_nose(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, -down_track);
+				}
+				else {
+					face2 = g.track.faces + self->section->next->face_start;
+					collide = vec3_is_on_face(ship_nose(self), face2, alpha);
+					if (collide) {
+						ship_resolve_nose_collision(self, face, -down_track);
+					}
+				}
+			}
+			else if (flags_is(self->section->flags, SECTION_JUNCTION_END)) {
+				collide = vec3_is_on_face(ship_nose(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, -down_track);
+				}
+				else {
+					face2 = g.track.faces + self->section->prev->face_start;
+					collide = vec3_is_on_face(ship_nose(self), face2, alpha);
+					if (collide) {
+						ship_resolve_nose_collision(self, face, -down_track);
+					}
+				}
+			}
+			else {
+				ship_resolve_nose_collision(self, face, -down_track);
+			}
+			return;
+		}
+
+		alpha = vec3_distance_to_plane(ship_wing_left(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (
+				flags_is(self->section->flags, SECTION_JUNCTION_START) || 
+				flags_is(self->section->flags, SECTION_JUNCTION_END)
+			) {
+				collide = vec3_is_on_face(ship_wing_left(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, -down_track);
+				}
+			}
+			else {
+				ship_resolve_wing_collision(self, face, -down_track);
+			}
+			return;
+		}
+
+		alpha = vec3_distance_to_plane(ship_wing_right(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (
+				flags_is(self->section->flags, SECTION_JUNCTION_START) || 
+				flags_is(self->section->flags, SECTION_JUNCTION_END)
+			) {
+				collide = vec3_is_on_face(ship_wing_right(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, -down_track);
+				}
+			}
+			else {
+				ship_resolve_wing_collision(self, face, -down_track);
+			}
+			return;
+		}
+	}
+
+
+	// Collision check against 2nd wall
+	else {
+		flags_rm(self->flags, SHIP_LEFT_SIDE);
+
+		face++;
+		while (face->flags & FACE_TRACK_BASE) {
+			face++;
+		}
+
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+
+		alpha = vec3_distance_to_plane(ship_nose(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (flags_is(self->section->flags, SECTION_JUNCTION_START)) {
+				collide = vec3_is_on_face(ship_nose(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, down_track);
+				}
+				else {
+					face2 = g.track.faces + self->section->next->face_start;
+					face2 += 3;
+					collide = vec3_is_on_face(ship_nose(self), face2, alpha);
+					if (collide) {
+						ship_resolve_nose_collision(self, face, -down_track);
+					}
+				}
+			}
+			else if (flags_is(self->section->flags, SECTION_JUNCTION_END)) {
+				collide = vec3_is_on_face(ship_nose(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, -down_track);
+				}
+				else {
+					face2 = g.track.faces + self->section->prev->face_start;
+					face2 += 3;
+					collide = vec3_is_on_face(ship_nose(self), face2, alpha);
+					if (collide) {
+						ship_resolve_nose_collision(self, face2, -down_track);
+					}
+				}
+			}
+			else {
+				ship_resolve_nose_collision(self, face, down_track);
+			}
+			return;
+		}
+		
+		alpha = vec3_distance_to_plane(ship_wing_left(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (
+				flags_is(self->section->flags, SECTION_JUNCTION_START) ||
+				flags_is(self->section->flags, SECTION_JUNCTION_END)
+			) {
+				collide = vec3_is_on_face(ship_wing_left(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, down_track);
+				}
+			}
+			else {
+				ship_resolve_wing_collision(self, face, down_track);
+			}
+			return;
+		}
+
+		alpha = vec3_distance_to_plane(ship_wing_right(self), face_point, face->normal);
+		if (alpha <= 0) {
+			if (
+				flags_is(self->section->flags, SECTION_JUNCTION_START) ||
+				flags_is(self->section->flags, SECTION_JUNCTION_END)
+			) {
+				collide = vec3_is_on_face(ship_wing_right(self), face, alpha);
+				if (collide) {
+					ship_resolve_nose_collision(self, face, down_track);
+				}
+			}
+			else {
+				ship_resolve_wing_collision(self, face, down_track);
+			}
+			return;
+		}
+	}
+}
+
+
+bool ship_intersects_ship(ship_t *self, ship_t *other) {
+	// Get 4 points of collision model relative to the
+	// camera
+	vec3_t a = vec3_transform(other->collision_model->vertices[0], &other->mat);
+	vec3_t b = vec3_transform(other->collision_model->vertices[1], &other->mat);
+	vec3_t c = vec3_transform(other->collision_model->vertices[2], &other->mat);
+	vec3_t d = vec3_transform(other->collision_model->vertices[3], &other->mat);
+
+	vec3_t other_points[6] = {b, a, d, a, a, b};
+	vec3_t other_lines[6] = {
+		vec3_sub(c, b),
+		vec3_sub(c, a),
+		vec3_sub(c, d),
+		vec3_sub(b, a),
+		vec3_sub(d, a),
+		vec3_sub(d, b)
+	};
+
+
+	Prm poly = {.primitive = other->collision_model->primitives};
+	int primitives_len = other->collision_model->primitives_len;
+
+	vec3_t p1, p2, p3;
+
+	// for all 4 planes of the enemy ship
+	for (int pi = 0; pi < primitives_len; pi++) {
+		int16_t *indices;
+		switch (poly.primitive->type) {
+			case PRM_TYPE_F3:
+				indices = poly.f3->coords;
+				p1 =  vec3_transform(self->collision_model->vertices[indices[0]], &self->mat);
+				p2 =  vec3_transform(self->collision_model->vertices[indices[1]], &self->mat);
+				p3 =  vec3_transform(self->collision_model->vertices[indices[2]], &self->mat);
+				poly.f3++;
+				break;
+			case PRM_TYPE_G3:
+				indices = poly.g3->coords;
+				p1 =  vec3_transform(self->collision_model->vertices[indices[0]], &self->mat);
+				p2 =  vec3_transform(self->collision_model->vertices[indices[1]], &self->mat);
+				p3 =  vec3_transform(self->collision_model->vertices[indices[2]], &self->mat);
+				poly.g3++;
+				break;
+			case PRM_TYPE_FT3:
+				indices = poly.ft3->coords;
+				p1 =  vec3_transform(self->collision_model->vertices[indices[0]], &self->mat);
+				p2 =  vec3_transform(self->collision_model->vertices[indices[1]], &self->mat);
+				p3 =  vec3_transform(self->collision_model->vertices[indices[2]], &self->mat);
+				poly.ft3++;
+				break;
+			case PRM_TYPE_GT3:
+				indices = poly.gt3->coords;
+				p1 =  vec3_transform(self->collision_model->vertices[indices[0]], &self->mat);
+				p2 =  vec3_transform(self->collision_model->vertices[indices[1]], &self->mat);
+				p3 =  vec3_transform(self->collision_model->vertices[indices[2]], &self->mat);
+				poly.gt3++;
+				break;
+			default:
+				break;
+		}
+
+		// Find polyGon line vectors
+		vec3_t p1p2 = vec3_sub(p2, p1);
+		vec3_t p1p3 = vec3_sub(p3, p1);
+
+		// Find plane equations
+		vec3_t plane1 = vec3_cross(p1p2, p1p3);
+
+		for (int vi = 0; vi < 6; vi++) {
+			float dp1 = vec3_dot(vec3_sub(p1, other_points[vi]), plane1);
+			float dp2 = vec3_dot(other_lines[vi], plane1);
+			
+			if (dp2 != 0) {
+				float norm = dp1 / dp2;
+
+				if ((norm >= 0) && (norm <= 1)) {
+					vec3_t term = vec3_mulf(other_lines[vi], norm);
+					vec3_t res = vec3_add(term, other_points[vi]);
+
+					vec3_t v0 = vec3_sub(p1, res);
+					vec3_t v1 = vec3_sub(p2, res);
+					vec3_t v2 = vec3_sub(p3, res);
+					
+					float angle =
+						vec3_angle(v0, v1) +
+						vec3_angle(v1, v2) +
+						vec3_angle(v2, v0);
+
+					if ((angle >= M_PI * 2 - M_PI * 0.1)) {
+						return true;
+					}
+				}
+			}
+		}
+	}
+	return false;
+}
+
+void ship_collide_with_ship(ship_t *self, ship_t *other) {
+	float distance = vec3_len(vec3_sub(self->position, other->position));
+	
+	// Do a quick distance check; if ships are far apart, remove the collision flag
+	// and early out.
+	if (distance > 960) {
+		flags_rm(self->flags, SHIP_COLL);
+		flags_rm(other->flags, SHIP_COLL);
+		return;
+	}
+
+	// Ships are close, do a real collision test
+	if (!ship_intersects_ship(self, other)) {
+		return;
+	}
+
+	// Ships did collide, resolve
+
+	vec3_t vc = vec3_divf(
+		vec3_add(
+			vec3_mulf(self->velocity, self->mass),
+			vec3_mulf(other->velocity, other->mass)
+		),
+		self->mass + other->mass
+	);
+
+	vec3_t ship_react = vec3_mulf(vec3_sub(vc, self->velocity), 0.25);
+	vec3_t other_react = vec3_mulf(vec3_sub(vc, other->velocity), 0.25);
+	self->position = vec3_sub(self->position, vec3_mulf(self->velocity, 0.015625)); // >> 6
+	other->position = vec3_sub(other->position, vec3_mulf(other->velocity, 0.015625)); // >> 6
+
+	self->velocity = vec3_add(vc, ship_react);
+	other->velocity = vec3_add(vc, other_react);
+
+	vec3_t res = vec3_sub(self->position, other->position);
+
+	self->velocity = vec3_add(self->velocity, vec3_mulf(res, 2));  // << 2
+	self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.015625)); // >> 6
+
+	other->velocity = vec3_sub(other->velocity, vec3_mulf(res, 2)); // << 2
+	other->position = vec3_add(other->position, vec3_mulf(other->velocity, 0.015625)); // >> 6
+
+	if (
+		flags_not(self->flags, SHIP_COLL) && 
+		flags_not(other->flags, SHIP_COLL) &&
+		self->last_impact_time > 0.2
+	) {
+		self->last_impact_time = 0;
+		vec3_t sound_pos = vec3_mulf(vec3_add(self->position, other->position), 0.5);
+		sfx_play_at(SFX_CRUNCH, sound_pos, vec3(0, 0, 0), 1);
+	}
+	flags_add(self->flags, SHIP_COLL);
+	flags_add(other->flags, SHIP_COLL);
+}
+
+
+
--- /dev/null
+++ b/src/wipeout/ship.h
@@ -1,0 +1,168 @@
+#ifndef SHIP_H
+#define SHIP_H
+
+#include "../types.h"
+#include "track.h"
+#include "sfx.h"
+
+#define SHIP_IN_TOW			 	(1<< 0)
+#define SHIP_VIEW_REMOTE	 	(1<< 1)
+#define SHIP_VIEW_INTERNAL		(1<< 2)
+#define SHIP_DIRECTION_FORWARD	(1<< 3)
+#define SHIP_FLYING				(1<< 4)
+#define SHIP_LEFT_SIDE			(1<< 5)
+#define SHIP_RACING				(1<< 6)
+#define SHIP_COLL				(1<< 7)
+#define SHIP_ON_JUNCTION		(1<< 8)
+#define SHIP_VISIBLE			(1<< 9)
+#define SHIP_IN_RESCUE 			(1<<10)
+#define SHIP_OVERTAKEN 			(1<<11)
+#define SHIP_JUST_IN_FRONT	    (1<<12)
+#define SHIP_JUNCTION_LEFT		(1<<13)
+#define SHIP_SHIELDED			(1<<14)
+#define SHIP_ELECTROED			(1<<15)
+#define SHIP_REVCONNED			(1<<16)
+#define SHIP_SPECIALED			(1<<17)
+
+
+// Timings
+
+#define UPDATE_TIME_INITIAL   (200.0 * (1.0/30.0))
+#define UPDATE_TIME_THREE     (150.0 * (1.0/30.0))
+#define UPDATE_TIME_RACE_VIEW (100.0 * (1.0/30.0))
+#define UPDATE_TIME_TWO       (100.0 * (1.0/30.0))
+#define UPDATE_TIME_ONE       ( 50.0 * (1.0/30.0))
+#define UPDATE_TIME_GO        (  0.0 * (1.0/30.0))
+
+
+// Physics conversion
+
+#define FIXED_TO_FLOAT(V) ((V) * (1.0/4096.0))
+#define ANGLE_NORM_TO_RADIAN(V) ((V) * M_PI * 2.0)
+#define NTSC_STEP_TO_RATE_PER_SECOND(V) ((V) * 30.0)
+#define NTSC_ACCELERATION(V) NTSC_STEP_TO_RATE_PER_SECOND(NTSC_STEP_TO_RATE_PER_SECOND(V))
+#define NTSC_VELOCITY(V) NTSC_STEP_TO_RATE_PER_SECOND(V)
+
+#define PITCH_VELOCITY(V) ((V) * (1.0/16.0))
+#define YAW_VELOCITY(V) ((V) * (1.0/64.0))
+#define ROLL_VELOCITY(V) ((V) * (1.0))
+
+#define SHIP_FLYING_GRAVITY   80000.0
+#define SHIP_ON_TRACK_GRAVITY 30000.0
+#define SHIP_MIN_RESISTANCE 	20	 // 12
+#define SHIP_MAX_RESISTANCE 	74
+#define SHIP_VELOCITY_SHIFT 	6
+#define SHIP_TRACK_MAGNET		64	// 64
+#define SHIP_TRACK_FLOAT 	256
+
+#define SHIP_PITCH_ACCEL    NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(PITCH_VELOCITY(30))))
+#define SHIP_THRUST_RATE    NTSC_VELOCITY(16)
+#define SHIP_THRUST_FALLOFF NTSC_VELOCITY(8)
+#define SHIP_BRAKE_RATE     NTSC_VELOCITY(32)
+
+typedef struct ship_t {
+	int16_t pilot;
+	int flags;
+
+	section_t *section, *prev_section;
+
+	vec3_t dir_forward;
+	vec3_t dir_right;
+	vec3_t dir_up;
+
+	vec3_t position;
+	vec3_t velocity;
+	vec3_t acceleration;
+	vec3_t thrust;
+
+	vec3_t angle;
+	vec3_t angular_velocity;
+	vec3_t angular_acceleration;
+
+	vec3_t temp_target; // used for start position and rescue target
+	
+	float turn_rate;
+	float turn_rate_max;
+	float turn_rate_from_hit;
+
+	float mass;
+	float thrust_mag;
+	float thrust_max;
+	float current_thrust_max;
+	float speed;
+	float brake_left;
+	float brake_right;
+
+	float resistance;
+	float skid;
+
+	float remote_thrust_mag;
+	float remote_thrust_max;
+
+	// Remote Ship Attributes
+	int16_t fight_back;
+	float start_accelerate_timer;
+
+	// Weapon Attributes
+	uint8_t weapon_type;
+	struct ship_t *weapon_target;
+
+	float ebolt_timer;
+	float ebolt_effect_timer;
+	float revcon_timer;
+	float special_timer;
+
+	// Race Control Attributes
+	int position_rank;
+	int lap;
+	int max_lap;
+	float lap_time;
+
+	int16_t section_num;
+	int16_t prev_section_num;
+	int16_t total_section_num;
+
+	float update_timer;
+	float last_impact_time;
+
+	mat4_t mat;
+	Object *model;
+	Object *collision_model;
+	uint16_t shadow_texture;
+
+	struct {
+		vec3_t *v;
+		vec3_t initial;
+	} exhaust_plume[3];
+
+	// Control Routines
+	vec3_t (*update_strat_func)(struct ship_t *, track_face_t *);
+	void (*update_func)(struct ship_t *);
+
+	// Audio
+	sfx_t *sfx_engine_thrust;
+	sfx_t *sfx_engine_intake;
+	sfx_t *sfx_turbulence;
+	sfx_t *sfx_shield;
+} ship_t;
+
+void ships_load();
+void ships_init(section_t *section);
+void ships_draw();
+void ships_update();
+
+void ship_init(ship_t *self, section_t *section, int pilot, int position);
+void ship_init_exhaust_plume(ship_t *self);
+void ship_draw(ship_t *self);
+void ship_draw_shadow(ship_t *self);
+void ship_update(ship_t *self);
+void ship_collide_with_track(ship_t *self, track_face_t *face);
+void ship_collide_with_ship(ship_t *self, ship_t *other);
+
+vec3_t ship_cockpit(ship_t *self);
+vec3_t ship_nose(ship_t *self);
+vec3_t ship_wing_left(ship_t *self);
+vec3_t ship_wing_right(ship_t *self);
+
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/ship_ai.c
@@ -1,0 +1,536 @@
+#include "../mem.h"
+#include "../input.h"
+#include "../system.h"
+#include "../utils.h"
+
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "hud.h"
+#include "droid.h"
+#include "camera.h"
+#include "ship_ai.h"
+#include "game.h"
+
+vec3_t ship_ai_strat_hold_center(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_hold_right(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_hold_left(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_block(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_avoid(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_avoid_other(ship_t *self, track_face_t *face);
+vec3_t ship_ai_strat_zig_zag(ship_t *self, track_face_t *face);
+
+void ship_ai_update_intro(ship_t *self) {
+	self->temp_target = self->position;
+	self->update_func = ship_ai_update_intro_await_go;
+
+	self->sfx_engine_thrust = sfx_reserve_loop(SFX_ENGINE_REMOTE);
+	sfx_set_position(self->sfx_engine_thrust, self->position, self->velocity, 0.1);
+}
+
+void ship_ai_update_intro_await_go(ship_t *self) {
+	self->position.y = self->temp_target.y + sin(self->update_timer * (80.0 + self->pilot * 3.0) * 30.0 * M_PI * 2.0 / 4096.0) * 32;
+
+	self->update_timer -= system_tick();
+	if (self->update_timer <= UPDATE_TIME_GO) {
+		self->update_func = ship_ai_update_race;
+	}
+}
+
+vec3_t ship_ai_strat_hold_left(ship_t *self, track_face_t *face) {
+	vec3_t fv1 = face->tris[0].vertices[1].pos;
+	vec3_t fv2 = face->tris[0].vertices[0].pos;
+
+	return vec3_mulf(vec3_sub(fv1, fv2), 0.5);
+}
+
+vec3_t ship_ai_strat_hold_right(ship_t *self, track_face_t *face) {
+	vec3_t fv1 = face->tris[0].vertices[0].pos;
+	vec3_t fv2 = face->tris[0].vertices[1].pos;
+
+	return vec3_mulf(vec3_sub(fv1, fv2), 0.5);
+}
+
+
+vec3_t ship_ai_strat_hold_center(ship_t *self, track_face_t *face) {
+	return vec3(0, 0, 0);
+}
+
+vec3_t ship_ai_strat_block(ship_t *self, track_face_t *face) {
+	if (flags_is(g.ships[g.pilot].flags, SHIP_LEFT_SIDE)) {
+		return ship_ai_strat_hold_left(self, face);
+	}
+	else {
+		return ship_ai_strat_hold_right(self, face);
+	}
+
+}
+
+vec3_t ship_ai_strat_avoid(ship_t *self, track_face_t *face) {
+	if (flags_is(g.ships[g.pilot].flags, SHIP_LEFT_SIDE)) {
+		return ship_ai_strat_hold_right(self, face);
+	}
+	else {
+		return ship_ai_strat_hold_left(self, face);
+	}
+}
+
+
+
+vec3_t ship_ai_strat_avoid_other(ship_t *self, track_face_t *face) {
+	int min_section_num = 100;
+	ship_t *avoid_ship;
+
+	for (int i = 0; i < NUM_PILOTS; i++) {
+		if (i != self->pilot) {
+			int section_diff = g.ships[i].total_section_num - self->total_section_num;
+			if (min_section_num < section_diff) {
+				min_section_num = section_diff;
+				avoid_ship = &g.ships[i];
+			}
+		}
+	}
+
+	if (avoid_ship && min_section_num < 10 && min_section_num > -2) {
+		if (flags_is(avoid_ship->flags, SHIP_LEFT_SIDE)) {
+			return ship_ai_strat_hold_right(self, face);
+		}
+		else {
+			return ship_ai_strat_hold_left(self, face);
+		}
+	}
+	return vec3(0, 0, 0);
+}
+
+
+
+vec3_t ship_ai_strat_zig_zag(ship_t *self, track_face_t *face) {
+	int update_count = (self->update_timer * 30)/50;
+	if (update_count % 2) {
+		return ship_ai_strat_hold_right(self, face);
+	}
+	else {
+		return ship_ai_strat_hold_left(self, face);
+	}
+}
+
+
+void ship_ai_update_race(ship_t *self) {
+	vec3_t offset_vector = vec3(0, 0, 0);
+
+	ship_t *player = &(g.ships[g.pilot]);
+
+	if (self->ebolt_timer > 0) {
+		self->ebolt_timer -= system_tick();
+	}
+	if (self->ebolt_timer <= 0) {
+		flags_rm(self->flags, SHIP_ELECTROED);
+	}
+
+	int behind_speed = def.circuts[g.circut].settings[g.race_class].behind_speed;
+
+
+	if (flags_not(self->flags, SHIP_FLYING)) {
+		// Find First track base section
+		track_face_t *face = track_section_get_base_face(self->section);
+
+		int section_diff = self->total_section_num - player->total_section_num;
+
+		flags_rm(self->flags, SHIP_JUST_IN_FRONT);
+
+		if (self == player) {
+			self->update_strat_func = ship_ai_strat_avoid_other;
+			if (self->remote_thrust_max > self->speed) {
+				self->speed += self->remote_thrust_mag * 30 * system_tick();
+			}
+		}
+		else {
+			// Make global DPA decisions , these will effect the craft in
+			// relation to your race position
+
+			// Accelerate remote ships away at start, start_accelerate_count set in
+			// InitShipData and is an exponential progression
+
+			if (self->start_accelerate_timer > 0) {
+				self->start_accelerate_timer -= system_tick();
+				self->update_timer = 0;
+				self->update_strat_func = ship_ai_strat_avoid;
+				if ((self->remote_thrust_max + 1200) > self->speed) {
+					self->speed += (self->remote_thrust_mag + 150) * 30 * system_tick();
+				}
+			}
+
+
+			// Ship has been left WELL BEHIND; set it to avoid
+			// other ships and update its speed as normal
+
+			else if (section_diff < -10) { // Ship behind, AVOID
+				self->update_timer = 0;
+				self->update_strat_func = ship_ai_strat_avoid;
+
+				// If ship has been well passed, increase its speed to allow
+				// it to make a challenge when the player fouls up
+
+				if (((self->remote_thrust_max + behind_speed) > self->speed)) {
+					self->speed += self->remote_thrust_mag * 30 * system_tick();
+				}
+			}
+
+
+			// Ship is JUST AHEAD
+
+			else if ((section_diff <= 4) && (section_diff > 0)) { // Ship close by, beware does not account for lapped opponents yet
+				flags_add(self->flags, SHIP_JUST_IN_FRONT);
+
+				if (self->update_timer <= 0) { // Make New Decision
+					int chance = rand_int(0, 64); // 12
+
+					self->update_timer = UPDATE_TIME_JUST_FRONT;
+					if (self->fight_back) { // Ship wants to make life difficult
+						if ((chance < 40) || (self->weapon_type == WEAPON_TYPE_NONE)) { // Ship will try to block you
+							self->update_strat_func = ship_ai_strat_block;
+						}
+						else if ((chance >= 40) && (chance < 52)) {	// Ship will attempt to drop mines in your path
+							self->update_strat_func = ship_ai_strat_block;
+							if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
+								sfx_play(SFX_VOICE_MINES);
+								self->weapon_type = WEAPON_TYPE_MINE;
+								weapons_fire_delayed(self, self->weapon_type);
+							}
+						}
+						else if ((chance >= 52) && (chance < 64)) {	// Ship will raise its shield
+							self->update_strat_func = ship_ai_strat_block;
+							if (flags_not(self->flags, SHIP_SHIELDED)) {
+								self->weapon_type = WEAPON_TYPE_SHIELD;
+								weapons_fire(self, self->weapon_type);
+							}
+						}
+					}
+					else { // Let the first ships be easy to pass
+						self->update_strat_func = ship_ai_strat_avoid;
+					}
+				}
+
+				self->update_timer -= system_tick();
+
+				if (flags_is(self->flags, SHIP_OVERTAKEN)) {
+					// If ship has just overtaken, slow it down to a reasonable speed
+					if ((self->remote_thrust_max + behind_speed) > self->speed) {
+						self->speed += self->remote_thrust_mag * 30 * system_tick();
+					}
+				}
+				else {
+					// Increase the speed of any craft just in front slightly
+					if (((self->remote_thrust_max + (behind_speed >> 1)) > self->speed)) {
+						self->speed += self->remote_thrust_mag * 30 * system_tick();
+					}
+				}
+
+			}
+
+			
+			// Ship is JUST BEHIND; we must decided if and how many times it 'should have a go back'
+
+			else if ((section_diff >= -10) && (section_diff <= 0)) { // Ship just behind, MAKE DECISION
+				if (self->update_timer <= 0) { // Make New Decision
+					self->update_timer = UPDATE_TIME_JUST_BEHIND;
+
+					if (self->fight_back) { // Ship wants you to say "Outside Now!"
+						if (self->weapon_type == WEAPON_TYPE_NONE) {
+							self->update_strat_func = ship_ai_strat_avoid;
+							flags_add(self->flags, SHIP_OVERTAKEN);
+						}
+						else {
+							int chance = rand_int(0, 64);
+
+							if (chance < 48) {
+								self->update_strat_func = ship_ai_strat_block;
+							}
+							else if ((chance >= 40) && (chance < 54)) {
+								self->update_strat_func = ship_ai_strat_avoid;
+								flags_rm(self->flags, SHIP_OVERTAKEN);
+								if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
+									sfx_play(SFX_VOICE_ROCKETS);
+									self->weapon_type = WEAPON_TYPE_ROCKET;
+									weapons_fire_delayed(self, self->weapon_type);
+								}
+							}
+							else if ((chance >= 54) && (chance < 60)) {
+								self->update_strat_func = ship_ai_strat_avoid;
+								flags_rm(self->flags, SHIP_OVERTAKEN);
+								if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
+									sfx_play(SFX_VOICE_MISSILE);
+									self->weapon_type = WEAPON_TYPE_MISSILE;
+									self->weapon_target = &g.ships[g.pilot];
+									weapons_fire_delayed(self, self->weapon_type);
+								}
+							}
+							else if ((chance >= 60) && (chance < 64)) {
+								self->update_strat_func = ship_ai_strat_avoid;
+								flags_rm(self->flags, SHIP_OVERTAKEN);
+								if (flags_not(self->flags, SHIP_SHIELDED) && flags_is(self->flags, SHIP_RACING)) {
+									sfx_play(SFX_VOICE_SHOCKWAVE);
+									self->weapon_type = WEAPON_TYPE_EBOLT;
+									self->weapon_target = &g.ships[g.pilot];
+									weapons_fire_delayed(self, self->weapon_type);
+								}
+							}
+						}
+					}
+					else { // If ship destined to be tail-ender then slow down
+						self->remote_thrust_max = 2100 ;
+						self->remote_thrust_mag = 25;
+						self->speed = 2100 ;
+						self->update_strat_func = ship_ai_strat_avoid;
+						flags_rm(self->flags, SHIP_OVERTAKEN);
+					}
+				}
+
+				for (int i = 0; i < NUM_PILOTS; i++) { // If another ship is just in front pass fight on
+					if (flags_is(g.ships[i].flags, SHIP_JUST_IN_FRONT)) {
+						self->update_strat_func = ship_ai_strat_avoid;
+						flags_rm(self->flags, SHIP_OVERTAKEN);
+					}
+				}
+
+				self->update_timer -= system_tick();
+
+
+				if (flags_is(self->flags, SHIP_OVERTAKEN)) {
+					if ((self->remote_thrust_max + 700) > self->speed) {
+						self->speed += self->remote_thrust_mag * 2 * 30 * system_tick();
+					}
+				}
+				else {
+					if (((self->remote_thrust_max + behind_speed) > self->speed)) {
+						self->speed += self->remote_thrust_mag * 30 * system_tick();
+					}
+				}
+			}
+
+
+			// Ship is WELL AHEAD; we must slow the opponent to
+			// give the weaker player a chance to catch up
+			
+			else if (section_diff > (NUM_PILOTS - self->position_rank) * 15 && section_diff < 150) {
+				self->speed += self->remote_thrust_mag * 0.5 * 30 * system_tick();
+				if (self->speed > self->remote_thrust_max * 0.5) {
+					self->speed = self->remote_thrust_max * 0.5;
+				}
+
+				self->update_timer = 0;
+				self->update_strat_func = ship_ai_strat_hold_center;
+			}
+
+
+			// Ship is TOO FAR AHEAD
+
+			else if (section_diff >= 150) { // Ship too far ahead, let it continue
+				self->update_timer = 0;
+				self->update_strat_func = ship_ai_strat_avoid;
+
+				if ((self->remote_thrust_max > self->speed)) {
+					self->speed += self->remote_thrust_mag * 30 * system_tick();
+				}
+			}
+
+
+			// Ship is IN SIGHT
+
+			else if ((section_diff <= 10) && (section_diff > 4)) { // Ship close by, beware does not account for lapped opponents yet
+				if (self->update_timer <= 0) { // Make New Decision
+					int chance = rand_int(0, 5);
+
+					self->update_timer = UPDATE_TIME_IN_SIGHT;
+					switch (chance) {
+						case 0: self->update_strat_func = ship_ai_strat_hold_center; break;
+						case 1: self->update_strat_func = ship_ai_strat_hold_left; break;
+						case 2: self->update_strat_func = ship_ai_strat_hold_right; break;
+						case 3:	self->update_strat_func = ship_ai_strat_block; break;
+						case 4:	self->update_strat_func = ship_ai_strat_zig_zag; break;
+					}
+				}
+
+				self->update_timer -= system_tick();
+
+				if ((self->remote_thrust_max > self->speed)) {
+					self->speed += self->remote_thrust_mag * 30 * system_tick();
+				}
+			} // End of DPA control options
+
+
+			// Ship is JUST OUT OF SIGHT
+
+			else {
+				self->update_timer = 0;
+				self->update_strat_func = ship_ai_strat_hold_center;
+				if ((self->remote_thrust_max > self->speed)) {
+					self->speed += self->remote_thrust_mag * 30 * system_tick();
+				}
+			}
+		}
+
+		if (!self->update_strat_func) {
+			self->update_strat_func = ship_ai_strat_hold_center;
+		}
+		offset_vector = (self->update_strat_func)(self, face);
+
+
+		// Make decision as to which path the craft will take at a junction
+
+		section_t *section = self->section->prev;
+
+		for (int i = 0; i < 4; i++) {
+			section = section->next;
+		}
+
+		if (section->junction) {
+			if (flags_is(section->junction->flags, SECTION_JUNCTION_START)) {
+				int chance = rand_int(0, 2);
+				if (chance == 0) {
+					flags_add(self->flags, SHIP_JUNCTION_LEFT);
+				}
+				else {
+					flags_rm(self->flags, SHIP_JUNCTION_LEFT);
+				}
+			}
+		}
+
+		section = self->section->prev;
+
+		for (int i = 0; i < 4; i++) {
+			if (section->junction) {
+				if (flags_is(section->junction->flags, SECTION_JUNCTION_START)) {
+					if (flags_is(self->flags, SHIP_JUNCTION_LEFT)) {
+						section = section->junction;
+					}
+					else {
+						section = section->next;
+					}
+				}
+				else {
+					section = section->next;
+				}
+			}
+			else {
+				section = section->next;
+			}
+		}
+		section_t *next = section->next;
+		section = self->section;
+
+
+
+
+		// General routines - Non decision based
+
+
+		// Bleed off speed as orientation changes
+
+		self->speed -= fabsf(self->speed * self->angular_velocity.y) * 4 / (M_PI * 2) * system_tick(); // >> 14
+		self->speed -= fabsf(self->speed * self->angular_velocity.x) * 4 / (M_PI * 2) * system_tick(); // >> 14
+
+		// If remote has gone over boost
+		if (flags_is(face->flags, FACE_BOOST) && (self->update_strat_func == ship_ai_strat_hold_left || self->update_strat_func == ship_ai_strat_hold_center)) {
+			self->speed += 200 * 30 * system_tick();
+		}
+		face++;
+		if (flags_is(face->flags, FACE_BOOST) && (self->update_strat_func == ship_ai_strat_hold_right || self->update_strat_func == ship_ai_strat_hold_center)) {
+			self->speed += 200 * 30 * system_tick();
+		}
+
+		vec3_t track_target;
+		if (flags_is(self->section->flags, SECTION_JUMP)) {	// Cure for ships getting stuck on hump lip
+			track_target = vec3_sub(self->section->center, self->section->prev->center);
+		}
+		else {
+			track_target = vec3_sub(next->center, section->center);
+		}
+
+		float gap_length = vec3_len(track_target);
+		track_target = vec3_mulf(track_target, self->speed / gap_length);
+
+		vec3_t path1 = vec3_add(section->center, offset_vector);
+		vec3_t path2 = vec3_add(next->center, offset_vector);
+
+		vec3_t best_path = vec3_project_to_ray(self->position, path2, path1);
+		self->acceleration = vec3_add(track_target, vec3_mulf(vec3_sub(best_path, self->position), 0.5));
+
+
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+		float height = vec3_distance_to_plane(self->position, face_point, face->normal);
+
+		if (height < 50) {
+			height = 50;
+		}
+
+		self->acceleration = vec3_add(self->acceleration, vec3_mulf(vec3_sub(
+			vec3_mulf(face->normal, (SHIP_TRACK_FLOAT * SHIP_TRACK_MAGNET) / height),
+			vec3_mulf(face->normal, SHIP_TRACK_MAGNET)
+		), 16.0));
+		self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
+
+
+		float xy_dist = sqrt(track_target.x * track_target.x + track_target.z * track_target.z);
+
+		self->angular_velocity.x = wrap_angle(-atan2(track_target.y, xy_dist) - self->angle.x) * (1.0/16.0) * 30;
+		self->angular_velocity.y = (wrap_angle(-atan2(track_target.x, track_target.z) - self->angle.y) * (1.0/16.0)) * 30 + self->turn_rate_from_hit;
+	}
+
+
+	// Ship is SHIP_FLYING
+
+	else {
+		section_t *section = self->section->next->next;
+		section_t *next = section->next;
+
+		self->update_strat_func = ship_ai_strat_hold_center;
+		offset_vector = (self->update_strat_func)(self, NULL);
+
+		if (self->remote_thrust_max > self->speed) {
+			self->speed += self->remote_thrust_mag ;
+		}
+
+		self->speed -= fabsf(self->speed * self->angular_velocity.y) * (4 * M_PI * 2) * system_tick();
+		vec3_t track_target = vec3_sub(next->center, section->center);
+		float gap_length = vec3_len(track_target);
+
+		track_target.x = (track_target.x * self->speed) / gap_length;
+		track_target.z = (track_target.z * self->speed) / gap_length;
+
+		track_target.y = 500;
+
+		vec3_t best_path = vec3_project_to_ray(self->position, next->center, self->section->center);
+		self->acceleration = vec3(
+			(track_target.x + ((best_path.x - self->position.x) * 0.5)),
+			track_target.y,
+			(track_target.z + ((best_path.z - self->position.z) * 0.5))
+		);
+		self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
+
+		self->angular_velocity.x = -0.3 - self->angle.x * 30;
+		self->angular_velocity.y = wrap_angle(-atan2(track_target.x, track_target.z) - self->angle.y) * (1.0/16.0) * 30;
+	}
+
+	
+	self->angular_velocity.z += (self->angular_velocity.y * 2.0 - self->angular_velocity.z * 0.5) * 30 * system_tick();
+	self->turn_rate_from_hit -= self->turn_rate_from_hit * 0.125 * 30 * system_tick();
+
+	self->angle = vec3_add(self->angle, vec3_mulf(self->angular_velocity, system_tick()));
+	self->angle.z -= self->angle.z * 0.125 * 30 * system_tick();
+	self->angle = vec3_wrap_angle(self->angle);
+
+	self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.125 * 30 * system_tick()));
+	self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.015625 * 30 * system_tick()));
+
+	if (flags_is(self->flags, SHIP_ELECTROED)) {
+		self->position = vec3_add(self->position, vec3(rand_float(-20, 20), rand_float(-20, 20), rand_float(-20, 20)));
+
+		if (rand_int(0, 50) == 0) {
+			self->speed -= self->speed * 0.5 * 30 * system_tick();
+		}
+	}
+
+	sfx_set_position(self->sfx_engine_thrust, self->position, self->velocity, 0.5);
+}
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/ship_ai.h
@@ -1,0 +1,14 @@
+#ifndef SHIP_AI_H
+#define SHIP_AI_H
+
+#include "ship.h"
+
+#define UPDATE_TIME_JUST_FRONT  (150.0 * (1.0/30.0))
+#define UPDATE_TIME_JUST_BEHIND (200.0 * (1.0/30.0))
+#define UPDATE_TIME_IN_SIGHT    (200.0 * (1.0/30.0))
+
+void ship_ai_update_race(ship_t *self);
+void ship_ai_update_intro(ship_t *self);
+void ship_ai_update_intro_await_go(ship_t *self);
+
+#endif
--- /dev/null
+++ b/src/wipeout/ship_player.c
@@ -1,0 +1,582 @@
+#include <stdint.h>
+#include <math.h>
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "../mem.h"
+#include "object.h"
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "hud.h"
+#include "droid.h"
+#include "camera.h"
+#include "../utils.h"
+#include "scene.h"
+
+
+#include "../input.h"
+#include "../system.h"
+
+#include "sfx.h"
+#include "ship_player.h"
+#include "ship_ai.h"
+#include "game.h"
+#include "particle.h"
+
+void ship_player_update_sfx(ship_t *self) {
+	float speedf = self->speed * 0.000015;
+	self->sfx_engine_intake->volume = clamp(speedf, 0, 0.5);
+	self->sfx_engine_intake->pitch = 0.5 + speedf * 1.25;
+
+	self->sfx_engine_thrust->volume = 0.05 + 0.025 * (self->thrust_mag / self->thrust_max);
+	self->sfx_engine_thrust->pitch = 0.2 + 0.5 * (self->thrust_mag / self->thrust_max) + speedf;
+
+	float brake_left = self->brake_left * 0.0035;
+	float brake_right = self->brake_right * 0.0035;
+	self->sfx_turbulence->volume = (speedf * brake_left + speedf * brake_right) * 0.5;
+	self->sfx_turbulence->pan = (brake_right - brake_left);
+
+	self->sfx_shield->volume = flags_is(self->flags, SHIP_SHIELDED) ? 0.5 : 0;
+}
+
+void ship_player_update_intro(ship_t *self) {
+	self->temp_target = self->position;
+
+	self->sfx_engine_thrust = sfx_reserve_loop(SFX_ENGINE_THRUST);
+	self->sfx_engine_intake = sfx_reserve_loop(SFX_ENGINE_INTAKE);
+	self->sfx_shield = sfx_reserve_loop(SFX_SHIELD);
+	self->sfx_turbulence = sfx_reserve_loop(SFX_TURBULENCE);
+
+	ship_player_update_intro_general(self);
+	self->update_func = ship_player_update_intro_await_three;
+}
+
+void ship_player_update_intro_await_three(ship_t *self) {
+	ship_player_update_intro_general(self);
+
+	if (self->update_timer <= UPDATE_TIME_THREE) {
+		sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_3);
+		self->update_func = ship_player_update_intro_await_two;
+	}
+}
+
+void ship_player_update_intro_await_two(ship_t *self) {
+	ship_player_update_intro_general(self);	
+
+	if (self->update_timer <= UPDATE_TIME_TWO) {
+		scene_set_start_booms(1);
+		sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_2);
+		self->update_func = ship_player_update_intro_await_one;
+	}
+}
+
+void ship_player_update_intro_await_one(ship_t *self) {
+	ship_player_update_intro_general(self);
+
+	if (self->update_timer <= UPDATE_TIME_ONE) {
+		scene_set_start_booms(2);
+		sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_1);
+		self->update_func = ship_player_update_intro_await_go;
+	}
+}
+
+void ship_player_update_intro_await_go(ship_t *self) {
+	ship_player_update_intro_general(self);
+
+	if (self->update_timer <= UPDATE_TIME_GO) {
+		scene_set_start_booms(3);
+		sfx_t *sfx = sfx_play(SFX_VOICE_COUNT_GO);
+		
+		if (flags_is(self->flags, SHIP_RACING)) {
+			// Check for stall
+			if (self->thrust_mag >= 680 && self->thrust_mag <= 700) {
+				self->thrust_mag = 1800;
+				self->current_thrust_max = 1800;
+			}
+			else if (self->thrust_mag < 680) {
+				self->current_thrust_max = self->thrust_max;
+			}
+			else {
+				self->current_thrust_max = 200;
+			}
+
+			self->update_timer = UPDATE_TIME_STALL;
+			self->update_func = ship_player_update_race;
+		}
+		else {
+			self->update_func = ship_ai_update_race;
+		}
+	}
+}
+
+void ship_player_update_intro_general(ship_t *self) {
+	self->update_timer -= system_tick();
+	self->position.y = self->temp_target.y + sin(self->update_timer * 80.0 * 30.0 * M_PI * 2.0 / 4096.0) * 32;
+
+	// Thrust
+	if (input_state(A_THRUST)) {
+		self->thrust_mag += input_state(A_THRUST) * SHIP_THRUST_RATE * system_tick();
+	}
+	else {
+		self->thrust_mag -= SHIP_THRUST_RATE * system_tick();
+	}
+
+	self->thrust_mag = clamp(self->thrust_mag, 0, self->thrust_max);
+
+	// View
+	if (input_pressed(A_CHANGE_VIEW)) {
+		if (flags_not(self->flags, SHIP_VIEW_INTERNAL)) {
+			g.camera.update_func = camera_update_race_internal;
+			flags_add(self->flags, SHIP_VIEW_INTERNAL);
+		}
+		else {
+			g.camera.update_func = camera_update_race_external;
+			flags_rm(self->flags, SHIP_VIEW_INTERNAL);
+		}
+	}
+
+	ship_player_update_sfx(self);
+}
+
+
+void ship_player_update_race(ship_t *self) {
+	if (flags_not(self->flags, SHIP_RACING)) {
+		self->update_func = ship_ai_update_race;
+		return;
+	}
+
+	if (self->ebolt_timer > 0) {
+		self->ebolt_timer -= system_tick();
+	}
+
+	if (self->ebolt_timer <= 0) {
+		flags_rm(self->flags, SHIP_ELECTROED);
+	}
+
+	if (self->revcon_timer > 0) {
+		self->revcon_timer -= system_tick();
+	}
+
+	if (self->revcon_timer <= 0) {
+		flags_rm(self->flags, SHIP_REVCONNED);
+	}
+
+	if (self->special_timer > 0) {
+		self->special_timer -= system_tick();
+	}
+
+	if (self->special_timer <= 0) {
+		flags_rm(self->flags, SHIP_SPECIALED);
+	}
+
+	if (flags_is(self->flags, SHIP_REVCONNED)) {
+		// FIXME_PL: make sure revconned is honored
+	}
+
+	self->angular_acceleration = vec3(0, 0, 0);
+
+	if (input_state(A_LEFT)) {
+		if (self->angular_velocity.y >= 0) {
+			self->angular_acceleration.y += input_state(A_LEFT) * self->turn_rate;
+		}
+		else if (self->angular_velocity.y < 0) {
+			self->angular_acceleration.y += input_state(A_LEFT) * self->turn_rate * 2;
+		}
+	}
+	else if (input_state(A_RIGHT)) {
+		if (self->angular_velocity.y <= 0) {
+			self->angular_acceleration.y -= input_state(A_RIGHT) * self->turn_rate;
+		}
+		else if (self->angular_velocity.y > 0) {
+			self->angular_acceleration.y -= input_state(A_RIGHT) * self->turn_rate * 2;
+		}
+	}
+	
+	if (flags_is(self->flags, SHIP_ELECTROED)) {
+		self->ebolt_effect_timer += system_tick();
+		// Yank the ship every 0.1 seconds
+		if (self->ebolt_effect_timer > 0.1) {
+			if (flags_is(self->flags, SHIP_VIEW_INTERNAL)) {
+				// SetShake(2); // FIXME
+			}
+			self->angular_velocity.y += rand_float(-0.5, 0.5); // FIXME: 60fps
+			self->ebolt_effect_timer -= 0.1;
+		}
+	}
+
+	self->angular_acceleration.x += input_state(A_DOWN) * SHIP_PITCH_ACCEL;
+	self->angular_acceleration.x -= input_state(A_UP) * SHIP_PITCH_ACCEL;
+
+	// Handle Stall
+	if (self->update_timer > 0) {
+		if (self->current_thrust_max < 500) {
+			self->current_thrust_max += rand_float(0, 165) * system_tick();
+		}
+		self->update_timer -= system_tick();
+	}
+	else {
+		// End stall / boost
+		self->current_thrust_max = self->thrust_max;
+	}
+
+	// Thrust
+	if (input_state(A_THRUST)) {
+		self->thrust_mag += input_state(A_THRUST) * SHIP_THRUST_RATE * system_tick();
+	}
+	else {
+		self->thrust_mag -= SHIP_THRUST_FALLOFF * system_tick();
+	}
+	self->thrust_mag = clamp(self->thrust_mag, 0, self->current_thrust_max);
+
+	if (flags_is(self->flags, SHIP_ELECTROED) && rand_int(0, 80) == 0) {
+		self->thrust_mag -= self->thrust_mag * 0.25; // FIXME: 60fps
+	}
+
+	// Brake
+	if (input_state(A_BRAKE_RIGHT))	{
+		self->brake_right += SHIP_BRAKE_RATE * system_tick();
+	}
+	else if (self->brake_right > 0) {
+		self->brake_right -= SHIP_BRAKE_RATE * system_tick();
+	}
+	self->brake_right = clamp(self->brake_right, 0, 256);
+
+	if (input_state(A_BRAKE_LEFT))	{
+		self->brake_left += SHIP_BRAKE_RATE * system_tick();
+	}
+	else if (self->brake_left > 0) {
+		self->brake_left -= SHIP_BRAKE_RATE * system_tick();
+	}
+	self->brake_left = clamp(self->brake_left, 0, 256);
+
+	// View
+	if (input_pressed(A_CHANGE_VIEW)) {
+		if (flags_not(self->flags, SHIP_VIEW_INTERNAL)) {
+			g.camera.update_func = camera_update_race_internal;
+			flags_add(self->flags, SHIP_VIEW_INTERNAL);
+		}
+		else {
+			g.camera.update_func = camera_update_race_external;
+			flags_rm(self->flags, SHIP_VIEW_INTERNAL);
+		}
+	}
+
+	if (self->weapon_type == WEAPON_TYPE_MISSILE || self->weapon_type == WEAPON_TYPE_EBOLT) {
+		self->weapon_target = ship_player_find_target(self);
+	}
+	else {
+		self->weapon_target = NULL;
+	}
+
+	// Fire
+	// self->weapon_type = WEAPON_TYPE_MISSILE; // Test weapon
+
+	if (input_pressed(A_FIRE) && self->weapon_type != WEAPON_TYPE_NONE) {
+		if (flags_not(self->flags, SHIP_SHIELDED)) {
+			weapons_fire(self, self->weapon_type);
+		}
+		else {
+			sfx_play(SFX_MENU_MOVE);
+		}
+	}
+
+
+	// Physics
+
+	// Calculate thrust vector along principle axis of ship
+	self->thrust = vec3_mulf(self->dir_forward, self->thrust_mag * 64);
+	self->speed = vec3_len(self->velocity);
+	vec3_t forward_velocity = vec3_mulf(self->dir_forward, self->speed);
+
+	// SECTION_JUMP
+	if (flags_is(self->section->flags, SECTION_JUMP)) {
+		track_face_t *face = track_section_get_base_face(self->section);
+
+		// Project the ship's position to the track section using the face normal.
+		// If the point lands on the track, the sum of the angles between the 
+		// point and the track vertices will be M_PI*2.
+		// If it's less then M_PI*2 (minus a safety margin) we are flying!
+
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+		float height = vec3_distance_to_plane(self->position, face_point,  face->normal);
+		vec3_t plane_point = vec3_sub(self->position, vec3_mulf(face->normal, height));
+
+		vec3_t vec0 = vec3_sub(plane_point, face->tris[0].vertices[1].pos);
+		vec3_t vec1 = vec3_sub(plane_point, face->tris[0].vertices[2].pos);
+		face++;
+		vec3_t vec2 = vec3_sub(plane_point, face->tris[0].vertices[0].pos);
+		vec3_t vec3 = vec3_sub(plane_point, face->tris[1].vertices[0].pos);
+
+		float angle = 
+			vec3_angle(vec0, vec2) +
+			vec3_angle(vec2, vec3) +
+			vec3_angle(vec3, vec1) +
+			vec3_angle(vec1, vec0);
+		if (angle < M_PI * 2 - 0.01) {
+			flags_add(self->flags, SHIP_FLYING);
+		}
+	}
+
+	// Held by track
+	if (flags_not(self->flags, SHIP_FLYING)) {
+		track_face_t *face = track_section_get_base_face(self->section);
+		ship_collide_with_track(self, face);
+
+		if (flags_not(self->flags, SHIP_LEFT_SIDE)) {
+			face++;
+		}
+
+		// Boost
+		if (flags_not(self->flags, SHIP_SPECIALED) && flags_is(face->flags, FACE_BOOST)) {
+			vec3_t track_direction = vec3_sub(self->section->next->center, self->section->center);
+			self->velocity = vec3_add(self->velocity, vec3_mulf(track_direction, 30 * system_tick()));
+		}
+
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+		float height = vec3_distance_to_plane(self->position, face_point, face->normal);
+
+		// Collision with floor
+		if (height <= 0) {
+			if (self->last_impact_time > 0.2) {
+				self->last_impact_time = 0;
+				sfx_play_at(SFX_IMPACT, self->position, vec3(0,0,0), 1);
+			}
+			self->velocity = vec3_reflect(self->velocity, face->normal, 2);
+			self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.125));
+			self->velocity = vec3_sub(self->velocity, face->normal);
+		}
+		else if (height < 30) {
+			self->velocity = vec3_add(self->velocity, face->normal);
+		}
+
+		if (height < 50) {
+			height = 50;
+		}
+
+		// Calculate acceleration
+		float brake = (self->brake_left + self->brake_right);
+		float resistance = (self->resistance * (SHIP_MAX_RESISTANCE - (brake * 0.125))) * 0.0078125;
+
+		vec3_t force = vec3(0, SHIP_ON_TRACK_GRAVITY, 0);
+		force = vec3_add(force, vec3_mulf(vec3_mulf(face->normal, 4096), (SHIP_TRACK_MAGNET * SHIP_TRACK_FLOAT) / height));
+		force = vec3_sub(force, vec3_mulf(vec3_mulf(face->normal, 4096), SHIP_TRACK_MAGNET));
+		force = vec3_add(force, self->thrust);
+
+		self->acceleration = vec3_divf(vec3_sub(forward_velocity, self->velocity), self->skid + brake * 0.25);
+		self->acceleration = vec3_add(self->acceleration, vec3_divf(force, self->mass));
+		self->acceleration = vec3_sub(self->acceleration, vec3_divf(self->velocity, resistance));
+
+		// Burying the nose in the track? Move it out!
+		vec3_t nose_pos = vec3_add(self->position, vec3_mulf(self->dir_forward, 128));
+		float nose_height = vec3_distance_to_plane(nose_pos,face_point, face->normal);
+		if (nose_height < 600) {
+			self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT((height - nose_height + 5) * (1.0/16.0))));
+		}
+		else {
+			self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(-50.0/16.0)));
+		}
+	}
+
+	// Flying
+	else {
+		// Detect the need for a rescue droid
+		section_t *next = self->section->next;
+
+		vec3_t best_path = vec3_project_to_ray(self->position, next->center, self->section->center);
+		vec3_t distance = vec3_sub(best_path, self->position);
+
+		if (distance.y > 0) {
+			distance.y = distance.y * 0.0001;
+		}
+		else {
+			distance = vec3_mulf(distance, 8);
+		}
+
+		// Do we need to be rescued?
+		if (vec3_len(distance) > 8000) {
+			self->update_func = ship_player_update_rescue;
+			self->update_timer = UPDATE_TIME_RESCUE;
+			flags_add(self->flags, SHIP_IN_RESCUE | SHIP_FLYING);
+
+			section_t *landing = self->section->prev;
+			for (int i = 0; i < 3; i++) {
+				landing = landing->prev;
+			}
+			for (int i = 0; i < 10 && flags_not(landing->flags, SECTION_JUMP); i++) {
+				landing = landing->next;
+			}
+			self->section = landing;
+			self->temp_target = vec3_mulf(vec3_add(landing->center, landing->next->center), 0.5);
+			self->temp_target.y -= 2000;
+			self->velocity = vec3(0, 0, 0);
+		}
+
+
+		float brake = (self->brake_left + self->brake_right);
+		float resistance = (self->resistance * (SHIP_MAX_RESISTANCE - (brake * 0.125))) * 0.0078125;
+
+		vec3_t force = vec3(0, SHIP_FLYING_GRAVITY, 0);
+		force = vec3_add(force, self->thrust);
+
+		self->acceleration = vec3_divf(vec3_sub(forward_velocity, self->velocity), SHIP_MIN_RESISTANCE + brake * 4);
+		self->acceleration = vec3_add(self->acceleration, vec3_divf(force, self->mass));
+		self->acceleration = vec3_sub(self->acceleration, vec3_divf(self->velocity, resistance));
+
+		self->angular_acceleration.x += NTSC_ACCELERATION(ANGLE_NORM_TO_RADIAN(FIXED_TO_FLOAT(-50.0/16.0)));
+	}
+
+	// Position
+	self->velocity = vec3_add(self->velocity, vec3_mulf(self->acceleration, 30 * system_tick()));
+	self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.015625 * 30 * system_tick()));
+
+	self->angular_acceleration.x -= self->angular_velocity.x * 0.25 * 30;
+	self->angular_acceleration.z += (self->angular_velocity.y - 0.5 * self->angular_velocity.z) * 30;
+
+
+	// Orientation
+	if (self->angular_acceleration.y == 0) {
+		if (self->angular_velocity.y > 0) {
+			self->angular_acceleration.y -= min(self->turn_rate, self->angular_velocity.y / system_tick());
+		}
+		else if (self->angular_velocity.y < 0) {
+			self->angular_acceleration.y += min(self->turn_rate, -self->angular_velocity.y / system_tick());
+		}
+	}
+
+	self->angular_velocity = vec3_add(self->angular_velocity, vec3_mulf(self->angular_acceleration, system_tick()));
+	self->angular_velocity.y = clamp(self->angular_velocity.y, -self->turn_rate_max, self->turn_rate_max);
+	
+	float brake_dir = (self->brake_left - self->brake_right) * (0.125 / 4096.0);
+	self->angle.y += brake_dir * self->speed * 0.000030517578125 * M_PI * 2 * 30 * system_tick();
+
+	self->angle = vec3_add(self->angle, vec3_mulf(self->angular_velocity, system_tick()));
+	self->angle.z -= self->angle.z * 0.125 * 30 * system_tick();
+	self->angle = vec3_wrap_angle(self->angle);
+
+	// Prevent ship from going past the landing position of a SECTION_JUMP if going backwards.
+	if (flags_not(self->flags, SHIP_DIRECTION_FORWARD) && flags_is(self->section->prev->flags, SECTION_JUMP)) {
+		vec3_t repulse = vec3_sub(self->section->next->center, self->section->center);
+		self->velocity = vec3_add(self->velocity, vec3_mulf(repulse, 2));
+	}
+
+	ship_player_update_sfx(self);
+}
+
+
+void ship_player_update_rescue(ship_t *self) {
+
+	section_t *next = self->section->next;
+
+	if (flags_is(self->flags, SHIP_IN_TOW)) {
+		self->temp_target = vec3_add(self->temp_target, vec3_mulf(vec3_sub(next->center, self->temp_target), 0.0078125));
+		self->velocity = vec3_sub(self->temp_target, self->position);
+		vec3_t target_dir = vec3_sub(next->center, self->section->center);
+
+		self->angular_velocity.y = wrap_angle(-atan2(target_dir.x, target_dir.z) - self->angle.y) * (1.0/16.0) * 30;
+		self->angle.y = wrap_angle(self->angle.y + self->angular_velocity.y * system_tick());
+	}
+
+	self->angle.x -= self->angle.x * 0.125 * 30 * system_tick();
+	self->angle.z -= self->angle.z * 0.03125 * 30 * system_tick();
+
+	self->velocity = vec3_sub(self->velocity, vec3_mulf(self->velocity, 0.0625 * 30 * system_tick()));
+	self->position = vec3_add(self->position, vec3_mulf(self->velocity, 0.03125 * 30 * system_tick()));
+
+
+	// Are we done being rescued?
+	float distance = vec3_len(vec3_sub(self->position, self->temp_target));
+	if (flags_is(self->flags, SHIP_IN_TOW) && distance < 800) {
+		self->update_func = ship_player_update_race;
+		self->update_timer = 0;
+		flags_rm(self->flags, SHIP_IN_RESCUE);
+		flags_rm(self->flags, SHIP_VIEW_REMOTE);
+
+		if (flags_is(self->flags, SHIP_VIEW_INTERNAL)) {
+			g.camera.update_func = camera_update_race_internal;
+		}
+		else {
+			g.camera.update_func = camera_update_race_external;
+		}
+	}
+}
+
+
+ship_t *ship_player_find_target(ship_t *self) {
+	int shortest_distance = 256;
+	ship_t *nearest_ship = NULL;
+
+	for (int i = 0; i < len(g.ships); i++) {
+		ship_t *other = &g.ships[i];
+		if (self == other) {
+			continue;
+		}
+		
+		// We are on a branch
+		if (flags_is(self->section->flags, SECTION_JUNCTION)) {
+			// Other ship is on same branch
+			if (other->section->flags & SECTION_JUNCTION) {
+				int distance = other->section->num - self->section->num;
+
+				if (distance < shortest_distance && distance > 0) {
+					shortest_distance = distance;
+					nearest_ship = other;
+				}
+			}
+
+			// Other ship is not on branch
+			else {
+				section_t *section = self->section;
+				for (int distance = 0; distance < 10; distance++) {
+					section = section->next;
+					if (other->section == section && distance < shortest_distance && distance > 0) {
+						shortest_distance = distance;
+						nearest_ship = other;
+						break;
+					}
+				}
+			}
+		}
+
+		// We are not on a branch
+		else {
+			// Other ship is on a branch - check if we can reach the other ship's section
+			if (flags_is(other->section->flags, SECTION_JUNCTION)) {
+				section_t *section = self->section;
+				for (int distance = 0; distance < 10; distance++) {
+					if (section->junction) {
+						section = section->junction;
+					}
+					else {
+						section = section->next;
+					}
+					if (other->section == section && distance < shortest_distance && distance > 0) {
+						shortest_distance = distance;
+						nearest_ship = other;
+						break;
+					}
+				}
+			}
+
+			// Other ship is not on a branch
+			else {
+				int distance = other->section->num - self->section->num;
+
+				if (distance < shortest_distance && distance > 0) {
+					shortest_distance = distance;
+					nearest_ship = other;
+				}
+			}
+		}
+	}
+
+	if (shortest_distance < 10) {
+		return nearest_ship;
+	}
+	else {
+		return NULL;
+	}
+}
+
--- /dev/null
+++ b/src/wipeout/ship_player.h
@@ -1,0 +1,20 @@
+#ifndef SHIP_PLAYER_H
+#define SHIP_PLAYER_H
+
+#include "ship.h"
+
+#define UPDATE_TIME_RESCUE (500.0 * (1.0/30.0))
+#define UPDATE_TIME_STALL   (90.0 * (1.0/30.0))
+
+void ship_player_update_intro(ship_t *self);
+void ship_player_update_intro_await_three(ship_t *self);
+void ship_player_update_intro_await_two(ship_t *self);
+void ship_player_update_intro_await_one(ship_t *self);
+void ship_player_update_intro_await_go(ship_t *self);
+void ship_player_update_intro_general(ship_t *self);
+void ship_player_update_race(ship_t *self);
+void ship_player_update_rescue(ship_t *self);
+
+ship_t *ship_player_find_target(ship_t *self);
+
+#endif
--- /dev/null
+++ b/src/wipeout/title.c
@@ -1,0 +1,45 @@
+#include "../system.h"
+#include "../input.h"
+#include "../utils.h"
+
+#include "title.h"
+#include "ui.h"
+#include "image.h"
+#include "game.h"
+
+static uint16_t title_image;
+static float start_time;
+static bool has_shown_attract = false;
+
+void title_init() {
+	title_image = image_get_texture("wipeout/textures/wiptitle.tim");
+	start_time = system_time();
+	sfx_music_mode(SFX_MUSIC_RANDOM);
+}
+
+void title_update() {
+	render_set_view_2d();
+	render_push_2d(vec2i(0, 0), render_size(), rgba(128, 128, 128, 255), title_image);
+	ui_draw_text_centered("PRESS ENTER", ui_scaled_pos(UI_POS_BOTTOM | UI_POS_CENTER, vec2i(0, -40)), UI_SIZE_8, UI_COLOR_DEFAULT);
+
+
+	if (input_pressed(A_MENU_SELECT) || input_pressed(A_MENU_START)) {
+		sfx_play(SFX_MENU_SELECT);
+		game_set_scene(GAME_SCENE_MAIN_MENU);
+	}
+
+	float duration = system_time() - start_time;
+	if (
+		(has_shown_attract && duration > 5) ||
+		(duration > 10)
+	) {
+		sfx_music_mode(SFX_MUSIC_RANDOM);
+		has_shown_attract = true;
+		g.is_attract_mode = true;
+		g.pilot = rand_int(0, len(def.pilots));
+		g.circut = rand_int(0, NUM_NON_BONUS_CIRCUTS);
+		g.race_class = rand_int(0, NUM_RACE_CLASSES);
+		g.race_type = RACE_TYPE_SINGLE;
+		game_set_scene(GAME_SCENE_RACE);
+	}
+}
--- /dev/null
+++ b/src/wipeout/title.h
@@ -1,0 +1,8 @@
+#ifndef TITLE_H
+#define TITLE_H
+
+void title_init();
+void title_update();
+void title_cleanup();
+
+#endif
--- /dev/null
+++ b/src/wipeout/track.c
@@ -1,0 +1,387 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../render.h"
+#include "../system.h"
+
+#include "object.h"
+#include "track.h"
+#include "camera.h"
+#include "object.h"
+#include "game.h"
+
+void track_load(const char *base_path) {
+	// Load and assemble high res track tiles
+
+	g.track.textures.start = render_textures_len();
+	g.track.textures.len = 0;
+
+	ttf_t *ttf = track_load_tile_format(get_path(base_path, "library.ttf"));
+	cmp_t *cmp = image_load_compressed(get_path(base_path, "library.cmp"));
+
+	image_t *temp_tile = image_alloc(128, 128);
+	for (int i = 0; i < ttf->len; i++) {
+		for (int tx = 0; tx < 4; tx++) {
+			for (int ty = 0; ty < 4; ty++) {
+				uint32_t sub_tile_index = ttf->tiles[i].near[ty * 4 + tx];
+				image_t *sub_tile = image_load_from_bytes(cmp->entries[sub_tile_index], false);
+				image_copy(sub_tile, temp_tile, 0, 0, 32, 32, tx * 32, ty * 32);
+				mem_temp_free(sub_tile);
+			}
+		}
+		render_texture_create(temp_tile->width, temp_tile->height, temp_tile->pixels);
+		g.track.textures.len++;
+	}
+
+	mem_temp_free(temp_tile);
+	mem_temp_free(cmp);
+	mem_temp_free(ttf);
+
+	vec3_t *vertices = track_load_vertices(get_path(base_path, "track.trv"));
+	track_load_faces(get_path(base_path, "track.trf"), vertices);
+	mem_temp_free(vertices);
+
+	track_load_sections(get_path(base_path, "track.trs"));
+
+	g.track.pickups_len = 0;
+	section_t *s = g.track.sections;
+	section_t *j = NULL;
+
+	// Nummerate all sections; take care to give both stretches at a junction
+	// the same numbers.
+	int num = 0;
+	do {
+		s->num = num++;
+		if (s->junction) { // start junction
+			j = s->junction;
+			do {
+				j->num = num++;
+				j = j->next;
+			} while (!j->junction); // end junction
+			num = s->num;
+		}
+		s = s->next;
+	} while (s != g.track.sections);
+	g.track.total_section_nums = num;
+
+	g.track.pickups = mem_mark();
+	for (int i = 0; i < g.track.section_count; i++) {
+		track_face_t *face = track_section_get_base_face(&g.track.sections[i]);
+		
+		for (int f = 0; f < 2; f++) {
+			if (flags_any(face->flags, FACE_PICKUP_RIGHT | FACE_PICKUP_LEFT)) {
+				mem_bump(sizeof(track_pickup_t));
+				g.track.pickups[g.track.pickups_len].face = face;
+				g.track.pickups[g.track.pickups_len].cooldown_timer = 0;
+				g.track.pickups_len++;
+			}
+			
+			if (flags_is(face->flags, FACE_BOOST)) {
+				track_face_set_color(face, rgba(0, 0, 255, 255));
+			}
+			face++;
+		}
+		
+		error_if(g.track.pickups_len > TRACK_PICKUPS_MAX-1, "Track %s exceeds TRACK_PICKUPS_MAX", base_path);
+	}
+}
+
+ttf_t *track_load_tile_format(char *ttf_name) {
+	uint32_t ttf_size;
+	uint8_t *ttf_bytes = file_load(ttf_name, &ttf_size);
+
+	uint32_t p = 0;
+	uint32_t num_tiles = ttf_size / 42;
+
+	ttf_t *ttf = mem_temp_alloc(sizeof(ttf_t) + sizeof(ttf_tile_t) * num_tiles);
+	ttf->len = num_tiles;
+
+	for (int t = 0; t < num_tiles; t++) {
+		for (int i = 0; i < 16; i++) {
+			ttf->tiles[t].near[i] = get_i16(ttf_bytes, &p);
+		}
+		for (int i = 0; i < 4; i++) {
+			ttf->tiles[t].med[i] = get_i16(ttf_bytes, &p);
+		}
+		ttf->tiles[t].far = get_i16(ttf_bytes, &p);
+	}
+	mem_temp_free(ttf_bytes);
+
+	return ttf;
+}
+
+bool track_collect_pickups(track_face_t *face) {
+	if (flags_is(face->flags, FACE_PICKUP_ACTIVE)) {
+		flags_rm(face->flags, FACE_PICKUP_ACTIVE);
+		flags_add(face->flags, FACE_PICKUP_COLLECTED);
+		track_face_set_color(face, rgba(255, 255, 255, 255));
+		return true;
+	}
+	else {
+		return false;
+	}
+}
+
+vec3_t *track_load_vertices(char *file_name) {
+	uint32_t size;
+	uint8_t *bytes = file_load(file_name, &size);
+
+	g.track.vertex_count = size / 16; // VECTOR_SIZE
+	vec3_t *vertices = mem_temp_alloc(sizeof(vec3_t) * g.track.vertex_count);
+	
+	uint32_t p = 0;
+	for (int i = 0; i < g.track.vertex_count; i++) {
+		vertices[i].x = get_i32(bytes, &p);
+		vertices[i].y = get_i32(bytes, &p);
+		vertices[i].z = get_i32(bytes, &p);
+		p += 4; // padding
+	}
+
+	mem_temp_free(bytes);
+	return vertices;
+}
+
+static const vec2_t track_uv[2][4] = {
+	{{128, 0}, {  0, 0}, {  0, 128}, {128, 128}},
+	{{  0, 0}, {128, 0}, {128, 128}, {  0, 128}}
+};
+
+void track_load_faces(char *file_name, vec3_t *vertices) {
+	uint32_t size;
+	uint8_t *bytes = file_load(file_name, &size);
+
+	g.track.face_count = size / 20; // TRACK_FACE_DATA_SIZE
+	g.track.faces = mem_bump(sizeof(track_face_t) * g.track.face_count);
+
+	uint32_t p = 0;
+	track_face_t *tf = g.track.faces;
+
+	
+	for (int i = 0; i < g.track.face_count; i++) {
+
+		vec3_t v0 = vertices[get_i16(bytes, &p)];
+		vec3_t v1 = vertices[get_i16(bytes, &p)];
+		vec3_t v2 = vertices[get_i16(bytes, &p)];
+		vec3_t v3 = vertices[get_i16(bytes, &p)];
+		tf->normal.x = (float)get_i16(bytes, &p) / 4096.0;
+		tf->normal.y = (float)get_i16(bytes, &p) / 4096.0;
+		tf->normal.z = (float)get_i16(bytes, &p) / 4096.0;
+
+		tf->texture = get_i8(bytes, &p);
+		tf->flags = get_i8(bytes, &p);
+
+		rgba_t color = {.as_uint32 = get_i32_le(bytes, &p) | 0xff000000};
+		const vec2_t *uv = track_uv[flags_is(tf->flags, FACE_FLIP_TEXTURE) ? 1 : 0];
+
+		tf->tris[0] = (tris_t){
+			.vertices = {
+				{.pos = v0, .uv = uv[0], .color = color},
+				{.pos = v1, .uv = uv[1], .color = color},
+				{.pos = v2, .uv = uv[2], .color = color},
+			}
+		};
+		tf->tris[1] = (tris_t){
+			.vertices = {
+				{.pos = v3, .uv = uv[3], .color = color},
+				{.pos = v0, .uv = uv[0], .color = color},
+				{.pos = v2, .uv = uv[2], .color = color},
+			}
+		};
+
+		tf++;
+	}
+
+	mem_temp_free(bytes);
+}
+
+
+void track_load_sections(char *file_name) {
+	uint32_t size;
+	uint8_t *bytes = file_load(file_name, &size);
+
+	g.track.section_count = size / 156; // SECTION_DATA_SIZE
+	g.track.sections = mem_bump(sizeof(section_t) * g.track.section_count);
+
+	uint32_t p = 0;
+	section_t *ts = g.track.sections;
+	for (int i = 0; i < g.track.section_count; i++) {
+		int32_t junction_index = get_i32(bytes, &p);
+		if (junction_index != -1) {
+			ts->junction = g.track.sections + junction_index;
+		}
+		else {
+			ts->junction = NULL;
+		}
+
+		ts->prev = g.track.sections + get_i32(bytes, &p);
+		ts->next = g.track.sections + get_i32(bytes, &p);
+
+		ts->center.x = get_i32(bytes, &p);
+		ts->center.y = get_i32(bytes, &p);
+		ts->center.z = get_i32(bytes, &p);
+
+		int16_t version = get_i16(bytes, &p);
+		error_if(version != TRACK_VERSION, "Convert track with track10: section: %d Track: %d\n", version, TRACK_VERSION);
+		p += 2; // padding
+
+		p += 4 + 4; // objects pointer, objectCount
+		p += 5 * 3 * 4; // view section pointers
+		p += 5 * 3 * 2; // view section counts
+
+		for (int j = 0; j < 4; j++) {
+			ts->high[j] = get_i16(bytes, &p);
+		}
+		for (int j = 0; j < 4; j++) {
+			ts->med[j] = get_i16(bytes, &p);
+		}
+
+		ts->face_start = get_i16(bytes, &p);
+		ts->face_count = get_i16(bytes, &p);
+
+		p += 2 * 2; // global/local radius
+
+		ts->flags = get_i16(bytes, &p);
+		ts->num = get_i16(bytes, &p);
+		p += 2; // padding
+		ts++;
+	}
+
+	mem_temp_free(bytes);
+}
+
+
+
+
+void track_draw_section(section_t *section) {
+	track_face_t *face = g.track.faces + section->face_start;
+	int16_t face_count = section->face_count;
+	
+
+	for (uint32_t j = 0; j < face_count; j++) {
+		uint16_t tex_index = texture_from_list(g.track.textures, face->texture);
+		render_push_tris(face->tris[0], tex_index);
+		render_push_tris(face->tris[1], tex_index);
+		face++;
+	}
+}
+
+void track_draw(camera_t *camera) {	
+	render_set_model_mat(&mat4_identity());	
+	
+	float max_dist_sq = RENDER_FADEOUT_FAR * RENDER_FADEOUT_FAR;
+	vec3_t cam_pos = camera->position;
+
+	section_t *s = g.track.sections;
+	section_t *j = NULL;
+	do {
+		vec3_t d = vec3_sub(cam_pos, s->center);
+		float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
+		if (dist_sq <  max_dist_sq) {
+			track_draw_section(s);
+		}
+
+		if (s->junction) { // start junction
+			j = s->junction;
+			do {
+				vec3_t d = vec3_sub(cam_pos, j->center);
+				float dist_sq = d.x * d.x + d.y * d.y + d.z * d.z;
+				if (dist_sq <  max_dist_sq) {
+					track_draw_section(j);
+				}
+				j = j->next;
+			} while (!j->junction); // end junction
+		}
+		s = s->next;
+	} while (s != g.track.sections);
+	
+}
+
+void track_cycle_pickups() {
+	float pickup_cycle_time = 1.5 * system_cycle_time();
+
+	for (int i = 0; i < g.track.pickups_len; i++) {
+		if (flags_is(g.track.pickups[i].face->flags, FACE_PICKUP_COLLECTED)) {
+			flags_rm(g.track.pickups[i].face->flags, FACE_PICKUP_COLLECTED);
+			g.track.pickups[i].cooldown_timer = TRACK_PICKUP_COOLDOWN_TIME;
+		}
+		else if (g.track.pickups[i].cooldown_timer <= 0) {
+			flags_add(g.track.pickups[i].face->flags, FACE_PICKUP_ACTIVE);
+			track_face_set_color(g.track.pickups[i].face, rgba(
+				sin( pickup_cycle_time + i) * 127 + 128,
+				cos( pickup_cycle_time + i) * 127 + 128,
+				sin(-pickup_cycle_time - i) * 127 + 128,
+				255
+			));
+		}
+		else{
+			g.track.pickups[i].cooldown_timer -= system_tick();
+		}
+	}
+}
+
+void track_face_set_color(track_face_t *face, rgba_t color) {
+	face->tris[0].vertices[0].color = color;
+	face->tris[0].vertices[1].color = color;
+	face->tris[0].vertices[2].color = color;
+
+	face->tris[1].vertices[0].color = color;
+	face->tris[1].vertices[1].color = color;
+	face->tris[1].vertices[2].color = color;
+}
+
+track_face_t *track_section_get_base_face(section_t *section) {
+	track_face_t *face = g.track.faces +section->face_start;
+	while(flags_not(face->flags, FACE_TRACK_BASE)) {
+		face++;
+	}
+	return face;
+}
+
+section_t *track_nearest_section(vec3_t pos, section_t *section, float *distance) {
+	// Start search several sections before current section
+
+	for (int i = 0; i < TRACK_SEARCH_LOOK_BACK; i++) {
+		section = section->prev;
+	}
+
+	// Find vector from ship center to track section under
+	// consideration
+	float shortest_distance = 1000000000.0;
+	section_t *nearest_section = section;
+	section_t *junction = NULL;
+	for (int i = 0; i < TRACK_SEARCH_LOOK_AHEAD; i++) {
+		if (section->junction) {
+			junction = section->junction;
+		}
+
+		float d = vec3_len(vec3_sub(pos, section->center));
+		if (d < shortest_distance) {
+			shortest_distance = d;
+			nearest_section = section;
+		}
+
+		section = section->next;
+	}
+
+	if (junction) {
+		section = junction;
+		for (int i = 0; i < TRACK_SEARCH_LOOK_AHEAD; i++) {
+			float d = vec3_len(vec3_sub(pos, section->center));
+			if (d < shortest_distance) {
+				shortest_distance = d;
+				nearest_section = section;
+			}
+
+			if (flags_is(junction->flags, SECTION_JUNCTION_START)) {
+				section = section->next;
+			}
+			else {
+				section = section->prev;
+			}
+		}
+	}
+
+	if (distance != NULL) {
+		*distance = shortest_distance;
+	}
+	return nearest_section;
+}
--- /dev/null
+++ b/src/wipeout/track.h
@@ -1,0 +1,104 @@
+#ifndef TRACK_H
+#define TRACK_H
+
+
+#include "../types.h"
+#include "object.h"
+#include "image.h"
+
+#define TRACK_VERSION 8
+
+#define TRACK_VERTS_MAX    4096
+#define TRACK_FACES_MAX    3072
+#define TRACK_SECTIONS_MAX 1024
+#define TRACK_PICKUPS_MAX    64
+
+#define TRACK_PICKUP_COOLDOWN_TIME 1
+
+#define TRACK_SEARCH_LOOK_BACK 3
+#define TRACK_SEARCH_LOOK_AHEAD 6
+
+typedef struct track_face_t {
+	tris_t tris[2];
+	vec3_t normal;
+	uint8_t flags;
+	uint8_t texture;
+} track_face_t;
+
+#define FACE_TRACK_BASE       (1<<0)
+#define FACE_PICKUP_LEFT      (1<<1)
+#define FACE_FLIP_TEXTURE     (1<<2)
+#define FACE_PICKUP_RIGHT     (1<<3)
+#define FACE_START_GRID       (1<<4)
+#define FACE_BOOST            (1<<5)
+#define FACE_PICKUP_COLLECTED (1<<6)
+#define FACE_PICKUP_ACTIVE    (1<<7)
+
+typedef struct {
+	uint16_t near[16];
+	uint16_t med[4];
+	uint16_t far;
+} ttf_tile_t;
+
+typedef struct {
+	uint32_t len;
+	ttf_tile_t tiles[];
+} ttf_t;
+
+typedef struct section_t {
+	struct section_t *junction;
+	struct section_t *prev;
+	struct section_t *next;
+
+	vec3_t center;
+
+	int16_t high[4];
+	int16_t med[4];
+
+	int16_t face_start;
+	int16_t face_count;
+
+	int16_t flags;
+	int16_t num;
+} section_t;
+
+#define SECTION_JUMP            1
+#define SECTION_JUNCTION_END    8
+#define SECTION_JUNCTION_START 16
+#define SECTION_JUNCTION       32
+
+typedef struct {
+	track_face_t *face;
+	float cooldown_timer;
+} track_pickup_t;
+
+typedef struct track_t {
+	int32_t vertex_count;
+	int32_t face_count;
+	int32_t section_count;
+	int32_t pickups_len;
+	int32_t total_section_nums;
+	texture_list_t textures;
+	
+	track_face_t *faces;
+	section_t *sections;
+	track_pickup_t *pickups;
+} track_t;
+
+
+void track_load(const char *base_path);
+ttf_t *track_load_tile_format(char *ttf_name);
+vec3_t *track_load_vertices(char *file);
+void track_load_faces(char *file, vec3_t *vertices);
+void track_load_sections(char *file);
+bool track_collect_pickups(track_face_t *face);
+void track_face_set_color(track_face_t *face, rgba_t color);
+track_face_t *track_section_get_base_face(section_t *section);
+section_t *track_nearest_section(vec3_t pos, section_t *section, float *distance);
+
+struct camera_t;
+void track_draw(struct camera_t *camera);
+
+void track_cycle_pickups();
+
+#endif
\ No newline at end of file
--- /dev/null
+++ b/src/wipeout/ui.c
@@ -1,0 +1,213 @@
+#include "../render.h"
+#include "../utils.h"
+
+#include "ui.h"
+#include "image.h"
+
+typedef struct {
+	vec2i_t offset;
+	uint16_t width;
+} glyph_t;
+
+typedef struct {
+	uint16_t texture;
+	uint16_t height;
+	glyph_t glyphs[40];
+} char_set_t;
+
+int ui_scale = 2;
+
+char_set_t char_set[UI_SIZE_MAX] = {
+	[UI_SIZE_16] = {
+		.texture = 0,
+		.height = 16,
+		.glyphs = {
+			{{  0,   0}, 25}, {{ 25,   0}, 24}, {{ 49,   0}, 17}, {{ 66,   0}, 24}, {{ 90,   0}, 24}, {{114,   0}, 17}, {{131,   0}, 25}, {{156,   0}, 18},
+			{{174,   0},  7}, {{181,   0}, 17}, {{  0,  16}, 17}, {{ 17,  16}, 17}, {{ 34,  16}, 28}, {{ 62,  16}, 17}, {{ 79,  16}, 24}, {{103,  16}, 24},
+			{{127,  16}, 26}, {{153,  16}, 24}, {{177,  16}, 18}, {{195,  16}, 17}, {{  0,  32}, 17}, {{ 17,  32}, 17}, {{ 34,  32}, 29}, {{ 63,  32}, 24},
+			{{ 87,  32}, 17}, {{104,  32}, 18}, {{122,  32}, 24}, {{146,  32}, 10}, {{156,  32}, 18}, {{174,  32}, 17}, {{191,  32}, 18}, {{  0,  48}, 18},
+			{{ 18,  48}, 18}, {{ 36,  48}, 18}, {{ 54,  48}, 22}, {{ 76,  48}, 25}, {{101,  48},  7}, {{108,  48},  7}, {{198,   0},  0}, {{198,   0},  0}
+		}
+	},
+	[UI_SIZE_12] = {
+		.texture = 0,
+		.height = 12,
+		.glyphs = {
+			{{  0,   0}, 19}, {{ 19,   0}, 19}, {{ 38,   0}, 14}, {{ 52,   0}, 19}, {{ 71,   0}, 19}, {{ 90,   0}, 13}, {{103,   0}, 19}, {{122,   0}, 14},
+			{{136,   0},  6}, {{142,   0}, 13}, {{155,   0}, 14}, {{169,   0}, 14}, {{  0,  12}, 22}, {{ 22,  12}, 14}, {{ 36,  12}, 19}, {{ 55,  12}, 18},
+			{{ 73,  12}, 20}, {{ 93,  12}, 19}, {{112,  12}, 15}, {{127,  12}, 14}, {{141,  12}, 13}, {{154,  12}, 13}, {{167,  12}, 22}, {{  0,  24}, 19},
+			{{ 19,  24}, 13}, {{ 32,  24}, 14}, {{ 46,  24}, 19}, {{ 65,  24},  8}, {{ 73,  24}, 15}, {{ 88,  24}, 13}, {{101,  24}, 14}, {{115,  24}, 15},
+			{{130,  24}, 14}, {{144,  24}, 15}, {{159,  24}, 18}, {{177,  24}, 19}, {{196,  24},  5}, {{201,  24},  5}, {{183,   0},  0}, {{183,   0},  0}
+		}
+	},
+	[UI_SIZE_8] = {
+		.texture = 0,
+		.height = 8,
+		.glyphs = {
+			{{  0,   0}, 13}, {{ 13,   0}, 13}, {{ 26,   0}, 10}, {{ 36,   0}, 13}, {{ 49,   0}, 13}, {{ 62,   0},  9}, {{ 71,   0}, 13}, {{ 84,   0}, 10},
+			{{ 94,   0},  4}, {{ 98,   0},  9}, {{107,   0}, 10}, {{117,   0}, 10}, {{127,   0}, 16}, {{143,   0}, 10}, {{153,   0}, 13}, {{166,   0}, 13},
+			{{179,   0}, 14}, {{  0,   8}, 13}, {{ 13,   8}, 10}, {{ 23,   8},  9}, {{ 32,   8},  9}, {{ 41,   8},  9}, {{ 50,   8}, 16}, {{ 66,   8}, 14},
+			{{ 80,   8},  9}, {{ 89,   8}, 10}, {{ 99,   8}, 13}, {{112,   8},  6}, {{118,   8}, 11}, {{129,   8}, 10}, {{139,   8}, 10}, {{149,   8}, 11},
+			{{160,   8}, 10}, {{170,   8}, 10}, {{180,   8}, 12}, {{192,   8}, 14}, {{206,   8},  4}, {{210,   8},  4}, {{193,   0},  0}, {{193,   0},  0}
+		}
+	},
+};
+
+uint16_t icon_textures[UI_ICON_MAX];
+
+void ui_load() {
+	texture_list_t tl = image_get_compressed_textures("wipeout/textures/drfonts.cmp");
+	char_set[UI_SIZE_16].texture   = texture_from_list(tl, 0);
+	char_set[UI_SIZE_12].texture   = texture_from_list(tl, 1);
+	char_set[UI_SIZE_8 ].texture   = texture_from_list(tl, 2);
+	icon_textures[UI_ICON_HAND]    = texture_from_list(tl, 3);
+	icon_textures[UI_ICON_CONFIRM] = texture_from_list(tl, 5);
+	icon_textures[UI_ICON_CANCEL]  = texture_from_list(tl, 6);
+	icon_textures[UI_ICON_END]     = texture_from_list(tl, 7);
+	icon_textures[UI_ICON_DEL]     = texture_from_list(tl, 8);
+	icon_textures[UI_ICON_STAR]    = texture_from_list(tl, 9);
+}
+
+int ui_get_scale() {
+	return ui_scale;
+}
+
+void ui_set_scale(int scale) {
+	ui_scale = scale;
+}
+
+
+vec2i_t ui_scaled(vec2i_t v) {
+	return vec2i(v.x * ui_scale, v.y * ui_scale);
+}
+
+vec2i_t ui_scaled_screen() {
+	return vec2i_mulf(render_size(), ui_scale);
+}
+
+vec2i_t ui_scaled_pos(ui_pos_t anchor, vec2i_t offset) {
+	vec2i_t pos;
+	vec2i_t screen_size = render_size();
+
+	if (flags_is(anchor, UI_POS_LEFT)) {
+		pos.x = offset.x * ui_scale;
+	}
+	else if (flags_is(anchor, UI_POS_CENTER)) {
+		pos.x = (screen_size.x >> 1) + offset.x * ui_scale;
+	}
+	else if (flags_is(anchor, UI_POS_RIGHT)) {
+		pos.x = screen_size.x + offset.x * ui_scale;
+	}
+
+	if (flags_is(anchor, UI_POS_TOP)) {
+		pos.y = offset.y * ui_scale;
+	}
+	else if (flags_is(anchor, UI_POS_MIDDLE)) {
+		pos.y = (screen_size.y >> 1) + offset.y * ui_scale;
+	}
+	else if (flags_is(anchor, UI_POS_BOTTOM)) {
+		pos.y = screen_size.y + offset.y * ui_scale;
+	}
+
+	return pos;
+}
+
+#define char_to_glyph_index(C) (C >= '0' && C <= '9' ? (C - '0' + 26) : C - 'A')
+
+int ui_char_width(char c, ui_text_size_t size) {
+	if (c == ' ') {
+		return 8;
+	}
+	return char_set[size].glyphs[char_to_glyph_index(c)].width;
+}
+
+int ui_text_width(const char *text, ui_text_size_t size) {
+	int width = 0;
+	char_set_t *cs = &char_set[size];
+
+	for (int i = 0; text[i] != 0; i++) {
+		width += text[i] != ' '
+			? cs->glyphs[char_to_glyph_index(text[i])].width
+			: 8;
+	}
+
+	return width;
+}
+
+int ui_number_width(int num, ui_text_size_t size) {
+	char text_buffer[16];
+	text_buffer[15] = '\0';
+
+	int i;
+	for (i = 14; i > 0; i--) {
+		text_buffer[i] = '0' + (num % 10);
+		num = num / 10;
+		if (num == 0) {
+			break;
+		}
+	}
+	return ui_text_width(text_buffer + i, size);
+}
+
+void ui_draw_time(float time, vec2i_t pos, ui_text_size_t size, rgba_t color) {
+	int msec = time * 1000;
+	int tenths = (msec / 100) % 10;
+	int secs = (msec / 1000) % 60;
+	int mins = msec / (60 * 1000);
+
+	char text_buffer[8];
+	text_buffer[0] = '0' + (mins / 10) % 10;
+	text_buffer[1] = '0' + mins % 10;
+	text_buffer[2] = 'e'; // ":"
+	text_buffer[3] = '0' + secs / 10;
+	text_buffer[4] = '0' + secs % 10;
+	text_buffer[5] = 'f'; // "."
+	text_buffer[6] = '0' + tenths;
+	text_buffer[7] = '\0';
+	ui_draw_text(text_buffer, pos, size, color);
+}
+
+void ui_draw_number(int num, vec2i_t pos, ui_text_size_t size, rgba_t color) {
+	char text_buffer[16];
+	text_buffer[15] = '\0';
+
+	int i;
+	for (i = 14; i > 0; i--) {
+		text_buffer[i] = '0' + (num % 10);
+		num = num / 10;
+		if (num == 0) {
+			break;
+		}
+	}
+	ui_draw_text(text_buffer + i, pos, size, color);
+}
+
+void ui_draw_text(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color) {
+	char_set_t *cs = &char_set[size];
+
+	for (int i = 0; text[i] != 0; i++) {
+		if (text[i] != ' ') {
+			glyph_t *glyph = &cs->glyphs[char_to_glyph_index(text[i])];
+			vec2i_t size = vec2i(glyph->width, cs->height);
+			render_push_2d_tile(pos, glyph->offset, size, ui_scaled(size), color, cs->texture);
+			pos.x += glyph->width * ui_scale;
+		}
+		else {
+			pos.x += 8 * ui_scale;
+		}
+	}
+}
+
+void ui_draw_image(vec2i_t pos, uint16_t texture) {
+	vec2i_t scaled_size = ui_scaled(render_texture_size(texture));
+	render_push_2d(pos, scaled_size, rgba(128, 128, 128, 255), texture);
+}
+
+void ui_draw_icon(ui_icon_type_t icon, vec2i_t pos, rgba_t color) {
+	render_push_2d(pos, ui_scaled(render_texture_size(icon_textures[icon])), color, icon_textures[icon]);
+}
+
+void ui_draw_text_centered(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color) {
+	pos.x -= (ui_text_width(text, size) * ui_scale) >> 1;
+	ui_draw_text(text, pos, size, color);
+}
--- /dev/null
+++ b/src/wipeout/ui.h
@@ -1,0 +1,56 @@
+#ifndef UI_H
+#define UI_H
+
+#include "../types.h"
+
+typedef enum {
+	UI_SIZE_16,
+	UI_SIZE_12,
+	UI_SIZE_8,
+	UI_SIZE_MAX
+} ui_text_size_t;
+
+#define UI_COLOR_ACCENT rgba(123, 98, 12, 255)
+#define UI_COLOR_DEFAULT rgba(128, 128, 128, 255)
+
+typedef enum {
+	UI_ICON_HAND,
+	UI_ICON_CONFIRM,
+	UI_ICON_CANCEL,
+	UI_ICON_END,
+	UI_ICON_DEL,
+	UI_ICON_STAR,
+	UI_ICON_MAX
+} ui_icon_type_t;
+
+typedef enum {
+	UI_POS_LEFT   = 1 << 0,
+	UI_POS_CENTER = 1 << 1,
+	UI_POS_RIGHT =  1 << 2,
+	UI_POS_TOP =    1 << 3,
+	UI_POS_MIDDLE = 1 << 4,
+	UI_POS_BOTTOM = 1 << 5,
+} ui_pos_t;
+
+void ui_load();
+void ui_cleanup();
+
+int ui_get_scale();
+void ui_set_scale(int scale);
+vec2i_t ui_scaled(vec2i_t v);
+vec2i_t ui_scaled_screen();
+vec2i_t ui_scaled_pos(ui_pos_t anchor, vec2i_t offset);
+
+int ui_char_width(char c, ui_text_size_t size);
+int ui_text_width(const char *text, ui_text_size_t size);
+int ui_number_width(int num, ui_text_size_t size);
+
+void ui_draw_text(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color);
+void ui_draw_time(float time, vec2i_t pos, ui_text_size_t size, rgba_t color);
+void ui_draw_number(int num, vec2i_t pos, ui_text_size_t size, rgba_t color);
+
+void ui_draw_image(vec2i_t pos, uint16_t texture);
+void ui_draw_icon(ui_icon_type_t icon, vec2i_t pos, rgba_t color);
+void ui_draw_text_centered(const char *text, vec2i_t pos, ui_text_size_t size, rgba_t color);
+
+#endif
--- /dev/null
+++ b/src/wipeout/weapon.c
@@ -1,0 +1,693 @@
+#include "../mem.h"
+#include "../utils.h"
+#include "../system.h"
+
+#include "track.h"
+#include "ship.h"
+#include "weapon.h"
+#include "object.h"
+#include "game.h"
+#include "image.h"
+#include "particle.h"
+
+extern int32_t ctrlNeedTargetIcon;
+extern int ctrlnearShip;
+int16_t Shielded = 0;
+
+typedef struct weapon_t {
+	float timer;
+	ship_t *owner;
+	ship_t *target;
+	section_t *section;
+	Object *model;
+	bool active;
+
+	int16_t trail_particle;
+	int16_t track_hit_particle;
+	int16_t ship_hit_particle;
+	float trail_spawn_timer;
+
+	int16_t type;
+	vec3_t acceleration;
+	vec3_t velocity;
+	vec3_t position;
+	vec3_t angle;
+	float drag;
+
+	void (*update_func)(struct weapon_t *);
+} weapon_t;
+
+
+weapon_t *weapons;
+int weapons_active = 0;
+
+struct {
+	uint16_t reticle;
+	Object *rocket;
+	Object *mine;
+	Object *missile;
+	Object *shield;
+	Object *shield_internal;
+	Object *ebolt;
+} weapon_assets;
+
+void weapon_update_wait_for_delay(weapon_t *self);
+
+void weapon_fire_mine(ship_t *ship);
+void weapon_update_mine_wait_for_release(weapon_t *self);
+void weapon_update_mine(weapon_t *self);
+void weapon_update_mine_lights(weapon_t *self, int index);
+
+void weapon_fire_missile(ship_t *ship);
+void weapon_update_missile(weapon_t *self);
+
+void weapon_fire_rocket(ship_t *ship);
+void weapon_update_rocket(weapon_t *self);
+
+void weapon_fire_ebolt(ship_t *ship);
+void weapon_update_ebolt(weapon_t *self);
+
+void weapon_fire_shield(ship_t *ship);
+void weapon_update_shield(weapon_t *self);
+
+void weapon_fire_turbo(ship_t *ship);
+
+void invert_shield_polys(Object *shield);
+
+void weapons_load() {
+	weapons = mem_bump(sizeof(weapon_t) * WEAPONS_MAX);
+	weapon_assets.reticle = image_get_texture("wipeout/textures/target2.tim");
+
+	texture_list_t weapon_textures = image_get_compressed_textures("wipeout/common/mine.cmp");
+	weapon_assets.rocket = objects_load("wipeout/common/rock.prm", weapon_textures);
+	weapon_assets.mine = objects_load("wipeout/common/mine.prm", weapon_textures);
+	weapon_assets.missile = objects_load("wipeout/common/miss.prm", weapon_textures);
+	weapon_assets.shield = objects_load("wipeout/common/shld.prm", weapon_textures);
+	weapon_assets.shield_internal = objects_load("wipeout/common/shld.prm", weapon_textures);
+	weapon_assets.ebolt = objects_load("wipeout/common/ebolt.prm", weapon_textures);
+
+	// Invert shield polys for internal view
+	Prm poly = {.primitive = weapon_assets.shield_internal->primitives};
+	int primitives_len = weapon_assets.shield_internal->primitives_len;
+	for (int k = 0; k < primitives_len; k++) {
+		switch (poly.primitive->type) {
+		case PRM_TYPE_G3 :
+			swap(poly.g3->coords[0], poly.g3->coords[2]);
+			poly.g3 += 1;
+			break;
+
+		case PRM_TYPE_G4 :
+			swap(poly.g4->coords[0], poly.g4->coords[3]);
+			poly.g4 += 1;
+			break;
+		}
+	}
+
+	weapons_init();
+}
+
+void weapons_init() {
+	weapons_active = 0;
+}
+
+weapon_t *weapon_init(ship_t *ship) {
+	if (weapons_active == WEAPONS_MAX) {
+		return NULL;
+	}
+
+	weapon_t *weapon = &weapons[weapons_active++];
+	weapon->timer = 0;
+	weapon->owner = ship;
+	weapon->section = ship->section;
+	weapon->position = ship->position;
+	weapon->angle = ship->angle;	
+	weapon->acceleration = vec3(0, 0, 0);
+	weapon->velocity = vec3(0, 0, 0);
+	weapon->acceleration = vec3(0, 0, 0);
+	weapon->target = NULL;
+	weapon->model = NULL;
+	weapon->active = true;
+	weapon->trail_particle = PARTICLE_TYPE_NONE;
+	weapon->track_hit_particle = PARTICLE_TYPE_NONE;
+	weapon->ship_hit_particle = PARTICLE_TYPE_NONE;
+	weapon->trail_spawn_timer = 0;
+	weapon->drag = 0;
+	return weapon;
+}
+
+void weapons_fire(ship_t *ship, int weapon_type) {
+	switch (weapon_type) {
+		case WEAPON_TYPE_MINE:      weapon_fire_mine(ship); break;
+		case WEAPON_TYPE_MISSILE:   weapon_fire_missile(ship); break;
+		case WEAPON_TYPE_ROCKET:    weapon_fire_rocket(ship); break;
+		case WEAPON_TYPE_EBOLT:     weapon_fire_ebolt(ship); break;
+		case WEAPON_TYPE_SHIELD:    weapon_fire_shield(ship); break;
+		case WEAPON_TYPE_TURBO:     weapon_fire_turbo(ship); break;
+		default: die("Inavlid weapon type %d", weapon_type);
+	}
+	ship->weapon_type = WEAPON_TYPE_NONE;
+}
+
+void weapons_fire_delayed(ship_t *ship, int weapon_type) {
+	weapon_t *weapon = weapon_init(ship);
+	if (!weapon) {
+		return;
+	}
+	weapon->type = weapon_type;
+	weapon->timer = WEAPON_AI_DELAY;
+	weapon->update_func = weapon_update_wait_for_delay;
+}
+
+bool weapon_collides_with_track(weapon_t *self);
+
+void weapons_update() {
+	for (int i = 0; i < weapons_active; i++) {
+		weapon_t *weapon = &weapons[i];
+		
+		weapon->timer -= system_tick();
+		(weapon->update_func)(weapon);
+
+		// Handle projectiles
+		if (weapon->acceleration.x != 0 && weapon->acceleration.z != 0) {
+			weapon->velocity = vec3_add(weapon->velocity, vec3_mulf(weapon->acceleration, 30 * system_tick()));
+			weapon->velocity = vec3_sub(weapon->velocity, vec3_mulf(weapon->velocity, weapon->drag * 30 * system_tick()));
+			weapon->position = vec3_add(weapon->position, vec3_mulf(weapon->velocity, 30 * system_tick()));
+
+			// Move along track normal
+			track_face_t *face = track_section_get_base_face(weapon->section);
+			vec3_t face_point = face->tris[0].vertices[0].pos;
+			vec3_t face_normal = face->normal;
+			float height = vec3_distance_to_plane(weapon->position, face_point, face_normal);
+
+			if (height < 2000) {
+				weapon->position = vec3_add(weapon->position, vec3_mulf(face_normal, (200 - height) * 30 * system_tick()));
+			}
+
+			// Trail
+			if (weapon->trail_particle != PARTICLE_TYPE_NONE) {
+				weapon->trail_spawn_timer += system_tick();
+				while (weapon->trail_spawn_timer > 0) {
+					vec3_t pos = vec3_sub(weapon->position, vec3_mulf(weapon->velocity, 30 * system_tick() * weapon->trail_spawn_timer));
+					vec3_t velocity = vec3(rand_float(-128, 128), rand_float(-128, 128), rand_float(-128, 128));
+					particles_spawn(pos, weapon->trail_particle, velocity, 128);
+					weapon->trail_spawn_timer -= WEAPON_PARTICLE_SPAWN_RATE;
+				}
+			}
+
+			// Track collision
+			weapon->section = track_nearest_section(weapon->position, weapon->section, NULL);
+			if (weapon_collides_with_track(weapon)) {
+				for (int p = 0; p < 32; p++) {
+					vec3_t velocity = vec3(rand_float(-512, 512), rand_float(-512, 512), rand_float(-512, 512));
+					particles_spawn(weapon->position, weapon->track_hit_particle, velocity, 256);
+				}
+				sfx_play_at(SFX_EXPLOSION_2, weapon->position, vec3(0,0,0), 1);
+				weapon->active = false;
+			}
+		}
+
+		// If this weapon is released, we have to rewind one step
+		if (!weapon->active) {
+			weapons[i--] = weapons[--weapons_active];
+			continue;
+		}
+	}
+}
+
+void weapons_draw() {
+	mat4_t mat = mat4_identity();
+	for (int i = 0; i < weapons_active; i++) {
+		weapon_t *weapon = &weapons[i];
+		if (weapon->model) {
+			mat4_set_translation(&mat, weapon->position);
+			mat4_set_yaw_pitch_roll(&mat, weapon->angle);
+			if (weapon->model == weapon_assets.mine) {
+				weapon_update_mine_lights(weapon, i);
+			}
+			object_draw(weapon->model, &mat);
+		}
+	}
+}
+
+
+
+void weapon_set_trajectory(weapon_t *self) {
+	ship_t *ship = self->owner;
+	track_face_t *face = track_section_get_base_face(ship->section);
+
+	vec3_t face_point = face->tris[0].vertices[0].pos;
+	vec3_t target = vec3_add(ship->position, vec3_mulf(ship->dir_forward, 64));
+	float target_height = vec3_distance_to_plane(target, face_point, face->normal);
+	float ship_height = vec3_distance_to_plane(target, face_point, face->normal);
+
+	float nudge = target_height * 0.95 - ship_height;
+
+	self->acceleration = vec3_sub(vec3_sub(target, vec3_mulf(face->normal, nudge)), ship->position);
+	self->velocity = vec3_mulf(ship->velocity, 0.015625);
+	self->angle = ship->angle;
+}
+
+void weapon_follow_target(weapon_t *self) {
+	vec3_t angular_velocity = vec3(0, 0, 0);
+	if (self->target) {
+		vec3_t dir = vec3_mulf(vec3_sub(self->target->position, self->position), 0.125 * 30 * system_tick());
+		float height = sqrt(dir.x * dir.x + dir.z * dir.z);
+		angular_velocity.y = -atan2(dir.x, dir.z) - self->angle.y;
+		angular_velocity.x = -atan2(dir.y, height) - self->angle.x;
+	}
+
+	angular_velocity = vec3_wrap_angle(angular_velocity);
+	self->angle = vec3_add(self->angle, vec3_mulf(angular_velocity, 30 * system_tick() * 0.25));
+	self->angle = vec3_wrap_angle(self->angle);
+
+	self->acceleration.x = -sin(self->angle.y) * cos(self->angle.x) * 256;
+	self->acceleration.y = -sin(self->angle.x) * 256;
+	self->acceleration.z = cos(self->angle.y) * cos(self->angle.x) * 256;
+}
+
+ship_t *weapon_collides_with_ship(weapon_t *self) {
+	for (int i = 0; i < NUM_PILOTS; i++) {
+		ship_t *ship = &g.ships[i];
+		if (ship == self->owner) {
+			continue;
+		}
+
+		float distance = vec3_len(vec3_sub(ship->position, self->position));
+		if (distance < 512) {
+			for (int p = 0; p < 32; p++) {
+				vec3_t velocity = vec3(rand_float(-512, 512), rand_float(-512, 512), rand_float(-512, 512));
+				velocity = vec3_add(velocity, vec3_mulf(ship->velocity, 0.25));
+				particles_spawn(self->position, self->ship_hit_particle, velocity, 256);
+			}
+			return ship;
+		}
+	}
+
+	return NULL;
+}
+
+
+bool weapon_collides_with_track(weapon_t *self) {
+	if (flags_is(self->section->flags, SECTION_JUMP)) {
+		return false;
+	}
+
+	track_face_t *face = g.track.faces + self->section->face_start;
+	for (int i = 0; i < self->section->face_count; i++) {
+		vec3_t face_point = face->tris[0].vertices[0].pos;
+		float distance = vec3_distance_to_plane(self->position, face_point, face->normal);
+		if (distance < 0) {
+			return true;
+		}
+		face++;
+	}
+
+	return false;
+}
+
+void weapon_update_wait_for_delay(weapon_t *self) {
+	if (self->timer <= 0) {
+		weapons_fire(self->owner, self->type);
+		self->active = false;
+	}
+}
+
+
+void weapon_fire_mine(ship_t *ship) {
+	float timer = 0;
+	for (int i = 0; i < WEAPON_MINE_COUNT; i++) {
+		weapon_t *self = weapon_init(ship);
+		if (!self) {
+			return;
+		}
+		timer += WEAPON_MINE_RELEASE_RATE;
+		self->timer = timer;
+		self->update_func = weapon_update_mine_wait_for_release;
+	}
+}
+
+
+
+void weapon_update_mine_wait_for_release(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->timer = WEAPON_MINE_DURATION;
+		self->update_func = weapon_update_mine;
+		self->model = weapon_assets.mine;
+		self->position = self->owner->position;
+		self->angle.y = rand_float(0, M_PI * 2);
+
+		self->trail_particle = PARTICLE_TYPE_NONE;
+		self->track_hit_particle = PARTICLE_TYPE_NONE;
+		self->ship_hit_particle = PARTICLE_TYPE_FIRE;
+
+		if (self->owner->pilot == g.pilot) {
+			sfx_play(SFX_MINE_DROP);
+		}
+	}
+}
+
+void weapon_update_mine_lights(weapon_t *self, int index) {
+	Prm prm = {.primitive = self->model->primitives};
+
+	uint8_t r = sin(system_cycle_time() * M_PI * 2 + index * 0.66) * 128 + 128;
+	for (int i = 0; i < 8; i++) {
+		switch (prm.primitive->type) {
+		case PRM_TYPE_GT3:
+			prm.gt3->colour[0].as_rgba.r = 230;
+			prm.gt3->colour[1].as_rgba.r = r;
+			prm.gt3->colour[2].as_rgba.r = r;
+			prm.gt3->colour[0].as_rgba.g = 0;
+			prm.gt3->colour[1].as_rgba.g = 0x40;
+			prm.gt3->colour[2].as_rgba.g = 0x40;
+			prm.gt3->colour[0].as_rgba.b = 0;
+			prm.gt3->colour[1].as_rgba.b = 0;
+			prm.gt3->colour[2].as_rgba.b = 0;
+			prm.gt3 += 1;
+			break;
+		}
+	}
+}
+
+void weapon_update_mine(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->active = false;
+		return;
+	}
+
+	// TODO: oscilate perpendicular to track!?
+	self->angle.y += system_tick();
+
+	ship_t *ship = weapon_collides_with_ship(self);
+	if (ship) {
+		sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
+		self->active = false;
+		if (flags_not(ship->flags, SHIP_SHIELDED)) {
+			if (ship->pilot == g.pilot) {
+				ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.125));
+				// SetShake(20); // FIXME
+			}
+			else {
+				ship->speed = ship->speed * 0.125;
+			}
+		}
+	}	
+}
+
+
+void weapon_fire_missile(ship_t *ship) {
+	weapon_t *self = weapon_init(ship);
+	if (!self) {
+		return;
+	}
+
+	self->timer = WEAPON_MISSILE_DURATION;
+	self->model = weapon_assets.missile;
+	self->update_func = weapon_update_missile;
+	self->trail_particle = PARTICLE_TYPE_SMOKE;
+	self->track_hit_particle = PARTICLE_TYPE_FIRE_WHITE;
+	self->ship_hit_particle = PARTICLE_TYPE_FIRE;
+	self->target = ship->weapon_target;
+	self->drag = 0.25;
+	weapon_set_trajectory(self);
+
+	if (self->owner->pilot == g.pilot) {
+		sfx_play(SFX_MISSILE_FIRE);
+	}
+}
+
+void weapon_update_missile(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->active = false;
+		return;
+	}
+
+	weapon_follow_target(self);
+
+	// Collision with other ships
+	ship_t *ship = weapon_collides_with_ship(self);
+	if (ship) {
+		sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
+		self->active = false;
+
+		if (flags_not(ship->flags, SHIP_SHIELDED)) {
+			if (ship->pilot == g.pilot) {
+				ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.75));
+				ship->angular_velocity.z += rand_float(-0.1, 0.1);
+				ship->turn_rate_from_hit = rand_float(-0.1, 0.1);
+				// SetShake(20);  // FIXME
+			}
+			else {
+				ship->speed = ship->speed * 0.03125;
+				ship->angular_velocity.z += 10 * M_PI;
+				ship->turn_rate_from_hit = rand_float(-M_PI, M_PI);
+			}
+		}
+	}
+}
+
+void weapon_fire_rocket(ship_t *ship) {
+	weapon_t *self = weapon_init(ship);
+	if (!self) {
+		return;
+	}
+
+	self->timer = WEAPON_ROCKET_DURATION;
+	self->model = weapon_assets.rocket;
+	self->update_func = weapon_update_rocket;
+	self->trail_particle = PARTICLE_TYPE_SMOKE;
+	self->track_hit_particle = PARTICLE_TYPE_FIRE_WHITE;
+	self->ship_hit_particle = PARTICLE_TYPE_FIRE;
+	self->drag = 0.03125;
+	weapon_set_trajectory(self);
+
+	if (self->owner->pilot == g.pilot) {
+		sfx_play(SFX_MISSILE_FIRE);
+	}
+}
+
+void weapon_update_rocket(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->active = false;
+		return;
+	}
+
+	// Collision with other ships
+	ship_t *ship = weapon_collides_with_ship(self);
+	if (ship) {
+		sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
+		self->active = false;
+
+		if (flags_not(ship->flags, SHIP_SHIELDED)) {
+			if (ship->pilot == g.pilot) {
+				ship->velocity = vec3_sub(ship->velocity, vec3_mulf(ship->velocity, 0.75));
+				ship->angular_velocity.z += rand_float(-0.1, 0.1);;
+				ship->turn_rate_from_hit = rand_float(-0.1, 0.1);;
+				// SetShake(20);  // FIXME
+			}
+			else {
+				ship->speed = ship->speed * 0.03125;
+				ship->angular_velocity.z += 10 * M_PI;
+				ship->turn_rate_from_hit = rand_float(-M_PI, M_PI);
+			}
+		}
+	}
+}
+
+
+void weapon_fire_ebolt(ship_t *ship) {
+	weapon_t *self = weapon_init(ship);
+	if (!self) {
+		return;
+	}
+
+	self->timer = WEAPON_EBOLT_DURATION;
+	self->model = weapon_assets.ebolt;
+	self->update_func = weapon_update_ebolt;
+	self->trail_particle = PARTICLE_TYPE_EBOLT;
+	self->track_hit_particle = PARTICLE_TYPE_EBOLT;
+	self->ship_hit_particle = PARTICLE_TYPE_GREENY;
+	self->target = ship->weapon_target;
+	self->drag = 0.25;
+	weapon_set_trajectory(self);
+
+	if (self->owner->pilot == g.pilot) {
+		sfx_play(SFX_EBOLT);
+	}
+}
+
+void weapon_update_ebolt(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->active = false;
+		return;
+	}
+
+	weapon_follow_target(self);
+
+	// Collision with other ships
+	ship_t *ship = weapon_collides_with_ship(self);
+	if (ship) {
+		sfx_play_at(SFX_EXPLOSION_1, self->position, vec3(0,0,0), 1);
+		self->active = false;
+
+		if (flags_not(ship->flags, SHIP_SHIELDED)) {
+			flags_add(ship->flags, SHIP_ELECTROED);
+			ship->ebolt_timer = WEAPON_EBOLT_DURATION;
+		}
+	}
+}
+
+void weapon_fire_shield(ship_t *ship) {
+	weapon_t *self = weapon_init(ship);
+	if (!self) {
+		return;
+	}
+
+	self->timer = WEAPON_SHIELD_DURATION;
+	self->model = weapon_assets.shield;
+	self->update_func = weapon_update_shield;
+
+	flags_add(self->owner->flags, SHIP_SHIELDED);
+}
+
+void weapon_update_shield(weapon_t *self) {
+	if (self->timer <= 0) {
+		self->active = false;
+
+		if (self->owner->pilot == g.pilot) {
+			// KillNote(SHIELDS);
+		}
+		flags_rm(self->owner->flags, SHIP_SHIELDED);
+		return;
+	}
+
+
+	if (flags_is(self->owner->flags, SHIP_VIEW_INTERNAL)) {
+		self->position = ship_cockpit(self->owner);
+		self->model = weapon_assets.shield_internal;
+	}
+	else {
+		self->position = self->owner->position;
+		self->model = weapon_assets.shield;
+	}
+	self->angle = self->owner->angle;
+
+
+	Prm poly = {.primitive = self->model->primitives};
+	int primitives_len = self->model->primitives_len;
+	uint8_t col0, col1, col2, col3;
+	int16_t *coords;
+	uint8_t shield_alpha = 48;
+
+	// FIXME: this looks kinda close to the PSX original!?
+	float color_timer = self->timer * 0.05;
+	for (int k = 0; k < primitives_len; k++) {
+		switch (poly.primitive->type) {
+		case PRM_TYPE_G3 :
+			coords = poly.g3->coords;
+
+			col0 = sin(color_timer * coords[0]) * 127 + 128;
+			col1 = sin(color_timer * coords[1]) * 127 + 128;
+			col2 = sin(color_timer * coords[2]) * 127 + 128;
+
+			poly.g3->colour[0].as_rgba.r = col0;
+			poly.g3->colour[0].as_rgba.g = col0;
+			poly.g3->colour[0].as_rgba.b = 255;
+			poly.g3->colour[0].as_rgba.a = shield_alpha;
+
+			poly.g3->colour[1].as_rgba.r = col1;
+			poly.g3->colour[1].as_rgba.g = col1;
+			poly.g3->colour[1].as_rgba.b = 255;
+			poly.g3->colour[1].as_rgba.a = shield_alpha;
+
+			poly.g3->colour[2].as_rgba.r = col2;
+			poly.g3->colour[2].as_rgba.g = col2;
+			poly.g3->colour[2].as_rgba.b = 255;
+			poly.g3->colour[2].as_rgba.a = shield_alpha;
+			poly.g3 += 1;
+			break;
+
+		case PRM_TYPE_G4 :
+			coords = poly.g4->coords;
+
+			col0 = sin(color_timer * coords[0]) * 127 + 128;
+			col1 = sin(color_timer * coords[1]) * 127 + 128;
+			col2 = sin(color_timer * coords[2]) * 127 + 128;
+			col3 = sin(color_timer * coords[3]) * 127 + 128;
+
+			poly.g4->colour[0].as_rgba.r = col0;
+			poly.g4->colour[0].as_rgba.g = col0;
+			poly.g4->colour[0].as_rgba.b = 255;
+			poly.g4->colour[0].as_rgba.a = shield_alpha;
+
+			poly.g4->colour[1].as_rgba.r = col1;
+			poly.g4->colour[1].as_rgba.g = col1;
+			poly.g4->colour[1].as_rgba.b = 255;
+			poly.g4->colour[1].as_rgba.a = shield_alpha;
+
+			poly.g4->colour[2].as_rgba.r = col2;
+			poly.g4->colour[2].as_rgba.g = col2;
+			poly.g4->colour[2].as_rgba.b = 255;
+			poly.g4->colour[2].as_rgba.a = shield_alpha;
+
+			poly.g4->colour[3].as_rgba.r = col3;
+			poly.g4->colour[3].as_rgba.g = col3;
+			poly.g4->colour[3].as_rgba.b = 255;
+			poly.g4->colour[3].as_rgba.a = shield_alpha;
+			poly.g4 += 1;
+			break;
+		}
+	}
+}
+
+
+void weapon_fire_turbo(ship_t *ship) {
+	ship->velocity = vec3_add(ship->velocity, vec3_mulf(ship->dir_forward, 39321)); // unitVecNose.vx) << 3) * FR60) / 50
+	
+	if (ship->pilot == g.pilot) {
+		sfx_t *sfx = sfx_play(SFX_MISSILE_FIRE);
+		sfx->pitch = 0.25;
+	}
+}
+
+int weapon_get_random_type(int type_class) {
+	if (type_class == WEAPON_CLASS_ANY) {
+		int index = rand_int(0, 65);
+		if (index < 17) {
+			return WEAPON_TYPE_ROCKET;
+		}
+		else if (index < 35) {
+			return WEAPON_TYPE_MINE;
+		}
+		else if (index < 45) {
+			return WEAPON_TYPE_SHIELD;
+		}
+		else if (index < 53) {
+			return WEAPON_TYPE_MISSILE;
+		}
+		else if (index < 59) {
+			return WEAPON_TYPE_TURBO;
+		}
+		else {
+			return WEAPON_TYPE_EBOLT;
+		}
+	}
+	else if (type_class == WEAPON_CLASS_PROJECTILE) { 
+		int index = rand_int(0, 60);
+		if (index < 27) {
+			return WEAPON_TYPE_ROCKET;
+		}
+		else if (index < 40) {
+			return WEAPON_TYPE_MISSILE;
+		}
+		else if (index < 50) {
+			return WEAPON_TYPE_TURBO;
+		}
+		else {
+			return WEAPON_TYPE_EBOLT;
+		}
+	}
+	else {
+		die("Unknown WEAPON_CLASS_ %d", type_class);
+	}
+}
+
--- /dev/null
+++ b/src/wipeout/weapon.h
@@ -1,0 +1,51 @@
+#ifndef WEAPON_H
+#define WEAPON_H
+
+#define WEAPONS_MAX 64
+
+#define WEAPON_MINE_DURATION (450 * (1.0/30.0))
+#define WEAPON_ROCKET_DURATION (200 * (1.0/30.0))
+#define WEAPON_EBOLT_DURATION (140 * (1.0/30.0))
+#define WEAPON_REV_CON_DURATION (60 * (1.0/30.0))
+#define WEAPON_MISSILE_DURATION (200 * (1.0/30.0))
+#define WEAPON_SHIELD_DURATION (200 * (1.0/30.0))
+#define WEAPON_FLARE_DURATION (200 * (1.0/30.0))
+#define WEAPON_SPECIAL_DURATION (400 * (1.0/30.0))
+
+#define WEAPON_MINE_RELEASE_RATE (3 * (1.0/30.0))
+#define WEAPON_DELAY (40 * (1.0/30.0))
+
+#define WEAPON_TYPE_NONE      0
+#define WEAPON_TYPE_MINE      1
+#define WEAPON_TYPE_MISSILE   2
+#define WEAPON_TYPE_ROCKET    3
+#define WEAPON_TYPE_SPECIAL   4
+#define WEAPON_TYPE_EBOLT     5
+#define WEAPON_TYPE_FLARE     6
+#define WEAPON_TYPE_REV_CON   7
+#define WEAPON_TYPE_SHIELD    8
+#define WEAPON_TYPE_TURBO     9
+#define WEAPON_TYPE_MAX      10
+
+#define WEAPON_MINE_COUNT 5
+
+#define WEAPON_HIT_NONE 0
+#define WEAPON_HIT_SHIP 1
+#define WEAPON_HIT_TRACK 2
+
+#define WEAPON_PARTICLE_SPAWN_RATE 0.011
+#define WEAPON_AI_DELAY 1.1
+
+#define WEAPON_CLASS_ANY 1
+#define WEAPON_CLASS_PROJECTILE 2
+
+
+void weapons_load();
+void weapons_init();
+void weapons_fire(ship_t *ship, int weapon_type);
+void weapons_fire_delayed(ship_t *ship, int weapon_type);
+void weapons_update();
+void weapons_draw();
+int weapon_get_random_type(int type_class);
+
+#endif