From 560bd53d0e893a91a1bbc37fdaa1207ca2952a18 Mon Sep 17 00:00:00 2001
From: Thomas Voss <mail@thomasvoss.com>
Date: Fri, 16 Feb 2024 01:04:44 +0100
Subject: Implement a working SDL GUI

---
 src/ahoy/emulator.c | 184 ++++++++++++++++-----------------
 src/ahoy/emulator.h |  21 ++++
 src/ahoy/gui.c      | 289 ++++++++++++++++++++++++++++++++++++++++++++--------
 src/ahoy/gui.h      |  14 +++
 src/ahoy/main.c     |  45 ++++++++
 5 files changed, 419 insertions(+), 134 deletions(-)

diff --git a/src/ahoy/emulator.c b/src/ahoy/emulator.c
index b04e2ef..92f3a1c 100644
--- a/src/ahoy/emulator.c
+++ b/src/ahoy/emulator.c
@@ -9,6 +9,7 @@
 
 #include "cerr.h"
 #include "emulator.h"
+#include "gui.h"
 #include "macros.h"
 
 #define MEM_RESERVED 0x200
@@ -18,23 +19,7 @@
 static void opexec(uint16_t);
 [[noreturn]] static void badins(uint16_t);
 
-/* Uppercase variables in this file will be used to represent registers.  The
-   following registers exist:
-
-   Vx — 16 general-purpose registers
-   DT — delay timer
-   ST — sound timer
-   SP — stack pointer
-   PC — program counter
-   I  — register to hold addresses
-   */
-static uint8_t V[16];
-static uint8_t DT, ST, SP;
-static uint16_t PC, I;
-
-static bool kbd[16];
-static uint16_t callstack[16];
-static uint64_t screen[32];
+struct chip8 c8;
 
 /* Preload font into memory */
 static uint8_t mem[MEM_TOTAL] = {
@@ -56,21 +41,6 @@ static uint8_t mem[MEM_TOTAL] = {
 	0xF0, 0x80, 0xF0, 0x80, 0x80, /* F */
 };
 
-static void
-scrdrw(void)
-{
-	fputs("\33[2J", stdout);
-	for (size_t i = 0; i < lengthof(screen); i++) {
-		for (size_t j = UINT64_WIDTH; j-- > 0;) {
-			char buf[U8_LEN_MAX];
-			bool bitset = (screen[i] & ((uint64_t)1 << j)) != 0;
-			int w = rtou8(buf, bitset ? U'█' : U' ', sizeof(buf));
-			fwrite(buf, 1, w, stdout);
-		}
-		putchar('\n');
-	}
-}
-
 void
 emuinit(struct u8view prog)
 {
@@ -81,8 +51,8 @@ emuinit(struct u8view prog)
 		     (double)prog.len / 1024, MEM_FREE);
 	}
 
-	PC = MEM_RESERVED;
-	memcpy(mem + PC, prog.p, prog.len);
+	c8.PC = MEM_RESERVED;
+	memcpy(mem + c8.PC, prog.p, prog.len);
 
 	if (clock_gettime(CLOCK_REALTIME, &tp) == -1)
 		die("clock_gettime");
@@ -92,9 +62,8 @@ emuinit(struct u8view prog)
 void
 emutick(void)
 {
-	opexec((mem[PC] << 8) | mem[PC + 1]);
-	scrdrw();
-	PC += 2;
+	opexec((mem[c8.PC] << 8) | mem[c8.PC + 1]);
+	c8.PC += 2;
 }
 
 void
