| @@ -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 $@ | |||
| @@ -0,0 +1 @@ | |||
| ../e6502/ | |||
| @@ -0,0 +1,18 @@ | |||
| #ifndef ENES_APU_H_ | |||
| #define ENES_APU_H_ | |||
| #include <stdint.h> | |||
| // 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_ | |||
| @@ -0,0 +1,84 @@ | |||
| #include <string.h> | |||
| #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; | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| #ifndef INES_H_ | |||
| #define INES_H_ | |||
| #include <stdint.h> | |||
| #include <stdio.h> | |||
| #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_ | |||
| @@ -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; | |||
| } | |||
| @@ -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_ | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,155 @@ | |||
| #include <stdio.h> | |||
| #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); | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| #ifndef ENES_PPU_H_ | |||
| #define ENES_PPU_H_ | |||
| #include <stdint.h> | |||
| #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_ | |||