diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | src/main.c | 64 | ||||
| -rw-r--r-- | src/render.c | 112 | ||||
| -rw-r--r-- | src/render.h | 12 |
4 files changed, 186 insertions, 4 deletions
@@ -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 @@ -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 |
