From c567757f537c08a02e9cc2b457c27c8a5b4438a8 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Wed, 14 Feb 2024 00:47:14 +0100 Subject: Implement the start of an emulator --- src/ahoy/NOTES | 3 + src/ahoy/emulator.c | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ahoy/emulator.h | 8 ++ src/ahoy/main.c | 66 +++++++++-- 4 files changed, 385 insertions(+), 9 deletions(-) create mode 100644 src/ahoy/NOTES create mode 100644 src/ahoy/emulator.c create mode 100644 src/ahoy/emulator.h diff --git a/src/ahoy/NOTES b/src/ahoy/NOTES new file mode 100644 index 0000000..1a339ce --- /dev/null +++ b/src/ahoy/NOTES @@ -0,0 +1,3 @@ +While the I register is intended to store 12-bit addresses, it *is* a +16-bit register. If we want to stay true to the CHIP-8, it should be a +uint16_t instead of an unsigned _BitInt(12); diff --git a/src/ahoy/emulator.c b/src/ahoy/emulator.c new file mode 100644 index 0000000..4ada357 --- /dev/null +++ b/src/ahoy/emulator.c @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "cerr.h" +#include "emulator.h" +#include "macros.h" + +#define MEM_RESERVED 0x200 +#define MEM_TOTAL 0xFFF +#define MEM_FREE (MEM_TOTAL - MEM_RESERVED) + +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]; + +/* Preload font into memory */ +static uint8_t mem[MEM_TOTAL] = { + 0xF0, 0x90, 0x90, 0x90, 0xF0, /* 0 */ + 0x20, 0x60, 0x20, 0x20, 0x70, /* 1 */ + 0xF0, 0x10, 0xF0, 0x80, 0xF0, /* 2 */ + 0xF0, 0x10, 0xF0, 0x10, 0xF0, /* 3 */ + 0x90, 0x90, 0xF0, 0x10, 0x10, /* 4 */ + 0xF0, 0x80, 0xF0, 0x10, 0xF0, /* 5 */ + 0xF0, 0x80, 0xF0, 0x90, 0xF0, /* 6 */ + 0xF0, 0x10, 0x20, 0x40, 0x40, /* 7 */ + 0xF0, 0x90, 0xF0, 0x90, 0xF0, /* 8 */ + 0xF0, 0x90, 0xF0, 0x10, 0xF0, /* 9 */ + 0xF0, 0x90, 0xF0, 0x90, 0x90, /* A */ + 0xE0, 0x90, 0xE0, 0x90, 0xE0, /* B */ + 0xF0, 0x80, 0x80, 0x80, 0xF0, /* C */ + 0xE0, 0x90, 0x90, 0x90, 0xE0, /* D */ + 0xF0, 0x80, 0xF0, 0x80, 0xF0, /* E */ + 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 +emulate(struct u8view prog) +{ + struct timespec tp; + + if (prog.len > MEM_FREE) + diex("%s: binary of size %zu KiB too large to fit in RAM", "TODO", 0ul); + + PC = MEM_RESERVED; + memcpy(mem + PC, prog.p, prog.len); + + clock_gettime(CLOCK_REALTIME, &tp); + srand(tp.tv_sec ^ tp.tv_nsec); + + for (;; PC += 2) { + uint16_t op = (mem[PC] << 8) | mem[PC + 1]; + opexec(op); + scrdrw(); + } +} + +void +opexec(uint16_t op) +{ + switch (op >> 12) { + case 0x0: + switch (op) { + case 0x00E0: + memset(screen, 0, sizeof(screen)); + break; + case 0x00EE: + if (SP == 0) + diex("%s: stack pointer underflow", "TODO"); + PC = callstack[--SP]; + break; + default: + /* sys instruction is ignored */ + } + break; + + case 0x1: + /* -2 because we +2 each iteration */ + PC = (op & 0xFFF) - 2; + break; + + case 0x2: + if (SP == lengthof(callstack)) + diex("%s: stack pointer overflow", "TODO"); + callstack[SP++] = PC; + PC = (op & 0xFFF) - 2; + break; + + case 0x3: { + unsigned x = (op & 0x0F00) >> 8; + if (V[x] == (op & 0xFF)) + PC += 2; + break; + } + + case 0x4: { + unsigned x = (op & 0x0F00) >> 8; + if (V[x] != (op & 0xFF)) + PC += 2; + break; + } + + case 0x5: { + unsigned x = (op & 0x0F00) >> 8; + unsigned y = (op & 0x00F0) >> 4; + if (V[x] == V[y]) + PC += 2; + break; + } + + case 0x6: { + unsigned x = (op & 0x0F00) >> 8; + V[x] = op & 0xFF; + break; + } + + case 0x7: { + unsigned x = (op & 0x0F00) >> 8; + V[x] += op & 0xFF; + break; + } + + case 0x8: { + unsigned x = (op & 0x0F00) >> 8; + unsigned y = (op & 0x00F0) >> 4; + + switch (op & 0xF) { + case 0x0: + V[x] = V[y]; + break; + case 0x1: + V[x] |= V[y]; + break; + case 0x2: + V[x] &= V[y]; + break; + case 0x3: + V[x] ^= V[y]; + break; + case 0x4: { + unsigned n = V[x] + V[y]; + V[x] = n; + V[0xF] = n > UINT8_MAX; + break; + } + case 0x5: + V[0xF] = V[x] > V[y]; + V[x] -= V[y]; + break; + case 0x6: + V[0xF] = V[x] & 1; + V[x] >>= 1; + break; + case 0x7: + V[0xF] = V[y] > V[x]; + V[x] = V[y] - V[x]; + break; + case 0xE: + V[0xF] = V[x] & 0x80; + V[x] <<= 1; + break; + default: + badins(op); + } + + break; + } + + case 0x9: { + unsigned x = (op & 0x0F00) >> 8; + unsigned y = (op & 0x00F0) >> 4; + if (V[x] != V[y]) + PC += 2; + break; + } + + case 0xA: + I = op & 0xFFF; + break; + + case 0xB: + PC = (op & 0xFFF) + V[0] - 2; + break; + + case 0xC: { + unsigned x = (op & 0x0F00) >> 8; + V[x] = rand() & (op & 0xFF); + break; + } + + case 0xD: { + unsigned x = (op & 0x0F00) >> 8; + unsigned y = (op & 0x00F0) >> 4; + unsigned n = (op & 0x000F) >> 0; + + for (unsigned i = 0; i < n; i++) { + /* TODO: bounds check? */ + uint8_t spr_row = mem[I + i]; + uint8_t scr_row = V[y] + i; + uint64_t msk; + + if (scr_row >= lengthof(screen)) + break; + + msk = ((uint64_t)spr_row << (64 - 8)) >> V[x]; + V[0xF] = screen[scr_row] & msk; + screen[scr_row] ^= msk; + } + break; + } + + case 0xE: { + unsigned x = (op & 0x0F00) >> 8; + + switch (op & 0xFF) { + case 0x9E: + if (V[x] < lengthof(kbd) && kbd[V[x]]) + PC += 2; + break; + case 0xA1: + if (V[x] >= lengthof(kbd) || !kbd[V[x]]) + PC += 2; + break; + default: + badins(op); + } + break; + } + + case 0xF: { + unsigned x = (op & 0x0F00) >> 8; + + switch (op & 0xFF) { + case 0x07: + V[x] = DT; + break; + case 0x0A: + badins(op); + break; + case 0x15: + DT = V[x]; + break; + case 0x18: + ST = V[x]; + break; + case 0x1E: + I += V[x]; + break; + case 0x29: + /* Each character sprite is 5 bytes */ + I = 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; + break; + case 0x55: + memcpy(mem + I, V, x); + break; + case 0x65: + memcpy(V, mem + I, x); + break; + default: + badins(op); + } + + break; + } + + default: + unreachable(); + } +} + +void +badins(uint16_t op) +{ + diex("%s: invalid opcode: %04X", "TODO", op); +} diff --git a/src/ahoy/emulator.h b/src/ahoy/emulator.h new file mode 100644 index 0000000..31d596c --- /dev/null +++ b/src/ahoy/emulator.h @@ -0,0 +1,8 @@ +#ifndef AHOY_AHOY_EMULATOR_H +#define AHOY_AHOY_EMULATOR_H + +#include + +void emulate(struct u8view); + +#endif /* !AHOY_AHOY_EMULATOR_H */ diff --git a/src/ahoy/main.c b/src/ahoy/main.c index 8cc7106..ae7d5a1 100644 --- a/src/ahoy/main.c +++ b/src/ahoy/main.c @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -5,11 +7,24 @@ #include #include +#include + #include "cerr.h" +#include "emulator.h" #include "macros.h" +[[noreturn]] static void usage(void); static void run(int, const char *); +static const char *argv0; + +void +usage(void) +{ + fprintf(stderr, "Usage: %s [file]\n", argv0); + exit(EXIT_FAILURE); +} + int main(int argc, char **argv) { @@ -19,39 +34,72 @@ main(int argc, char **argv) {nullptr, no_argument, nullptr, 0 }, }; + argv0 = argv[0]; cerrinit(*argv); + while ((opt = getopt_long(argc, argv, "h", longopts, nullptr)) != -1) { switch (opt) { case 'h': execlp("man", "man", "1", argv[0], nullptr); die("execlp: man 1 %s", argv[0]); default: - fprintf(stderr, "Usage: %s [file ...]\n", argv[0]); - exit(EXIT_FAILURE); + usage(); } } argc -= optind; argv += optind; - if (!argc) + switch (argc) { + case 0: run(STDIN_FILENO, "-"); - for (int i = 0; i < argc; i++) { - if (streq("-", argv[i])) + break; + case 1: + if (streq("-", argv[0])) run(STDIN_FILENO, "-"); else { int fd; - if ((fd = open(argv[i], O_RDONLY)) == -1) - die("open: %s", argv[i]); - run(fd, argv[i]); + if ((fd = open(argv[0], O_RDONLY)) == -1) + die("open: %s", argv[0]); + run(fd, argv[0]); close(fd); } + break; + default: + usage(); } return EXIT_SUCCESS; } void -run([[maybe_unused]] int fd, [[maybe_unused]] const char *fn) +run(int fd, const char *fn) { + char *buf; + size_t blksize; + ssize_t nr; + struct stat st; + struct u8str sb; + + if (fstat(fd, &st) == -1) + die("fstat: %s", fn); + blksize = MAX(st.st_blksize, BUFSIZ); + if (!(buf = malloc(blksize))) + die("malloc"); + + u8strinit(&sb, S_ISREG(st.st_mode) ? (size_t)st.st_size : blksize); + while ((nr = read(fd, buf, blksize)) > 0) { + struct u8view v = { + .p = buf, + .len = nr, + }; + if (!u8strpush(&sb, v)) + die("u8strpush"); + } + if (nr == -1) + die("read: %s", fn); + + free(buf); + emulate(u8strtou8(sb)); + u8strfree(sb); } -- cgit v1.2.3