aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2024-02-16 01:04:44 +0100
committerThomas Voss <mail@thomasvoss.com> 2024-02-16 01:04:44 +0100
commit560bd53d0e893a91a1bbc37fdaa1207ca2952a18 (patch)
treea6126de23df09b7d30b6523511141dd3c73fd2ab /src
parent8bd68f14846a7a33057d47d05b420177e89fbe85 (diff)
Implement a working SDL GUI
Diffstat (limited to 'src')
-rw-r--r--src/ahoy/emulator.c184
-rw-r--r--src/ahoy/emulator.h21
-rw-r--r--src/ahoy/gui.c289
-rw-r--r--src/ahoy/gui.h14
-rw-r--r--src/ahoy/main.c45
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();
}