@@ -104,12 +73,13 @@ opexec(uint16_t op)
 	case 0x0:
 		switch (op) {
 		case 0x00E0:
-			memset(screen, 0, sizeof(screen));
+			memset(c8.screen, 0, sizeof(c8.screen));
+			c8.needs_redraw = true;
 			break;
 		case 0x00EE:
-			if (SP == 0)
+			if (c8.SP == 0)
 				diex("%s: stack pointer underflow", "TODO");
-			PC = callstack[--SP];
+			c8.PC = c8.callstack[--c8.SP];
 			break;
 		default:
 			/* sys instruction is ignored */
@@ -118,47 +88,47 @@ opexec(uint16_t op)
 
 	case 0x1:
 		/* -2 because we +2 each iteration */
-		PC = (op & 0xFFF) - 2;
+		c8.PC = (op & 0xFFF) - 2;
 		break;
 
 	case 0x2:
-		if (SP == lengthof(callstack))
+		if (c8.SP == lengthof(c8.callstack))
 			diex("%s: stack pointer overflow", "TODO");
-		callstack[SP++] = PC;
-		PC = (op & 0xFFF) - 2;
+		c8.callstack[c8.SP++] = c8.PC;
+		c8.PC = (op & 0xFFF) - 2;
 		break;
 
 	case 0x3: {
 		unsigned x = (op & 0x0F00) >> 8;
-		if (V[x] == (op & 0xFF))
-			PC += 2;
+		if (c8.V[x] == (op & 0xFF))
+			c8.PC += 2;
 		break;
 	}
 
 	case 0x4: {
 		unsigned x = (op & 0x0F00) >> 8;
-		if (V[x] != (op & 0xFF))
-			PC += 2;
+		if (c8.V[x] != (op & 0xFF))
+			c8.PC += 2;
 		break;
 	}
 
 	case 0x5: {
 		unsigned x = (op & 0x0F00) >> 8;
 		unsigned y = (op & 0x00F0) >> 4;
-		if (V[x] == V[y])
-			PC += 2;
+		if (c8.V[x] == c8.V[y])
+			c8.PC += 2;
 		break;
 	}
 
 	case 0x6: {
 		unsigned x = (op & 0x0F00) >> 8;
-		V[x] = op & 0xFF;
+		c8.V[x] = op & 0xFF;
 		break;
 	}
 
 	case 0x7: {
 		unsigned x = (op & 0x0F00) >> 8;
-		V[x] += op & 0xFF;
+		c8.V[x] += op & 0xFF;
 		break;
 	}
 
@@ -168,38 +138,38 @@ opexec(uint16_t op)
 
 		switch (op & 0xF) {
 		case 0x0:
-			V[x] = V[y];
+			c8.V[x] = c8.V[y];
 			break;
 		case 0x1:
-			V[x] |= V[y];
+			c8.V[x] |= c8.V[y];
 			break;
 		case 0x2:
-			V[x] &= V[y];
+			c8.V[x] &= c8.V[y];
 			break;
 		case 0x3:
-			V[x] ^= V[y];
+			c8.V[x] ^= c8.V[y];
 			break;
 		case 0x4: {
-			unsigned n = V[x] + V[y];
-			V[x] = n;
-			V[0xF] = n > UINT8_MAX;
+			unsigned n = c8.V[x] + c8.V[y];
+			c8.V[x] = n;
+			c8.V[0xF] = n > UINT8_MAX;
 			break;
 		}
 		case 0x5:
-			V[0xF] = V[x] > V[y];
-			V[x] -= V[y];
+			c8.V[0xF] = c8.V[x] > c8.V[y];
+			c8.V[x] -= c8.V[y];
 			break;
 		case 0x6:
-			V[0xF] = V[x] & 1;
-			V[x] >>= 1;
+			c8.V[0xF] = c8.V[x] & 1;
+			c8.V[x] >>= 1;
 			break;
 		case 0x7:
-			V[0xF] = V[y] > V[x];
-			V[x] = V[y] - V[x];
+			c8.V[0xF] = c8.V[y] > c8.V[x];
+			c8.V[x] = c8.V[y] - c8.V[x];
 			break;
 		case 0xE:
-			V[0xF] = V[x] & 0x80;
-			V[x] <<= 1;
+			c8.V[0xF] = c8.V[x] & 0x80;
+			c8.V[x] <<= 1;
 			break;
 		default:
 			badins(op);
@@ -211,22 +181,22 @@ opexec(uint16_t op)
 	case 0x9: {
 		unsigned x = (op & 0x0F00) >> 8;
 		unsigned y = (op & 0x00F0) >> 4;
-		if (V[x] != V[y])
-			PC += 2;
+		if (c8.V[x] != c8.V[y])
+			c8.PC += 2;
 		break;
 	}
 
 	case 0xA:
-		I = op & 0xFFF;
+		c8.I = op & 0xFFF;
 		break;
 
 	case 0xB:
-		PC = (op & 0xFFF) + V[0] - 2;
+		c8.PC = (op & 0xFFF) + c8.V[0] - 2;
 		break;
 
 	case 0xC: {
 		unsigned x = (op & 0x0F00) >> 8;
-		V[x] = rand() & (op & 0xFF);
+		c8.V[x] = rand() & (op & 0xFF);
 		break;
 	}
 
@@ -237,17 +207,19 @@ opexec(uint16_t op)
 
 		for (unsigned i = 0; i < n; i++) {
 			/* TODO: bounds check? */
-			uint8_t spr_row = mem[I + i];
-			uint8_t scr_row = V[y] + i;
+			uint8_t spr_row = mem[c8.I + i];
+			uint8_t scr_row = c8.V[y] + i;
 			uint64_t msk;
 
-			if (scr_row >= lengthof(screen))
+			if (scr_row >= lengthof(c8.screen))
 				break;
 
-			msk = ((uint64_t)spr_row << (UINT64_WIDTH - 8)) >> V[x];
-			V[0xF] = screen[scr_row] & msk;
-			screen[scr_row] ^= msk;
+			msk = ((uint64_t)spr_row << (UINT64_WIDTH - 8)) >> c8.V[x];
+			c8.V[0xF] = (bool)(c8.screen[scr_row] & msk);
+			c8.screen[scr_row] ^= msk;
 		}
+
+		c8.needs_redraw = true;
 		break;
 	}
 
@@ -256,12 +228,12 @@ opexec(uint16_t op)
 
 		switch (op & 0xFF) {
 		case 0x9E:
-			if (V[x] < lengthof(kbd) && kbd[V[x]])
-				PC += 2;
+			if (c8.V[x] < lengthof(c8.kbd) && c8.kbd[c8.V[x]])
+				c8.PC += 2;
 			break;
 		case 0xA1:
-			if (V[x] >= lengthof(kbd) || !kbd[V[x]])
-				PC += 2;
+			if (c8.V[x] >= lengthof(c8.kbd) || !c8.kbd[c8.V[x]])
+				c8.PC += 2;
 			break;
 		default:
 			badins(op);
@@ -274,34 +246,58 @@ opexec(uint16_t op)
 
 		switch (op & 0xFF) {
 		case 0x07:
-			V[x] = DT;
+			c8.V[x] = c8.DT;
 			break;
-		case 0x0A:
-			badins(op);
+		case 0x0A: {
+			static bool any_key_pressed = false;
+			static uint8_t key = 0xFF;
+
+			for (uint8_t i = 0; key == 0xFF && i < lengthof(c8.kbd); i++) {
+				if (c8.kbd[i]) {
+					key = i;
+					any_key_pressed = true;
+					break;
+				}
+			}
+
+			if (!any_key_pressed)
+				c8.PC -= 2;
+			else {
+				if (c8.kbd[key])
+					c8.PC -= 2;
+				else {
+					c8.V[x] = key;
+					key = 0xFF;
+					any_key_pressed = false;
+				}
+			}
 			break;
+		}
 		case 0x15:
-			DT = V[x];
+			c8.DT = c8.V[x];
 			break;
 		case 0x18:
-			ST = V[x];
+			c8.ST = c8.V[x];
 			break;
 		case 0x1E:
-			I += V[x];
+			c8.I += c8.V[x];
 			break;
 		case 0x29:
 			/* Each character sprite is 5 bytes */
-			I = V[x] * 5;
+			c8.I = c8.V[x] * 5;
 			break;
 		case 0x33:
-			mem[I + 0] = V[x] / 100 % 10;
-			mem[I + 1] = V[x] / 10 % 10;
-			mem[I + 2] = V[x] / 1 % 10;
+			mem[c8.I + 0] = c8.V[x] / 100 % 10;
+			mem[c8.I + 1] = c8.V[x] / 10 % 10;
+			mem[c8.I + 2] = c8.V[x] / 1 % 10;
 			break;
 		case 0x55:
-			memcpy(mem + I, V, x);
+			memcpy(mem + c8.I, c8.V, x);
+			c8.I += x;
 			break;
 		case 0x65:
-			memcpy(V, mem + I, x);
+			memcpy(c8.V, mem + c8.I, x);
+			c8.I += x;
 			break;
 		default:
 			badins(op);
diff --git a/src/ahoy/emulator.h b/src/ahoy/emulator.h
index e9ba11f..56b5545 100644
--- a/src/ahoy/emulator.h
+++ b/src/ahoy/emulator.h
@@ -3,7 +3,28 @@
 
 #include <mbstring.h>
 
+/* Uppercase variables represent registers.  The following registers exist:
+
+   Vx — 16 general-purpose registers
+   DT — delay timer
+   ST — sound timer
+   SP — stack pointer
+   PC — program counter
+   I  — register to hold addresses
+   */
+struct chip8 {
+	bool needs_redraw;
+	bool kbd[16];
+	uint8_t V[16];
+	uint8_t DT, ST, SP;
+	uint16_t PC, I;
+	uint16_t callstack[16];
+	uint64_t screen[32];
+};
+
 void emuinit(struct u8view);
 void emutick(void);
 
+extern struct chip8 c8;
+
 #endif /* !AHOY_AHOY_EMULATOR_H */
diff --git a/src/ahoy/gui.c b/src/ahoy/gui.c
index 1f3b748..fd6a660 100644
--- a/src/ahoy/gui.c
+++ b/src/ahoy/gui.c
@@ -1,71 +1,280 @@
 #include <err.h>
+#include <stddef.h>
+#include <stdint.h>
 
 #include <SDL2/SDL.h>
 
 #include "cerr.h"
+#include "emulator.h"
+#include "gui.h"
+#include "macros.h"
 
-#define SCR_SCALE 10
+#define SCR_SCALE 20
 #define SCR_WDTH  64
 #define SCR_HIGH  32
 
-#define diesx(fmt, ...) \
-	diex(fmt ": %s" __VA_OPT__(,) __VA_ARGS__, SDL_GetError())
+#define diesx(fmt) diex(fmt ": %s", SDL_GetError())
 
-SDL_Window *win;
-SDL_Renderer *rndr;
-SDL_Texture *txtr;
-SDL_AudioDeviceID adev;
-unsigned long asmpcnt;
-struct {
-	void *p;
-	size_t sz;
-} abuf;
+static void audio_callback(void *, uint8_t *, int);
+
+guistate gs;
+static SDL_Window *win;
+static SDL_Renderer *rndr;
+static SDL_AudioDeviceID adev;
 
 void
 wininit(void)
 {
-	SDL_AudioSpec have;
-	SDL_AudioSpec want = {
-		.freq = 64 * 60,
-		.format = AUDIO_F32,
-		.channels = 1,
-		.samples = 64,
-	};
+	SDL_AudioSpec want, have;
 
-	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0)
+	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0)
 		diesx("failed to initialize SDL");
 
-	win = SDL_CreateWindow("Ahoy!", SDL_WINDOWPOS_CENTERED,
+	win = SDL_CreateWindow("Ahoy! CHIP-8 Emulator", SDL_WINDOWPOS_CENTERED,
 	                       SDL_WINDOWPOS_CENTERED, SCR_WDTH * SCR_SCALE,
-	                       SCR_HIGH * SCR_SCALE, SDL_WINDOW_RESIZABLE);
+	                       SCR_HIGH * SCR_SCALE, 0);
 	if (!win)
 		diesx("failed to create window");
 
-	rndr = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED
-	                                 | SDL_RENDERER_PRESENTVSYNC);
-	if (!rndr)
+	if (!(rndr = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED)))
 		diesx("failed to create renderer");
 
