| @@ -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_ | |||||