From 560bd53d0e893a91a1bbc37fdaa1207ca2952a18 Mon Sep 17 00:00:00 2001 From: Thomas Voss 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 +/* 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 +#include +#include #include #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 +#include + +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 #include +#include #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