-	txtr = SDL_CreateTexture(rndr, SDL_PIXELFORMAT_RGBA8888,
-	                         SDL_TEXTUREACCESS_STREAMING, SCR_WDTH, SCR_HIGH);
-	if (!txtr)
-		diesx("failed to create texture");
+	want = (SDL_AudioSpec){
+		.freq = 44100,
+		.format = AUDIO_S16LSB,
+		.channels = 1,
+		.samples = 512,
+		.callback = audio_callback,
+	};
+
+	if (!(adev = SDL_OpenAudioDevice(nullptr, 0, &want, &have, 0)))
+		warnx("failed to get audio device: %s", SDL_GetError());
+	if ((want.format != have.format) || (want.channels != have.channels))
+		warnx("failed to get desired audio spec");
+}
+
+void
+winfree(void)
+{
+	SDL_DestroyRenderer(rndr);
+	SDL_DestroyWindow(win);
+	SDL_CloseAudioDevice(adev);
+	SDL_Quit();
+}
+
+void
+winclr(void)
+{
+	SDL_SetRenderDrawColor(rndr, 0, 0, 0, UINT8_MAX);
+	SDL_RenderClear(rndr);
+}
+
+void
+windrw(void)
+{
+	SDL_Rect r = {
+		.x = 0,
+		.y = 0,
+		.w = SCR_SCALE,
+		.h = SCR_SCALE,
+	};
+	static const uint64_t cols[] = {
+		63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48,
+		47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32,
+		31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16,
+		15, 14, 13, 12, 11, 10, 9,  8,  7,  6,  5,  4,  3,  2,  1,  0,
+	};
+
+	c8.needs_redraw = false;
 
-	adev = SDL_OpenAudioDevice(nullptr, 0, &want, &have,
-	                           SDL_AUDIO_ALLOW_FORMAT_CHANGE);
-	if (!adev) {
-		warnx("failed to open audio device: %s", SDL_GetError());
-		return;
+	for (size_t i = 0; i < lengthof(c8.screen); i++) {
+		if (c8.screen[i])
+			goto noclr;
 	}
+	winclr();
+	return;
 
-	asmpcnt = have.samples * have.channels;
-	abuf.sz = asmpcnt * 4;
-	if (!(abuf.p = malloc(abuf.sz)))
-		die("malloc");
-	SDL_PauseAudioDevice(adev, 0);
+noclr:
+	for (size_t i = 0; i < lengthof(c8.screen); i++) {
+		for (size_t j = 64; j-- > 0;) {
+			bool set = ((uint64_t)1 << j) & c8.screen[i];
+			r.x = cols[j] * SCR_SCALE;
+			r.y = i * SCR_SCALE;
+			if (set)
+				SDL_SetRenderDrawColor(rndr, 0, UINT8_MAX, 0, UINT8_MAX);
+			else
+				SDL_SetRenderDrawColor(rndr, 0, 0, 0, UINT8_MAX);
+			SDL_RenderFillRect(rndr, &r);
+		}
+	}
+
+	SDL_RenderPresent(rndr);
 }
 
 void
-winfree(void)
+readkb(void)
+{
+	SDL_Event e;
+
+	while (SDL_PollEvent(&e)) {
+		switch (e.type) {
+		case SDL_QUIT:
+			gs = GUI_STOP;
+			break;
+
+		case SDL_KEYDOWN:
+			switch (e.key.keysym.sym) {
+			case SDLK_SPACE:
+				gs = gs == GUI_RUNNING ? GUI_PAUSED : GUI_RUNNING;
+				break;
+
+			// case SDLK_EQUALS:
+			// 	init_chip8(chip8, *config, chip8->rom_name);
+			// 	break;
+			//
+			// case SDLK_j:
+			// 	// 'j': Decrease color lerp rate
+			// 	if (config->color_lerp_rate > 0.1)
+			// 		config->color_lerp_rate -= 0.1;
+			// 	break;
+			//
+			// case SDLK_k:
+			// 	// 'k': Increase color lerp rate
+			// 	if (config->color_lerp_rate < 1.0)
+			// 		config->color_lerp_rate += 0.1;
+			// 	break;
+			//
+			// case SDLK_o:
+			// 	// 'o': Decrease Volume
+			// 	if (config->volume > 0)
+			// 		config->volume -= 500;
+			// 	break;
+			//
+			// case SDLK_p:
+			// 	// 'p': Increase Volume
+			// 	if (config->volume < INT16_MAX)
+			// 		config->volume += 500;
+			// 	break;
+
+			// Map qwerty keys to CHIP8 keypad
+			case SDLK_1:
+				c8.kbd[0x1] = true;
+				break;
+			case SDLK_2:
+				c8.kbd[0x2] = true;
+				break;
+			case SDLK_3:
+				c8.kbd[0x3] = true;
+				break;
+			case SDLK_4:
+				c8.kbd[0xC] = true;
+				break;
+			case SDLK_q:
+				c8.kbd[0x4] = true;
+				break;
+			case SDLK_w:
+				c8.kbd[0x5] = true;
+				break;
+			case SDLK_e:
+				c8.kbd[0x6] = true;
+				break;
+			case SDLK_r:
+				c8.kbd[0xD] = true;
+				break;
+			case SDLK_a:
+				c8.kbd[0x7] = true;
+				break;
+			case SDLK_s:
+				c8.kbd[0x8] = true;
+				break;
+			case SDLK_d:
+				c8.kbd[0x9] = true;
+				break;
+			case SDLK_f:
+				c8.kbd[0xE] = true;
+				break;
+			case SDLK_z:
+				c8.kbd[0xA] = true;
+				break;
+			case SDLK_x:
+				c8.kbd[0x0] = true;
+				break;
+			case SDLK_c:
+				c8.kbd[0xB] = true;
+				break;
+			case SDLK_v:
+				c8.kbd[0xF] = true;
+				break;
+			}
+			break;
+
+		case SDL_KEYUP:
+			switch (e.key.keysym.sym) {
+			case SDLK_1:
+				c8.kbd[0x1] = false;
+				break;
+			case SDLK_2:
+				c8.kbd[0x2] = false;
+				break;
+			case SDLK_3:
+				c8.kbd[0x3] = false;
+				break;
+			case SDLK_4:
+				c8.kbd[0xC] = false;
+				break;
+			case SDLK_q:
+				c8.kbd[0x4] = false;
+				break;
+			case SDLK_w:
+				c8.kbd[0x5] = false;
+				break;
+			case SDLK_e:
+				c8.kbd[0x6] = false;
+				break;
+			case SDLK_r:
+				c8.kbd[0xD] = false;
+				break;
+			case SDLK_a:
+				c8.kbd[0x7] = false;
+				break;
+			case SDLK_s:
+				c8.kbd[0x8] = false;
+				break;
+			case SDLK_d:
+				c8.kbd[0x9] = false;
+				break;
+			case SDLK_f:
+				c8.kbd[0xE] = false;
+				break;
+			case SDLK_z:
+				c8.kbd[0xA] = false;
+				break;
+			case SDLK_x:
+				c8.kbd[0x0] = false;
+				break;
+			case SDLK_c:
+				c8.kbd[0xB] = false;
+				break;
+			case SDLK_v:
+				c8.kbd[0xF] = false;
+				break;
+			}
+			break;
+		}
+	}
+}
+
+void
+audio_callback(void *, uint8_t *stream, int len)
 {
+	uint16_t *data = (uint16_t *)stream;
+	static uint32_t si;
+	const uint32_t half_sqrwv_p = 44100 / 440 / 2;
+
+	/* We are filling out 2 bytes at a time (uint16_t), len is in bytes, so
+	   divide by 2.  If the current chunk of audio for the square wave is the
+	   crest of the wave, this will add the volume, otherwise it is the trough
+	   of the wave, and will add ‘negative’ volume. */
+	for (int i = 0; i < len / 2; i++)
+		data[i] = ((si++ / half_sqrwv_p) & 1) ? -3000 : +3000;
 }
diff --git a/src/ahoy/gui.h b/src/ahoy/gui.h
index 251045e..e262620 100644
--- a/src/ahoy/gui.h
+++ b/src/ahoy/gui.h
@@ -1,7 +1,21 @@
 #ifndef AHOY_AHOY_GUI_H
 #define AHOY_AHOY_GUI_H
 
+#include <stddef.h>
+#include <stdint.h>
+
+typedef enum {
+	GUI_RUNNING,
+	GUI_PAUSED,
+	GUI_STOP,
+} guistate;
+
 void wininit(void);
 void winfree(void);
+void winclr(void);
+void windrw(void);
+void readkb(void);
+
+extern guistate gs;
 
 #endif /* !AHOY_AHOY_GUI_H */
diff --git a/src/ahoy/main.c b/src/ahoy/main.c
index dbc75f0..10e79e6 100644
--- a/src/ahoy/main.c
+++ b/src/ahoy/main.c
@@ -8,11 +8,16 @@
 #include <unistd.h>
 
 #include <builder.h>
+#include <SDL2/SDL.h>
 
 #include "cerr.h"
 #include "emulator.h"
 #include "gui.h"
 #include "macros.h"
+#include "SDL_timer.h"
+
+#define FPS           60
+#define INSTS_PER_SEC 700
 
 [[noreturn]] static void usage(void);
 static void run(int, const char *);
@@ -73,6 +78,20 @@ main(int argc, char **argv)
 	return EXIT_SUCCESS;
 }
 
