#include "gb.h" #include "ppu.h" #include "timer.h" #include "cpu.h" #include #include #include #include void gb_request_interrupt(GB *gb, u8 flag) { gb->iff |= flag; } // ---------- Joypad ---------- static u8 joyp_read(GB *gb) { u8 r = 0xCF | gb->joyp_sel; // sel bit4=directions, bit5=buttons ; 0 = selected. Pressed = 0. if (!(gb->joyp_sel & 0x10)) { // directions if (gb->buttons & BTN_RIGHT) r &= ~0x01; if (gb->buttons & BTN_LEFT) r &= ~0x02; if (gb->buttons & BTN_UP) r &= ~0x04; if (gb->buttons & BTN_DOWN) r &= ~0x08; } if (!(gb->joyp_sel & 0x20)) { // buttons if (gb->buttons & BTN_A) r &= ~0x01; if (gb->buttons & BTN_B) r &= ~0x02; if (gb->buttons & BTN_SELECT) r &= ~0x04; if (gb->buttons & BTN_START) r &= ~0x08; } return r; } // ---------- OAM DMA ---------- static void oam_dma(GB *gb, u8 val) { gb->dma = val; u16 src = val << 8; for (int i = 0; i < 0xA0; i++) gb->ppu.oam[i] = gb_read(gb, src + i); } // ---------- HDMA (CGB) ---------- static void hdma_do_block(GB *gb) { for (int i = 0; i < 0x10; i++) { u8 v = gb_read(gb, gb->hdma_src++); ppu_write(gb, 0x8000 + (gb->hdma_dst++ & 0x1FFF), v); } } // ---------- Bus ---------- u8 gb_read(GB *gb, u16 addr) { // Boot ROM overlays low memory until the game disables it via 0xFF50. // CGB layout leaves a hole at 0x0100-0x01FF where the cart header shows // through (the boot logo check reads it); the ROM resumes at 0x0200. if (gb->boot_rom_active) { if (addr < 0x0100) return gb->boot_rom[addr]; if (gb->boot_rom_len > 0x0100 && addr >= 0x0200 && addr < gb->boot_rom_len) return gb->boot_rom[addr]; } if (addr < 0x8000) return cart_read(&gb->cart, addr); if (addr < 0xA000) return ppu_read(gb, addr); if (addr < 0xC000) return cart_read(&gb->cart, addr); if (addr < 0xD000) return gb->wram[addr - 0xC000]; if (addr < 0xE000) { int bank = gb->cgb_mode ? (gb->wram_bank ? gb->wram_bank : 1) : 1; return gb->wram[bank * 0x1000 + (addr - 0xD000)]; } if (addr < 0xFE00) return gb_read(gb, addr - 0x2000); // echo if (addr < 0xFEA0) return ppu_read(gb, addr); if (addr < 0xFF00) return 0xFF; // unusable // IO if (addr == 0xFF00) return joyp_read(gb); if (addr == 0xFF01) return gb->sb; if (addr == 0xFF02) return gb->sc | 0x7E; if (addr >= 0xFF04 && addr <= 0xFF07) return timer_read(gb, addr); if (addr == 0xFF0F) return gb->iff | 0xE0; if (addr >= 0xFF10 && addr <= 0xFF3F) { // Register-level APU: return stored value OR'd with the bits that read // back as 1 on real hardware (write-only / unused bits). NR50 (0xFF24) // has mask 0x00, so it reads back exactly what was written, which is // what the game's volume fade-out relies on. static const u8 apu_or[0x30] = { 0x80,0x3F,0x00,0xFF,0xBF, 0xFF, 0x3F,0x00,0xFF,0xBF, 0x7F,0xFF,0x9F,0xFF,0xBF, 0xFF, 0xFF,0x00,0x00,0xBF, 0x00,0x00,0x70, 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 /* FF30-FF3F wave RAM: exact */ }; return gb->apu[addr - 0xFF10] | apu_or[addr - 0xFF10]; } if (addr >= 0xFF40 && addr <= 0xFF4B) { if (addr == 0xFF46) return gb->dma; return ppu_read_reg(gb, addr); } if (addr == 0xFF4D) return gb->cgb_mode ? (gb->key1 | 0x7E) : 0xFF; if (addr == 0xFF4F) return gb->cgb_mode ? (gb->ppu.vram_bank | 0xFE) : 0xFF; if (addr == 0xFF51 && gb->cgb_mode) return gb->hdma_src >> 8; if (addr == 0xFF52 && gb->cgb_mode) return gb->hdma_src & 0xFF; if (addr == 0xFF53 && gb->cgb_mode) return gb->hdma_dst >> 8; if (addr == 0xFF54 && gb->cgb_mode) return gb->hdma_dst & 0xFF; if (addr == 0xFF55 && gb->cgb_mode) return gb->hdma_active ? gb->hdma_len : 0xFF; if ((addr == 0xFF68 || addr == 0xFF69 || addr == 0xFF6A || addr == 0xFF6B) && gb->cgb_mode) return ppu_read_reg(gb, addr); if (addr == 0xFF70) return gb->cgb_mode ? (gb->wram_bank | 0xF8) : 0xFF; if (addr >= 0xFF80 && addr < 0xFFFF) return gb->hram[addr - 0xFF80]; if (addr == 0xFFFF) return gb->ie; return 0xFF; } void gb_write(GB *gb, u16 addr, u8 val) { if (addr < 0x8000) { cart_write(&gb->cart, addr, val); return; } if (addr < 0xA000) { ppu_write(gb, addr, val); return; } if (addr < 0xC000) { cart_write(&gb->cart, addr, val); return; } if (addr < 0xD000) { gb->wram[addr - 0xC000] = val; return; } if (addr < 0xE000) { int bank = gb->cgb_mode ? (gb->wram_bank ? gb->wram_bank : 1) : 1; gb->wram[bank * 0x1000 + (addr - 0xD000)] = val; return; } if (addr < 0xFE00) { gb_write(gb, addr - 0x2000, val); return; } if (addr < 0xFEA0) { ppu_write(gb, addr, val); return; } if (addr < 0xFF00) return; if (addr == 0xFF00) { gb->joyp_sel = val & 0x30; return; } if (addr == 0xFF01) { gb->sb = val; return; } if (addr == 0xFF02) { gb->sc = val; if ((val & 0x81) == 0x81) { // internal clock (GB is master): transmit gb->sb. Completes now. u8 outb = gb->sb; if (gb->serial_log) { fputc(outb, stderr); fflush(stderr); } if (gb->serial_out_fd >= 0) { unsigned char c = outb; (void)!write(gb->serial_out_fd, &c, 1); } gb->sb = 0xFF; // nothing shifted in on a pure TX gb->sc &= ~0x80; gb_request_interrupt(gb, INT_SERIAL); } else if ((val & 0x81) == 0x80) { // external clock (GB waits for the peer): used as a clean RX path. // Complete only when an input byte is available - and do NOT echo // it to stdout - so a program can poll for input without emitting // junk. Stays pending (bit7 set) while stdin has nothing. if (gb->serial_in_fd >= 0) { unsigned char c; ssize_t n = read(gb->serial_in_fd, &c, 1); if (n == 1) { gb->sb = c; gb->sc &= ~0x80; gb_request_interrupt(gb, INT_SERIAL); } else if (n == 0) { if (!gb->serial_no_eof) { gb->sb = 0x04; // EOF -> EOT (Ctrl-D) gb->sc &= ~0x80; gb_request_interrupt(gb, INT_SERIAL); } // else: stay pending (OSK provides input) } // n < 0 (EAGAIN on a live tty): stay pending, poll again } } return; } if (addr >= 0xFF04 && addr <= 0xFF07) { timer_write(gb, addr, val); return; } if (addr == 0xFF0F) { gb->iff = val & 0x1F; return; } if (addr == 0xFF50) { if (val) gb->boot_rom_active = false; return; } // unmap BIOS if (addr >= 0xFF10 && addr <= 0xFF3F) { gb->apu[addr - 0xFF10] = val; return; } if (addr >= 0xFF40 && addr <= 0xFF4B) { if (addr == 0xFF46) { oam_dma(gb, val); return; } ppu_write_reg(gb, addr, val); return; } if (addr == 0xFF4D && gb->cgb_mode) { gb->key1 = (gb->key1 & 0x80) | (val & 1); return; } if (addr == 0xFF4F && gb->cgb_mode) { gb->ppu.vram_bank = val & 1; return; } if (addr == 0xFF51 && gb->cgb_mode) { gb->hdma_src = (gb->hdma_src & 0xFF) | (val << 8); return; } if (addr == 0xFF52 && gb->cgb_mode) { gb->hdma_src = (gb->hdma_src & 0xFF00) | (val & 0xF0); return; } if (addr == 0xFF53 && gb->cgb_mode) { gb->hdma_dst = (gb->hdma_dst & 0xFF) | ((val & 0x1F) << 8); return; } if (addr == 0xFF54 && gb->cgb_mode) { gb->hdma_dst = (gb->hdma_dst & 0xFF00) | (val & 0xF0); return; } if (addr == 0xFF55 && gb->cgb_mode) { int len = ((val & 0x7F) + 1) * 0x10; if (val & 0x80) { // HDMA (mode 1): transfer during hblank gb->hdma_len = val & 0x7F; gb->hdma_active = true; } else { if (gb->hdma_active) { // stopping an active hblank dma gb->hdma_active = false; gb->hdma_len = 0xFF; } else { // GDMA: transfer all now for (int i = 0; i < len; i++) { u8 v = gb_read(gb, gb->hdma_src++); ppu_write(gb, 0x8000 + (gb->hdma_dst++ & 0x1FFF), v); } } } return; } if ((addr >= 0xFF68 && addr <= 0xFF6B) && gb->cgb_mode) { ppu_write_reg(gb, addr, val); return; } if (addr == 0xFF70 && gb->cgb_mode) { gb->wram_bank = val & 0x07; return; } if (addr >= 0xFF80 && addr < 0xFFFF) { gb->hram[addr - 0xFF80] = val; return; } if (addr == 0xFFFF) { gb->ie = val; return; } } // advance subsystems by t-cycles void gb_tick(GB *gb, int tcycles) { // in double-speed mode CPU/timer run 2x, but PPU stays at normal rate. int ppu_cycles = gb->double_speed ? tcycles / 2 : tcycles; timer_tick(gb, tcycles); ppu_tick(gb, ppu_cycles); gb->cycles += tcycles; } // HDMA one block per hblank; called by ppu when entering hblank void gb_hdma_hblank(GB *gb) { if (!gb->hdma_active) return; hdma_do_block(gb); if (gb->hdma_len == 0) { gb->hdma_active = false; gb->hdma_len = 0xFF; } else { gb->hdma_len--; } } void gb_init(GB *gb) { u8 *saved_boot = gb->boot_rom; // preserve a boot ROM loaded pre-init int saved_len = gb->boot_rom_len; memset(gb, 0, sizeof(*gb)); gb->boot_rom = saved_boot; gb->boot_rom_len = saved_len; gb_reset(gb); } int gb_load_bootrom(GB *gb, const char *path) { FILE *f = fopen(path, "rb"); if (!f) { perror("open boot rom"); return -1; } fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET); if (n != 0x100 && n != 0x800 && n != 0x900) { fprintf(stderr, "boot rom '%s': unexpected size %ld (want 256 for DMG " "or 2304 for CGB)\n", path, n); fclose(f); return -1; } free(gb->boot_rom); gb->boot_rom = malloc(n); if (!gb->boot_rom || fread(gb->boot_rom, 1, n, f) != (size_t)n) { fclose(f); return -1; } gb->boot_rom_len = (int)n; fclose(f); return 0; } void gb_reset(GB *gb) { CPU *c = &gb->cpu; bool boot = (gb->boot_rom != NULL); // A CGB boot ROM implies a CGB console; otherwise follow the cartridge. gb->cgb_mode = boot ? (gb->boot_rom_len > 0x100) : gb->cart.cgb; c->sp = 0xFFFE; if (boot) { // Power-on state: start at 0x0000 inside the boot ROM, which sets up // all registers itself before jumping to the cartridge entry point. gb->boot_rom_active = true; c->pc = 0x0000; c->af = c->bc = c->de = c->hl = 0; c->sp = 0x0000; } else { // Skip the boot ROM: jump straight to the game with the documented // post-boot register state. gb->boot_rom_active = false; c->pc = 0x0100; if (gb->cgb_mode) { c->af = 0x1180; c->bc = 0x0000; c->de = 0xFF56; c->hl = 0x000D; } else { c->af = 0x01B0; c->bc = 0x0013; c->de = 0x00D8; c->hl = 0x014D; } } c->ime = false; gb->serial_out_fd = -1; gb->serial_in_fd = -1; gb->wram_bank = 1; gb->joyp_sel = 0x30; gb->iff = 0xE1; gb->hdma_len = 0xFF; ppu_reset(gb); gb->timer.div = boot ? 0x0000 : 0x1800; }