aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2024-02-14 00:47:14 +0100
committerThomas Voss <mail@thomasvoss.com> 2024-02-14 00:47:14 +0100
commitc567757f537c08a02e9cc2b457c27c8a5b4438a8 (patch)
tree21a94d21ce3879494df073cf6450256ca3adb032 /src
parent5d7ee4168b03ed7d95da3425d843bec5743eaeb6 (diff)
Implement the start of an emulator
Diffstat (limited to 'src')
-rw-r--r--src/ahoy/NOTES3
-rw-r--r--src/ahoy/emulator.c317
-rw-r--r--src/ahoy/emulator.h8
-rw-r--r--src/ahoy/main.c66
4 files changed, 385 insertions, 9 deletions
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 <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <mbstring.h>
+
+#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 <mbstring.h>
+
+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 <sys/stat.h>
+
#include <fcntl.h>
#include <getopt.h>
#include <stdio.h>
@@ -5,11 +7,24 @@
#include <string.h>
#include <unistd.h>
+#include <builder.h>
+
#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);
}