commit 13456ff79a70fb056c29dfc190c05d51a93c3b46 Author: Nathaniel Walizer Date: Fri Nov 29 18:15:31 2024 -0800 Initial commit: Everything in progress, with PPU R/W support nearly complete diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..84620c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +CC = gcc +LD = $(CC) +CFLAGS = -Wall -Werror -Wshadow -I.. -g #-DE6502_DEBUG +LDFLAGS = + +OBJDIR = obj +SRCDIR = src +BINDIR = bin + + +# nese + +TARGET_1 = nese +LDLIBS_1 = + +SRC_SRCS_1 = nese.c ines.c nes.c ppu.c + +EXT_SRCS_1 = e6502/e6502.c + + +SRCS_1 = $(SRC_SRCS_1:%=$(SRCDIR)/%) $(EXT_SRCS_1) +OBJS_1 = $(SRCS_1:%.c=$(OBJDIR)/%.o) + + +all: $(BINDIR)/$(TARGET_1) + +clean: ; rm -rf $(OBJDIR) $(BINDIR) + +$(BINDIR)/$(TARGET_1): $(OBJS_1) + @mkdir -p $(@D) + $(LD) $(LDFLAGS) $^ $(LDLIBS_1) -o $@ + +$(OBJDIR)/%.o: %.c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c $< -o $@ diff --git a/e6502 b/e6502 new file mode 120000 index 0000000..7810cc3 --- /dev/null +++ b/e6502 @@ -0,0 +1 @@ +../e6502/ \ No newline at end of file diff --git a/src/apu.h b/src/apu.h new file mode 100644 index 0000000..e1a5970 --- /dev/null +++ b/src/apu.h @@ -0,0 +1,18 @@ +#ifndef ENES_APU_H_ +#define ENES_APU_H_ + +#include + + +// TODO: Stubbed + +typedef struct { + +} nes_apu; + +static inline uint8_t nes_apu_read(nes_apu*, uint16_t addr) { return 0; } +static inline void nes_apu_write(nes_apu*, uint16_t addr, uint8_t) {} +static inline void nes_apu_reset(nes_apu*) {} + + +#endif // ENES_APU_H_ diff --git a/src/ines.c b/src/ines.c new file mode 100644 index 0000000..7c9d963 --- /dev/null +++ b/src/ines.c @@ -0,0 +1,84 @@ +#include + +#include "ines.h" +#include "nes.h" + + +#define INES_LOG(tag, fmt, ...) \ + fprintf(stderr, tag ": iNES: " fmt "\n" __VA_OPT__(,) __VA_ARGS__) +#define INES_ERR(...) INES_LOG("E", __VA_ARGS__) + + +#define ines_trainer_size (512U) +#define ines_prg_rom_chunk (16384U) +#define ines_chr_rom_chunk (8192U) + + +int ines_check(ines_Header* ret, FILE* file) { + int status = 0; + ines_Header hdr = {0}; + + if (1 != fread(&hdr, sizeof(ines_Header), 1, file)) { + INES_ERR("Failed to read header"); + status = -1; + } + + if (0 == status && 0 != memcmp(hdr.magic, + ines_magic, + sizeof(hdr.magic))) { + INES_ERR("Bad file magic: expected '%.4s', got '%.4s'", + ines_magic, hdr.magic); + status =-1; + } + + if (0 == status && NULL != ret) { + *ret = hdr; + } + + return status; +} + +int ines_load(nes_cart_rom* cart_rom, FILE* file) { + int status = 0; + ines_Header hdr = {0}; + + status = ines_check(&hdr, file); + + if (0 == status && (hdr.flags_6 & ines_Flag_Trainer)) { + // Skip trainer + status = fseek(file, ines_trainer_size, SEEK_CUR); + } + + if (0 == status) { + int prg_size = ines_prg_rom_chunk * hdr.prg_size_lsb; + if (prg_size > nes_mem_rom_size) { + INES_ERR("Program ROM too large: %d > %d", + prg_size, nes_mem_rom_size); + status = -1; + + } else if (1 != fread(cart_rom->prg, prg_size, 1, file)) { + INES_ERR("Failed to read program ROM"); + status = -1; + + } else if (1U == hdr.prg_size_lsb) { + // If there's only one PRG_ROM chunk, duplicate it + memcpy(cart_rom->prg + ines_prg_rom_chunk, + cart_rom, ines_prg_rom_chunk); + } + } + + if (0 == status) { + int chr_size = ines_chr_rom_chunk * hdr.chr_size_lsb; + if (chr_size > nes_ppu_mem_size) { + INES_ERR("Sprite ROM too large: %d > %d", + chr_size, nes_ppu_mem_size); + status = -1; + + } else if (1 != fread(cart_rom->chr, chr_size, 1, file)) { + INES_ERR("Failed to read sprite ROM"); + status = -1; + } + } + + return status; +} diff --git a/src/ines.h b/src/ines.h new file mode 100644 index 0000000..46a52f3 --- /dev/null +++ b/src/ines.h @@ -0,0 +1,53 @@ +#ifndef INES_H_ +#define INES_H_ + +#include +#include + +#include "nes.h" + + +#define ines_magic "NES\x1a" + +typedef enum { + ines_Flag_Horizontal = 0b00000001, + ines_Flag_Battery = 0b00000010, + ines_Flag_Trainer = 0b00000100, + ines_Flag_Alt_Nametable = 0b00001000, + ines_Mapper_Nibble_Lo = 0b11110000, +} ines_6_Flag; + +typedef enum { + ines_Console_Mask = 0b00000011, + ines_Console_NES = 0b00000000, + ines_Console_VS = 0b00000001, + ines_Console_PC10 = 0b00000010, + ines_Console_Ext = 0b00000011, + ines_NES_2_MASK = 0b00001100, + ines_NES_2_ID = 0b00001000, + ines_Mapper_Nibble_Hi = 0b11110000, +} ines_7_Flag; + +typedef struct { + char magic[4]; + uint8_t prg_size_lsb; + uint8_t chr_size_lsb; + uint8_t flags_6; + uint8_t flags_7; + uint8_t mapper_msb; + uint8_t size_msb; + uint8_t eeprom_size; + uint8_t ram_size; + uint8_t timing; + uint8_t sys_type; + uint8_t n_misc; + uint8_t expansion; +} __attribute__ (( packed )) ines_Header; + + +int ines_check(ines_Header*, FILE*); + +int ines_load(nes_cart_rom*, FILE*); + + +#endif // INES_H_ diff --git a/src/nes.c b/src/nes.c new file mode 100644 index 0000000..96b395c --- /dev/null +++ b/src/nes.c @@ -0,0 +1,91 @@ +#include "nes.h" + + +uint8_t nes_mem_read(nes* sys, uint16_t addr) { + uint8_t val = 0; + + if (addr < nes_mem_ppu_start) { + addr = (addr - nes_mem_ram_start) & (nes_mem_ram_size - 1); + val = sys->ram[addr]; + + } else if (addr < nes_mem_apu_start) { + addr = (addr - nes_mem_ppu_start) & (nes_ppu_map_size - 1); + val = nes_ppu_read(&sys->ppu, nes_mem_ppu_start + addr); + + } else if (addr < nes_mem_exp_start) { + val = nes_apu_read(&sys->apu, addr); + + } else if (addr < nes_mem_wram_start) { + // TODO: Expansion ROM support + + } else if (addr < nes_mem_rom_start) { + val = sys->cart.wram[addr - nes_mem_wram_start]; + + } else { + val = sys->cart.rom.prg[addr - nes_mem_rom_start]; + } + + return val; +} + +void nes_mem_write(nes* sys, uint16_t addr, uint8_t val) { + if (addr < nes_mem_ppu_start) { + addr = (addr - nes_mem_ram_start) & (nes_mem_ram_size - 1); + sys->ram[addr] = val; + + } else if (addr < nes_mem_apu_start) { + addr = (addr - nes_mem_ppu_start) & (nes_ppu_map_size - 1); + nes_ppu_write(&sys->ppu, nes_mem_ppu_start + addr, val); + + } else if (addr == nes_ppu_dma_reg) { + for (int i = 0; i < nes_ppu_oam_size; ++i) { + sys->ppu.oam[(uint8_t)(i + sys->ppu.oam_addr)] = + nes_mem_read(sys, ((uint16_t)val << 8) + i); + } + sys->cpu.cycle += 513U; + sys->ppu.cycle += (513U * nes_clock_cpu_div) / + nes_clock_ppu_div; + // TODO: Increment APU cycles? + + } else if (addr < nes_mem_exp_start) { + nes_apu_write(&sys->apu, addr, val); + + } else if (addr < nes_mem_wram_start) { + // No ROM writes + + } else if (addr < nes_mem_rom_start) { + sys->cart.wram[addr - nes_mem_wram_start] = val; + + } else { + // No ROM writes + } +} + +void nes_init(nes* sys) { + e6502_init(&sys->cpu, (e6502_Read*)nes_mem_read, + (e6502_Write*)nes_mem_write, sys); + nes_ppu_init(&sys->ppu, sys->cart.rom.chr); +} + +void nes_reset(nes* sys) { + e6502_reset(&sys->cpu); + nes_ppu_reset(&sys->ppu); + nes_apu_reset(&sys->apu); +} + +int nes_run(nes* sys, int master_cycles, int* run) { + int cpu_run = 0; + int cpu_cycles = (master_cycles + (nes_clock_cpu_div - 1)) / + nes_clock_cpu_div; + int status = e6502_run(&sys->cpu, cpu_cycles, &cpu_run, 0); + + master_cycles = cpu_run * nes_clock_cpu_div; + int ppu_cycles = master_cycles / nes_clock_ppu_div; + int vblank = nes_ppu_run(&sys->ppu, ppu_cycles); + + e6502_set_nmi(&sys->cpu, vblank); + + if (run) *run = master_cycles; + + return status; +} diff --git a/src/nes.h b/src/nes.h new file mode 100644 index 0000000..68e8ca4 --- /dev/null +++ b/src/nes.h @@ -0,0 +1,65 @@ +#ifndef NES_H_ +#define NES_H_ + +#include "apu.h" +#include "ppu.h" + +#include "e6502/e6502.h" + + +#define nes_clock_master_num (945U * 1000U * 1000U) +#define nes_clock_master_den (44U) +#define nes_clock_cpu_div (12U) +#define nes_clock_ppu_div (4U) + +#define nes_mem_ram_start (0x0000U) +#define nes_mem_ram_size (0x0800U) +#define nes_mem_ppu_start (0x2000U) +#define nes_mem_ppu_size (0x2000U) +#define nes_mem_apu_start (0x4000U) +#define nes_mem_apu_size (0x0020U) +#define nes_mem_exp_start (0x4020U) +#define nes_mem_exp_size (0x1FE0U) +#define nes_mem_wram_start (0x6000U) +#define nes_mem_wram_size (0x2000U) +#define nes_mem_rom_start (0x8000U) +#define nes_mem_rom_size (0x8000U) + +#define nes_ppu_mem_size (0x4000U) +#define nes_ppu_map_size (0x8U) +#define nes_ppu_dma_reg (0x4014U) + +#define nes_apu_map_size (0x20U) + +typedef struct { + // TODO: Dynamic size support + uint8_t prg[nes_mem_rom_size]; + uint8_t chr[nes_ppu_mem_size]; +} nes_cart_rom; + +typedef struct { + nes_cart_rom rom; + uint8_t wram[nes_mem_wram_size]; + // TODO: Mapper support +} nes_cart; + +typedef struct { + e6502_Core cpu; + nes_ppu ppu; + nes_apu apu; + uint8_t ram[nes_mem_ram_size]; + nes_cart cart; +} nes; + +uint8_t nes_mem_read(nes*, uint16_t addr); + +void nes_mem_write(nes*, uint16_t addr, uint8_t); + +void nes_init(nes*); + +void nes_reset(nes*); + +int nes_run(nes*, int cycles, int* run); + + +#endif // NES_H_ diff --git a/src/nese.c b/src/nese.c new file mode 100644 index 0000000..9d4be47 --- /dev/null +++ b/src/nese.c @@ -0,0 +1,72 @@ +#include "ines.h" + + + +void e6502_print_registers(const e6502_Registers* regs, + FILE* file) { + fprintf(file, "PC: $%04x\n", regs->PC); + fprintf(file, " S: $%02x\n", regs->S); + fprintf(file, " A: $%02x\n", regs->A); + fprintf(file, " X: $%02x\n", regs->X); + fprintf(file, " Y: $%02x\n", regs->Y); + fprintf(file, " P: $%02x\n", regs->P); +} + +void e6502_dump_mem(e6502_Core* core, uint16_t addr, + int len, FILE* file) { + for ( ; len > 0; --len, ++addr) { + fprintf(file, "$%04x: %02x\n", + addr, e6502_r8(core, addr)); + } +} + +void e6502_dump_stack(e6502_Core* core, FILE* file) { + int len = 0x100U + 2U - core->registers.S; + uint16_t addr = e6502_Memory_Stack + 0xFFU; + for ( ; len > 0; --len, --addr) { + fprintf(file, "$%03x: %02x\n", + addr, e6502_r8(core, addr)); + } +} + + + +static nes sys = {0}; + +int main(int argc, char* argv[]) { + int status = 0; + + status = ines_load(&sys.cart.rom, stdin); + + if (status == 0) { + nes_init(&sys); + nes_reset(&sys); + + int total_cycles = 0; + for (int i = 0; i < 15000; ++i) { + int run = 0; + status = nes_run(&sys, 1000, &run); + total_cycles += run; +/* + float us_run = ( run * 1000. * 1000. * + nes_clock_master_den) / + nes_clock_master_num; + fprintf(stdout, "Ran %f us, %d master cycles (%s)\n", + us_run, run, + status == 0 ? "OK" : "Halted"); +*/ + } + + float ms_run = ( total_cycles * 1000. * + nes_clock_master_den) / + nes_clock_master_num; + fprintf(stdout, "Ran %f ms, %d master cycles (%s)\n", + ms_run, total_cycles, + status == 0 ? "OK" : "Halted"); + + e6502_print_registers(&sys.cpu.registers, stdout); + e6502_dump_stack(&sys.cpu, stdout); + } + + return status; +} diff --git a/src/ppu.c b/src/ppu.c new file mode 100644 index 0000000..a760ae7 --- /dev/null +++ b/src/ppu.c @@ -0,0 +1,155 @@ +#include + +#include "ppu.h" + + +// TODO: Stubbed +// TODO: Retain open bus bits + +#define ppu_reg_ctrl (0x2000U) +#define ppu_reg_mask (0x2001U) +#define ppu_reg_status (0x2002U) +#define oam_reg_addr (0x2003U) +#define oam_reg_data (0x2004U) +#define ppu_reg_scroll (0x2005U) +#define ppu_reg_addr (0x2006U) +#define ppu_reg_data (0x2007U) +#define oam_reg_dma (0x4014U) + + +uint8_t nes_ppu_read(nes_ppu* ppu, uint16_t addr) { + uint8_t val = 0; + + if (ppu_reg_status == addr) { + val = ppu->status; + ppu->latch = 0; + + } else if (oam_reg_data == addr) { + val = ppu->oam[ppu->oam_addr]; + + } else if (ppu_reg_data == addr) { + val = ppu->data; + ppu->data = ppu->vram[ppu->addr - + nes_ppu_mem_vram_start]; + ppu->addr += (ppu->status & ppu_Control_VRAM_Inc) ? + 32 : 1; + } + + fprintf(stderr, "PPU: <-R $%04x %02x\n", addr, val); + + return val; +} + +void nes_ppu_write(nes_ppu* ppu, uint16_t addr, uint8_t val) { + fprintf(stderr, "PPU: W-> $%04x %02x\n", addr, val); + + if (ppu_reg_ctrl == addr) { + ppu->control = val; + // TODO: Trigger NMI if it's enabled during VBlank? + + } else if (oam_reg_addr == addr) { + ppu->oam_addr = val; + + } else if (oam_reg_data == addr) { + ppu->oam[ppu->oam_addr++] = val; + + } else if (ppu_reg_mask == addr) { + ppu->mask = val; + + } else if (ppu_reg_scroll == addr) { + if (ppu->latch) { + ppu->scroll &= 0xFF00U; + ppu->scroll |= val; + } else { + ppu->scroll &= 0x00FFU; + ppu->scroll |= (uint16_t)val << 8; + } + ppu->latch = !ppu->latch; + + } else if (ppu_reg_addr == addr) { + if (ppu->latch) { + ppu->addr &= 0xFF00U; + ppu->addr |= val; + } else { + ppu->addr &= 0x00FFU; + ppu->addr |= (uint16_t)(val & 0x3FU) << 8; + } + ppu->latch = !ppu->latch; + + } else if (ppu_reg_data == addr) { + ppu->vram[ppu->addr - nes_ppu_mem_vram_start] = val; + ppu->addr += (ppu->status & ppu_Control_VRAM_Inc) ? + 32 : 1; + } +} + +void nes_ppu_reset(nes_ppu* ppu) { + ppu->control = 0; + ppu->mask = 0; + ppu->latch = 0; + ppu->scroll = 0; + ppu->data = 0; + ppu->frame = 0; + ppu->scanline = 0; + ppu->cycle = 0; +} + +void nes_ppu_init(nes_ppu* ppu, uint8_t* chr_mem) { + ppu->chr_mem = chr_mem; + ppu->status = 0; + ppu->oam_addr = 0; + ppu->addr = 0; + nes_ppu_reset(ppu); +} + +int nes_ppu_run(nes_ppu* ppu, int cycles) { + int vblank = 0; + + ppu->cycle += cycles; + + while (ppu->cycle >= nes_ppu_dots) { + ppu->cycle -= nes_ppu_dots; + if ( ppu->scanline <= nes_ppu_prerender && + (ppu->frame & 1)) { + // Prerender line is one dot shorter in odd frames + // Fake it by incrementing the cycle in this case + ppu->cycle++; + } + ppu->scanline++; + if (ppu->scanline >= nes_ppu_prerender + + nes_ppu_height + + nes_ppu_postrender + + nes_ppu_vblank) { + ppu->status &= ~ppu_Status_VBlank; + ppu->scanline = 0; + ppu->frame++; + // TODO: Render callback? + } else if (ppu->scanline >= nes_ppu_prerender + + nes_ppu_height + + nes_ppu_postrender) { + ppu->status |= ppu_Status_VBlank; + if (ppu->control & ppu_Control_VBlank) { + vblank = 1; + } + } + } + + return vblank; +} + +#define nes_ppu_active_cycles \ + (nes_ppu_dots * (nes_ppu_prerender + \ + nes_ppu_height + \ + nes_ppu_postrender)) + +#define nes_ppu_vblank_cycles (nes_ppu_dots * nes_ppu_vblank) + +#define nes_frame_cycles (nes_ppu_active_cycles + \ + nes_ppu_vblank_cycles) + +int nes_ppu_cycles_til_vblank(nes_ppu* ppu) { + int cycles_til_vblank = nes_ppu_active_cycles - ( + ppu->cycle + (ppu->scanline * nes_ppu_dots)); + return (cycles_til_vblank > 0 ? cycles_til_vblank : + nes_ppu_vblank_cycles + cycles_til_vblank); +} diff --git a/src/ppu.h b/src/ppu.h new file mode 100644 index 0000000..2ae3308 --- /dev/null +++ b/src/ppu.h @@ -0,0 +1,64 @@ +#ifndef ENES_PPU_H_ +#define ENES_PPU_H_ + +#include + + +#define nes_ppu_dots (341U) +#define nes_ppu_prerender (1U) +#define nes_ppu_height (240U) +#define nes_ppu_postrender (1U) +#define nes_ppu_vblank (20U) + +#define nes_ppu_mem_vram_start (0x2000U) +#define nes_ppu_mem_vram_size (0x0800U) + +#define nes_ppu_oam_size (256U) + +typedef enum { + ppu_Control_Nametable_Mask = 0b00000011, + ppu_Control_VRAM_Inc = 0b00000100, + ppu_Control_Sprite_Addr = 0b00001000, + ppu_Control_Back_Addr = 0b00010000, + ppu_Control_Sprite_Size = 0b00100000, + ppu_Control_Master = 0b01000000, + ppu_Control_VBlank = 0b10000000, +} nes_ppu_Control; + +typedef enum { + ppu_Status_Open_Bus_Mask = 0b00011111, + ppu_Status_Overflow = 0b00100000, + ppu_Status_Hit = 0b01000000, + ppu_Status_VBlank = 0b10000000, +} nes_ppu_Status; + +typedef struct { + uint8_t* chr_mem; + uint8_t oam[nes_ppu_oam_size]; + uint8_t vram[nes_ppu_mem_vram_size]; + + int frame; + int scanline; + int cycle; + + uint8_t control; + uint8_t mask; + uint8_t status; + uint16_t scroll; + uint16_t addr; + uint8_t data; + uint8_t oam_addr; + + uint8_t latch; + +} nes_ppu; + +uint8_t nes_ppu_read(nes_ppu* ppu, uint16_t addr); +void nes_ppu_write(nes_ppu* ppu, uint16_t addr, uint8_t val); +void nes_ppu_reset(nes_ppu* ppu); +void nes_ppu_init(nes_ppu* ppu, uint8_t* chr_mem); + +int nes_ppu_run(nes_ppu* ppu, int cycles); +int nes_ppu_cycles_til_vblank(nes_ppu* ppu); + +#endif // ENES_PPU_H_