aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:12:57 +0200
committergbc dev <gbc@localhost>2026-07-04 22:12:57 +0200
commit5800f04eb0417c980fef2eddd78d8b60d19d0db5 (patch)
tree82f29e7b97b6b18eb077dcac51b6407c5966ba0b
parentcore emulator: SM83 CPU, MMU, timer, PPU, CGB support (diff)
downloadsl0pboy-5800f04eb0417c980fef2eddd78d8b60d19d0db5.tar.gz
sl0pboy-5800f04eb0417c980fef2eddd78d8b60d19d0db5.tar.xz
sl0pboy-5800f04eb0417c980fef2eddd78d8b60d19d0db5.zip
terminal renderer (truecolor half-block), input, screenshot mode
-rw-r--r--Makefile2
-rw-r--r--src/main.c64
-rw-r--r--src/render.c112
-rw-r--r--src/render.h12
4 files changed, 186 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 0d79c1f..74fdda6 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
CC := gcc
-CFLAGS := -std=c11 -O2 -Wall -Wextra -Wno-unused-parameter -g
+CFLAGS := -std=c11 -O2 -Wall -Wextra -Wno-unused-parameter -g -D_POSIX_C_SOURCE=200809L
LDFLAGS :=
SRCDIR := src
BUILD := build
diff --git a/src/main.c b/src/main.c
index 27e886a..273d6b7 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,8 +1,28 @@
#include "gb.h"
#include "cpu.h"
+#include "render.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <time.h>
+#include <signal.h>
+
+static volatile sig_atomic_t running = 1;
+static void on_sig(int s) { (void)s; running = 0; }
+
+static double now_sec(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec + ts.tv_nsec / 1e9;
+}
+
+static void run_frame(GB *gb) {
+ gb->ppu.frame_ready = false;
+ // safety bound in case LCD is off (no frame_ready)
+ u64 budget = gb->cycles + 70224 * 2;
+ while (!gb->ppu.frame_ready && gb->cycles < budget)
+ cpu_step(gb);
+}
int main(int argc, char **argv) {
if (argc < 2) {
@@ -12,15 +32,20 @@ int main(int argc, char **argv) {
const char *rom = argv[1];
bool test = false;
double seconds = 30.0;
+ int shot_frames = -1;
+ const char *shot_path = "shot.ppm";
for (int i = 2; i < argc; i++) {
if (!strcmp(argv[i], "--test")) {
test = true;
if (i + 1 < argc) seconds = atof(argv[++i]);
+ } else if (!strcmp(argv[i], "--shot")) {
+ if (i + 1 < argc) shot_frames = atoi(argv[++i]);
+ if (i + 1 < argc && argv[i+1][0] != '-') shot_path = argv[++i];
}
}
GB *gb = calloc(1, sizeof(GB));
- if (cart_load(&gb->cart, rom) != 0) return 1;
+ if (cart_load(&gb->cart, rom) != 0) { free(gb); return 1; }
gb_reset(gb);
if (test) {
@@ -33,8 +58,41 @@ int main(int argc, char **argv) {
return 0;
}
- // TODO: interactive terminal rendering
- fprintf(stderr, "interactive mode not yet implemented; use --test\n");
+ if (shot_frames >= 0) {
+ for (int f = 0; f < shot_frames; f++) run_frame(gb);
+ FILE *pf = fopen(shot_path, "wb");
+ fprintf(pf, "P6\n%d %d\n255\n", SCREEN_W, SCREEN_H);
+ for (int y = 0; y < SCREEN_H; y++)
+ for (int x = 0; x < SCREEN_W; x++)
+ fwrite(gb->ppu.fb[y][x], 1, 3, pf);
+ fclose(pf);
+ fprintf(stderr, "wrote %s after %d frames\n", shot_path, shot_frames);
+ cart_free(&gb->cart); free(gb); return 0;
+ }
+
+ signal(SIGINT, on_sig);
+ signal(SIGTERM, on_sig);
+ term_init();
+
+ const double frame_time = 1.0 / 59.73;
+ double next = now_sec();
+ while (running) {
+ if (!input_poll(gb)) break;
+ run_frame(gb);
+ render_frame(gb);
+
+ next += frame_time;
+ double t = now_sec();
+ double sleep = next - t;
+ if (sleep > 0) {
+ struct timespec ts = { (time_t)sleep, (long)((sleep - (time_t)sleep) * 1e9) };
+ nanosleep(&ts, NULL);
+ } else if (sleep < -0.1) {
+ next = t; // fell behind; resync
+ }
+ }
+
+ term_restore();
cart_free(&gb->cart);
free(gb);
return 0;
diff --git a/src/render.c b/src/render.c
new file mode 100644
index 0000000..005fd71
--- /dev/null
+++ b/src/render.c
@@ -0,0 +1,112 @@
+#include "render.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <termios.h>
+#include <fcntl.h>
+
+#define UPPER "\xe2\x96\x80" // ▀ U+2580
+
+static struct termios orig_termios;
+static bool raw_active = false;
+
+void term_restore(void) {
+ if (raw_active) {
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
+ raw_active = false;
+ }
+ // show cursor, leave alt screen, reset colors
+ printf("\x1b[0m\x1b[?25h\x1b[?1049l");
+ fflush(stdout);
+}
+
+void term_init(void) {
+ tcgetattr(STDIN_FILENO, &orig_termios);
+ struct termios raw = orig_termios;
+ raw.c_lflag &= ~(ECHO | ICANON | ISIG);
+ raw.c_iflag &= ~(IXON | ICRNL);
+ raw.c_cc[VMIN] = 0;
+ raw.c_cc[VTIME] = 0;
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
+ raw_active = true;
+ // enter alt screen, hide cursor, clear
+ printf("\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H");
+ fflush(stdout);
+ atexit(term_restore);
+}
+
+// large reusable output buffer
+static char outbuf[SCREEN_W * (SCREEN_H/2) * 40 + 4096];
+
+void render_frame(GB *gb) {
+ PPU *p = &gb->ppu;
+ char *o = outbuf;
+ o += sprintf(o, "\x1b[H");
+ for (int y = 0; y < SCREEN_H; y += 2) {
+ int pr = -1, pg = -1, pb = -1; // prev fg
+ int PR = -1, PG = -1, PB = -1; // prev bg
+ for (int x = 0; x < SCREEN_W; x++) {
+ u8 *top = p->fb[y][x];
+ u8 *bot = p->fb[y+1][x];
+ if (top[0] != pr || top[1] != pg || top[2] != pb) {
+ o += sprintf(o, "\x1b[38;2;%d;%d;%dm", top[0], top[1], top[2]);
+ pr = top[0]; pg = top[1]; pb = top[2];
+ }
+ if (bot[0] != PR || bot[1] != PG || bot[2] != PB) {
+ o += sprintf(o, "\x1b[48;2;%d;%d;%dm", bot[0], bot[1], bot[2]);
+ PR = bot[0]; PG = bot[1]; PB = bot[2];
+ }
+ memcpy(o, UPPER, 3); o += 3;
+ }
+ memcpy(o, "\x1b[0m\r\n", 6); o += 6;
+ }
+ fwrite(outbuf, 1, o - outbuf, stdout);
+ fflush(stdout);
+}
+
+// ---- input with decay-based "hold" emulation ----
+static int hold[8]; // frames remaining for each BTN bit position
+#define HOLD_FRAMES 4
+
+static void press(int bit) {
+ for (int i = 0; i < 8; i++) if ((1 << i) == bit) hold[i] = HOLD_FRAMES;
+}
+
+bool input_poll(GB *gb) {
+ unsigned char buf[64];
+ int n = read(STDIN_FILENO, buf, sizeof(buf));
+ bool quit = false;
+ for (int i = 0; i < n; i++) {
+ unsigned char ch = buf[i];
+ if (ch == 0x1b && i + 2 < n && buf[i+1] == '[') {
+ switch (buf[i+2]) {
+ case 'A': press(BTN_UP); break;
+ case 'B': press(BTN_DOWN); break;
+ case 'C': press(BTN_RIGHT); break;
+ case 'D': press(BTN_LEFT); break;
+ }
+ i += 2;
+ continue;
+ }
+ switch (ch) {
+ case 'w': press(BTN_UP); break;
+ case 's': press(BTN_DOWN); break;
+ case 'a': press(BTN_LEFT); break;
+ case 'd': press(BTN_RIGHT); break;
+ case 'z': case 'j': press(BTN_A); break;
+ case 'x': case 'k': press(BTN_B); break;
+ case '\r': case '\n': press(BTN_START); break;
+ case ' ': press(BTN_SELECT); break;
+ case 'q': case 3: quit = true; break;
+ }
+ }
+ // build button state from decay counters
+ u8 b = 0;
+ for (int i = 0; i < 8; i++) {
+ if (hold[i] > 0) { hold[i]--; b |= (1 << i); }
+ }
+ gb->buttons = b;
+ if (b) gb_request_interrupt(gb, INT_JOYPAD);
+ return !quit;
+}
diff --git a/src/render.h b/src/render.h
new file mode 100644
index 0000000..15f9cda
--- /dev/null
+++ b/src/render.h
@@ -0,0 +1,12 @@
+#ifndef GBC_RENDER_H
+#define GBC_RENDER_H
+
+#include "gb.h"
+
+void term_init(void);
+void term_restore(void);
+void render_frame(GB *gb);
+// Poll keyboard, update gb->buttons. Returns false if quit requested.
+bool input_poll(GB *gb);
+
+#endif