+static void
+update_timers(void)
+{
+	if (c8.DT > 0)
+		c8.DT--;
+
+	if (c8.ST > 0) {
+		c8.ST--;
+		// SDL_PauseAudioDevice(adev, 0); // Play sound
+	} else {
+		// SDL_PauseAudioDevice(adev, 1); // Pause sound
+	}
+}
+
 void
 run(int fd, const char *fn)
 {
@@ -103,6 +122,32 @@ run(int fd, const char *fn)
 	free(buf);
 	wininit();
 	emuinit(u8strtou8(sb));
+
+	while (gs != GUI_STOP) {
+		double dt;
+		uint64_t st, et;
+
+		readkb();
+		if (gs == GUI_PAUSED)
+			continue;
+
+		st = SDL_GetPerformanceCounter();
+		for (int i = 0; i < INSTS_PER_SEC / FPS; i++)
+			emutick();
+		et = SDL_GetPerformanceCounter();
+		dt = (double)((et - st) * 1000) / SDL_GetPerformanceFrequency();
+		SDL_Delay(16.67f > dt ? 16.67f - dt : 0);
+
+		// Update window with changes every 60hz
+		if (c8.needs_redraw)
+			windrw();
+
+		// Update delay & sound timers every 60hz
+		update_timers();
+
+		emutick();
+	}
+
 	u8strfree(sb);
 	winfree();
 }
-- 
cgit v1.2.3