diff --git a/Makefile b/Makefile index b3ab609..7acfb93 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,57 @@ #CROSS_COMPILE = arm-none-eabi- +OS = linux + CC = $(CROSS_COMPILE)gcc LD = $(CC) #CFLAGS = -mcpu=cortex-m33 -mthumb CFLAGS += -Wall -Werror -Wshadow -CFLAGS += -g -Ofast -CFLAGS += -DF6502_FLAT -CFLAGS += -DF6502_TEST -CFLAGS += -DF6502_HCF +CFLAGS += -I src +CFLAGS += -g #-Ofast +#CFLAGS += -DF6502_FLAT +#CFLAGS += -DF6502_TEST +#CFLAGS += -DF6502_HCF #CFLAGS += -DF6502_TRACE -OBJS = src/f6502.o src/f6502_opcodes.o test.o -all: test +OBJDIR = obj +SRCDIR = src +BINDIR = bin + + +TEST_SRC_SRCS = f6502.c f6502_opcodes.c +TEST_SRCS = test.c + +TEST_SRCS += $(TEST_SRC_SRCS:%=$(SRCDIR)/%) +TEST_OBJS = $(TEST_SRCS:%.c=$(OBJDIR)/%.o) + + +MAPDIR = $(SRCDIR)/map + +NESE_SRC_SRCS = f6502.c f6502_opcodes.c +NESE_SRC_SRCS += nese.c nes.c cart.c mapper.c +NESE_SRC_SRCS += ppu.c +NESE_SRC_SRCS += $(OS)/port.c +NESE_MAP_SRCS = $(notdir $(wildcard $(MAPDIR)/*)) + +NESE_DEBUG = CART + +NESE_SRCS += $(NESE_SRC_SRCS:%=$(SRCDIR)/%) +NESE_SRCS += $(NESE_MAP_SRCS:%=$(MAPDIR)/%) +NESE_OBJS = $(NESE_SRCS:%.c=$(OBJDIR)/%.o) + + +all: $(BINDIR)/nese + +$(BINDIR)/test: CFLAGS += -DF6502_FLAT -DF6502_TEST -DF6502_TRACE + +$(BINDIR)/nese: CFLAGS += $(foreach debug,$(NESE_DEBUG), -DDEBUG_$(debug)) +$(BINDIR)/nese: $(NESE_OBJS) + @mkdir -p $(@D) + $(LD) $^ $(LDFLAGS) -o $@ -test: $(OBJS) - $(LD) $(LDFLAGS) -o $@ $^ +$(OBJDIR)/%.o : %.c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c $< -o $@ clean: - rm -rf $(OBJS) + rm -rf $(OBJDIR) $(BINDIR) diff --git a/src/cart.c b/src/cart.c new file mode 100644 index 0000000..3c062ea --- /dev/null +++ b/src/cart.c @@ -0,0 +1,83 @@ +#include +#include + +#include "cart.h" + + +#ifdef DEBUG_CART +#define DEBUG "Cart" +#endif +#include "log.h" + + +int nes_cart_load_mem(void* data, int size, nes* sys) { + int status = 0; + nes_Memory* mem = &sys->core.memory; + ines_Header* hdr = (ines_Header*)data; + void* ptr = &hdr[1]; + + // Verify magic + if (0 != memcmp(hdr->magic, INES_MAGIC, 4)) { + LOGE("Bad magic: got %.4s, expected %.4s", hdr->magic, INES_MAGIC); + status = -1; + } + + if (0 == status) { + // Skip the trainer + if (hdr->flags_6 & ines_Flag_Trainer) { + ptr += INES_TRAINER_SIZE; + } + + // Mark the PRG ROM + int rom_size = hdr->prg_size_lsb * INES_PRG_ROM_SIZE; + LOGI("PRG ROM: %d kB", rom_size / 1024); + mem->rom = ptr; + mem->n_rom_banks = rom_size / NES_PRG_ROM_PAGE_SIZE; + ptr += rom_size; + + // Mark the CHR ROM + if (hdr->chr_size_lsb > 0) { + int chr_size = hdr->chr_size_lsb * INES_CHR_ROM_SIZE; + LOGI("CHR ROM: %d kB", chr_size / 1024); + mem->ppu.chr = ptr; + mem->ppu.n_chr_banks = chr_size / + NES_CHR_ROM_PAGE_SIZE; + ptr += chr_size; + } + + // Fail if we're beyond the bounds + if (ptr - data > size) { + LOGE("Past ROM bounds by %d bytes", (int)((ptr - data) - size)); + status = -1; + } + } + + if (0 == status) { + // Unused & Palette + for (int page = 12; page < 16; ++page) { + mem->ppu.bank[page] = mem->ppu.pal_bank; + } + + nes_PPU_set_mirroring( + &mem->ppu, hdr->flags_6 & ines_Flag_Vert_Mirror + ); + + int i_mapper = (hdr->flags_7 & ines_Mapper_Nibble_Hi) | + ((hdr->flags_6 & ines_Mapper_Nibble_Lo) >> 4); + const nes_Mapper *mapper = nes_mappers[i_mapper]; + if (NULL == mapper || NULL == mapper->init) { + LOGE("Mapper %d not supported", i_mapper); + status = -1; + } else { + LOGI("Mapper %d", i_mapper); + mem->mapper = *mapper; + status = mem->mapper.init(&mem->mapper, hdr, mem); + } + } + + if (0 == status) { + sys->cart_header = hdr; + } + + return status; +} diff --git a/src/cart.h b/src/cart.h new file mode 100644 index 0000000..c927303 --- /dev/null +++ b/src/cart.h @@ -0,0 +1,11 @@ +#ifndef NESE_CART_H_ +#define NESE_CART_H_ + +#include "ines.h" +#include "nes.h" + + +int nes_cart_load_mem(void* mem, int size, nes* sys); + + +#endif // NESE_CART_H_ diff --git a/src/f6502.c b/src/f6502.c index f1609cb..4e0e4b5 100644 --- a/src/f6502.c +++ b/src/f6502.c @@ -19,18 +19,18 @@ // TOTO: Temp Hack volatile uint8_t memval; -static inline uint8_t f6502_read_zp(f6502_Memory* mem, +static inline uint8_t f6502_read_zp(nes_Memory* mem, uint8_t addr) { return mem->ram[addr]; } -static inline uint16_t f6502_read16_zp(f6502_Memory* mem, +static inline uint16_t f6502_read16_zp(nes_Memory* mem, uint8_t addr) { return ( f6502_read_zp(mem, addr) | ((uint16_t)f6502_read_zp(mem, addr + 1) << 8)); } -static inline uint8_t f6502_read(f6502_Memory* mem, +static inline uint8_t f6502_read(nes_Memory* mem, uint16_t addr) { #ifdef F6502_FLAT #ifdef F6502_TRACE @@ -55,7 +55,9 @@ static inline uint8_t f6502_read(f6502_Memory* mem, return 0; case 0x6000: - return mem->sram_bank[addr & 0x1FFFU]; + if (mem->sram_bank) { + return mem->sram_bank[addr & 0x1FFFU]; + } } // Upper byte stays on the bus @@ -63,15 +65,15 @@ static inline uint8_t f6502_read(f6502_Memory* mem, #endif } -static inline uint16_t f6502_read16(f6502_Memory* mem, +static inline uint16_t f6502_read16(nes_Memory* mem, uint16_t addr) { return ( f6502_read(mem, addr) | ((uint16_t)f6502_read(mem, addr + 1) << 8)); } -static inline int f6502_write(f6502_Memory* mem, - uint16_t addr, uint8_t val) { - int ret = 0; +static inline bool f6502_write(nes_Memory* mem, + uint16_t addr, uint8_t val) { + bool ret = 0; #ifdef F6502_TRACE printf("W $%04X <- %02X\n", addr, val); #endif @@ -100,15 +102,36 @@ static inline int f6502_write(f6502_Memory* mem, #endif mem->ram[addr] = val; #else - // TODO - memval = val; + switch (addr & 0xe000) { + case 0x0000: + mem->ram[addr & 0x7FFU] = val; + break; + + case 0x2000: + // TODO: PPU + break; + + case 0x4000: + // TODO: APU + break; + + case 0x6000: + if (mem->sram_bank) { + mem->sram_bank[addr & 0x1FFFU] = val; + mem->flags |= nes_Mem_SRAM_Used; + + } else if (mem->mapper.write_sram) { + ret = mem->mapper.write_sram(&mem->mapper, + addr, val); + } + } #endif return ret; } void f6502_init(f6502_Core* core) { - // TODO: ??? + // TODO: Nothing for now } void f6502_reset(f6502_Core* core) { @@ -686,7 +709,6 @@ static int f6502_do_step(f6502_Core* core, int clocks) { JMP(addr); } #else - // TODO: Can check for HCF loop here JMP(A_ABS); #endif CLK(3); @@ -1408,7 +1430,9 @@ static int f6502_do_step(f6502_Core* core, int clocks) { if (int_trig) printf("Possible interrupt trigger\n"); #endif +#ifdef F6502_HCF step_done: +#endif core->registers = (f6502_Registers){ .PC = PC, .S = S, @@ -1424,6 +1448,7 @@ step_done: int f6502_step(f6502_Core* core, int clocks) { int clocks_elapsed = 0; // TODO: Why are we processing seven clocks prior to NMI? + // This passes the interrupt test with 6 steps /* if (f6502_nmi_ready(core)) { clocks_elapsed += f6502_do_step(core, 7); diff --git a/src/f6502.h b/src/f6502.h index dc5c387..d5791f8 100644 --- a/src/f6502.h +++ b/src/f6502.h @@ -4,8 +4,7 @@ #include #include - -#define F6502_RAM_SIZE (0x2000U) +#include "memory.h" typedef enum { @@ -41,22 +40,12 @@ typedef enum { f6502_Int_NMI_Serviced = 0b10000000, } f6502_Interrupt; -typedef struct __attribute__ ((__packed__)) { -#ifdef F6502_FLAT - uint8_t ram[65536]; -#else - uint8_t ram[F6502_RAM_SIZE / 4]; // Mirrored 3x - uint8_t *sram_bank; - uint8_t *rom_bank[4]; - // TODO -#endif -} f6502_Memory; - -typedef struct __attribute__ ((__packed__)) { +struct f6502_Core { f6502_Registers registers; f6502_Interrupt interrupts; - f6502_Memory memory; -} f6502_Core; + nes_Memory memory; +}; +typedef struct f6502_Core f6502_Core; void f6502_init(f6502_Core*); void f6502_reset(f6502_Core*); diff --git a/src/ines.h b/src/ines.h new file mode 100644 index 0000000..09b69e0 --- /dev/null +++ b/src/ines.h @@ -0,0 +1,45 @@ +#ifndef NESE_INES_H_ +#define NESE_INES_H_ + +#include + + +#define INES_TRAINER_SIZE (0x0200U) +#define INES_PRG_ROM_SIZE (0x4000U) +#define INES_CHR_ROM_SIZE (0x2000U) + +#define INES_MAGIC "NES\x1a" + +typedef enum __attribute__((packed)) { + ines_Flag_Vert_Mirror = 0b00000001, + ines_Flag_Battery = 0b00000010, + ines_Flag_Trainer = 0b00000100, + ines_Flag_Alt_Nametable = 0b00001000, + ines_Mapper_Nibble_Lo = 0b11110000, +} ines_6_Flag; + +typedef enum __attribute__((packed)) { + 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 __attribute__((packed)) { + char magic[4]; + uint8_t prg_size_lsb; + uint8_t chr_size_lsb; + ines_6_Flag flags_6; + ines_7_Flag flags_7; + uint8_t prg_ram_size; + uint8_t tv_system; + uint8_t flags_10; + uint8_t padding[5]; +} ines_Header; + + +#endif // NESE_INES_H_ diff --git a/src/linux/port.c b/src/linux/port.c new file mode 100644 index 0000000..368b8d9 --- /dev/null +++ b/src/linux/port.c @@ -0,0 +1,40 @@ +#include +#include + +#include + +#include "nese.h" +#include "port.h" + +#define DEBUG "Port" +#include "log.h" + + +void* nese_map_file(FILE* file, int size) { + void* addr = mmap(NULL, size, PROT_READ, MAP_SHARED, + fileno(file), 0); + if (MAP_FAILED == addr || NULL == addr) { + fprintf(stderr, "mmap failed: %d\n", (int)errno); + addr = NULL; + } + return addr; +} + +int nese_unmap_file(void* addr, int size) { + return munmap(addr, size); +} + + +static nes sys = {0}; + + +int main(int argc, char* argv[]) { + int status = 0; + if (argc <= 1) { + LOGE("No ROM file provided."); + status = -1; + } else { + status = nese_start(&sys, argv[1]); + } + return status; +} diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..6653590 --- /dev/null +++ b/src/log.h @@ -0,0 +1,19 @@ +#include + + +#ifdef DEBUG + +#define LOG(l, f, ...) fprintf(stderr, l ": " DEBUG ": " f "\n" __VA_OPT__(,) __VA_ARGS__) + +#else // !DEBUG + +#define LOG(...) + +#endif // DEBUG + + +#define LOGE(f, ...) LOG("E", f, __VA_ARGS__) +#define LOGW(f, ...) LOG("W", f, __VA_ARGS__) +#define LOGI(f, ...) LOG("I", f, __VA_ARGS__) +#define LOGD(f, ...) LOG("D", f, __VA_ARGS__) +#define LOGV(f, ...) LOG("V", f, __VA_ARGS__) diff --git a/src/map/map000.c b/src/map/map000.c new file mode 100644 index 0000000..86eb89d --- /dev/null +++ b/src/map/map000.c @@ -0,0 +1,31 @@ +#include "memory.h" + + +static int map000_init(nes_Mapper* map, const ines_Header* hdr, + nes_Memory* mem) { + if (mem->n_rom_banks > 2) { + mem->rom_bank[0] = prg_rom_page(mem, 0); + mem->rom_bank[1] = prg_rom_page(mem, 1); + mem->rom_bank[2] = prg_rom_page(mem, 2); + mem->rom_bank[3] = prg_rom_page(mem, 3); + } else { + mem->rom_bank[0] = prg_rom_page(mem, 0); + mem->rom_bank[1] = prg_rom_page(mem, 1); + mem->rom_bank[2] = prg_rom_page(mem, 0); + mem->rom_bank[3] = prg_rom_page(mem, 1); + } + + int page = 0; + // Pattern tables + for ( ; page < 8; ++page) { + mem->ppu.bank[page] = chr_page(&mem->ppu, page); + } + + return 0; +} + + +const nes_Mapper map000 = { + .max_chr_banks = 8, + .init = map000_init, +}; diff --git a/src/mapper.c b/src/mapper.c new file mode 100644 index 0000000..e49db98 --- /dev/null +++ b/src/mapper.c @@ -0,0 +1,9 @@ +#include "mapper.h" + + +extern const nes_Mapper map000; + + +const nes_Mapper* nes_mappers[256] = { + &map000, +}; diff --git a/src/mapper.h b/src/mapper.h new file mode 100644 index 0000000..88c282b --- /dev/null +++ b/src/mapper.h @@ -0,0 +1,30 @@ +#ifndef NESE_MAPPER_H_ +#define NESE_MAPPER_H_ + +#include + +#include "ines.h" + + +struct nes_Memory; + +struct nes_Mapper { + int max_chr_banks; + int (*init)(struct nes_Mapper*, const ines_Header*, struct nes_Memory*); + bool (*write_rom)(struct nes_Mapper*, uint16_t addr, uint8_t val); + bool (*write_sram)(struct nes_Mapper*, uint16_t addr, uint8_t val); + bool (*write_apu)(struct nes_Mapper*, uint16_t addr, uint8_t val); + uint8_t (*read_apu)(struct nes_Mapper*, uint16_t addr); + void (*hsync)(struct nes_Mapper*); + void (*vsync)(struct nes_Mapper*); + void (*ppu_bus)(struct nes_Mapper*, uint16_t addr); + void (*ppu_mem_mode)(struct nes_Mapper*, uint8_t mode); + void* data; +}; +typedef struct nes_Mapper nes_Mapper; + + +extern const nes_Mapper* nes_mappers[256]; + + +#endif // NESE_MAPPER_H_ diff --git a/src/memory.h b/src/memory.h new file mode 100644 index 0000000..c1bc0af --- /dev/null +++ b/src/memory.h @@ -0,0 +1,50 @@ +#ifndef NESE_MEMORY_H_ +#define NESE_MEMORY_H_ + +#include + +#include "mapper.h" +#include "ppu.h" + + +#define NES_RAM_SIZE (0x2000U) +#define NES_SRAM_SIZE (0x2000U) + +#define NES_PRG_ROM_PAGE_SIZE (0x2000U) + + +typedef enum { + nes_Mem_SRAM_Used = 0b00000001, +} nes_Memory_Flag; + +struct nes_Memory { +#ifdef F6502_FLAT + uint8_t ram[65536]; +#else + uint8_t ram[NES_RAM_SIZE / 4]; // Mirrored 3x + nes_Memory_Flag flags; + uint8_t reserved[3]; + uint8_t* sram; // Actual SRAM, if used - could this be a flag? + uint8_t* sram_bank; // Mapped to 0x6000 - 0x7FFF + uint8_t* rom; + uint8_t* rom_bank[4]; + int n_rom_banks; + nes_PPU_Memory ppu; + nes_Mapper mapper; +#endif +}; +typedef struct nes_Memory nes_Memory; + + +static inline uint8_t* prg_rom_page(nes_Memory* mem, int page) { + return &mem->rom[page * NES_PRG_ROM_PAGE_SIZE]; +} + +static inline uint8_t* prg_rom_last_page(nes_Memory* mem, int page) { + // TODO: Where's 0x2000U from? + return &mem->rom[ (mem->n_rom_banks * NES_PRG_ROM_PAGE_SIZE) - + ((page + 1) * 0x2000U)]; +} + + +#endif // NESE_MEMORY_H_ diff --git a/src/nes.c b/src/nes.c new file mode 100644 index 0000000..758108f --- /dev/null +++ b/src/nes.c @@ -0,0 +1,52 @@ +#include + +#include "nes.h" + + +void nes_init(nes* sys) { + f6502_init(&sys->core); +} + +void nes_done(nes* sys) { + // TODO: deallocate RAM, etc. +} + +static int nes_hsync(nes* sys) { + // TODO: APU sync + + // TODO: PPU draw line if visible + + // TODO: PPU update regs + + // TODO: Increment scanline + + // TODO: Scanline region updates + // TODO: nese_vsync called in nes_vsync + + return 0; +} + +int nes_loop(nes* sys) { + int status = 0; + int dot = 0; + + while (0 == status) { + // TODO: Partial scanline if sprite 0 hit + int dots_remaining = nes_ppu_scanline_dots - dot; + int cpu_cycles = (dots_remaining + 2) / 3; + dot += 3 * f6502_step(&sys->core, cpu_cycles); + dot -= nes_ppu_scanline_dots; + // TODO: Validate dot >= 0? + + // TODO: Frame IRQ + + nes_Memory* mem = &sys->core.memory; + if (NULL != mem->mapper.hsync) { + mem->mapper.hsync(&mem->mapper); + } + + status = nes_hsync(sys); + } + + return status; +} diff --git a/src/nes.h b/src/nes.h new file mode 100644 index 0000000..d06d130 --- /dev/null +++ b/src/nes.h @@ -0,0 +1,21 @@ +#ifndef NES_H_ +#define NES_H_ + +#include "ines.h" +#include "f6502.h" + + +typedef struct { + const ines_Header* cart_header; + f6502_Core core; + // TODO: PPU + // TODO: APU + // TODO: Input +} nes; + +void nes_init(nes*); +void nes_done(nes*); +int nes_loop(nes*); + + +#endif // NES_H_ diff --git a/src/nese.c b/src/nese.c new file mode 100644 index 0000000..057e6c7 --- /dev/null +++ b/src/nese.c @@ -0,0 +1,49 @@ +#include + +#include "cart.h" +#include "nese.h" +#include "port.h" + + +static int nese_file_size(FILE* file) { + int size = -1; + if (0 == fseek(file, 0, SEEK_END)) { + size = ftell(file); + } + return size; +} + + +int nese_start(nes* sys, const char* filename) { + int status = 0; + void* cart_data = NULL; + int filesize = -1; + FILE* file = NULL; + + file = fopen(filename, "rb"); + + if (NULL == file) { + status = -1; + } + + if (0 == status) { + filesize = nese_file_size(file); + cart_data = nese_map_file(file, filesize); + if (NULL == cart_data) { + fprintf(stderr, "Failed to map %s\n", filename); + status = -1; + } else { + status = nes_cart_load_mem(cart_data, filesize, sys); + } + } + + nes_init(sys); + + nes_loop(sys); + + if (cart_data) nese_unmap_file(cart_data, filesize); + + nes_done(sys); + + return 0; +} diff --git a/src/nese.h b/src/nese.h new file mode 100644 index 0000000..3851867 --- /dev/null +++ b/src/nese.h @@ -0,0 +1,10 @@ +#ifndef NESE_H_ +#define NESE_H_ + +#include "nes.h" + + +int nese_start(nes* sys, const char* filename); + + +#endif // NESE_H_ diff --git a/src/port.h b/src/port.h new file mode 100644 index 0000000..9e611bd --- /dev/null +++ b/src/port.h @@ -0,0 +1,9 @@ +#ifndef NESE_PORT_H_ +#define NESE_PORT_H_ + + +void* nese_map_file(FILE* file, int size); +int nese_unmap_file(void* addr, int size); + + +#endif // NESE_PORT_H_ diff --git a/src/ppu.c b/src/ppu.c new file mode 100644 index 0000000..0ac7247 --- /dev/null +++ b/src/ppu.c @@ -0,0 +1,20 @@ +#include "ppu.h" + + +static const uint8_t mirror_schemes[][4] = { + [nes_Mirror_Horizontal] = {0, 0, 1, 1}, + [nes_Mirror_Vertical] = {0, 1, 0, 1}, + [nes_Mirror_Only_0] = {0, 0, 0, 0}, + [nes_Mirror_Only_1] = {1, 1, 1, 1}, + [nes_Mirror_Four] = {0, 1, 2, 3}, +}; + + +void nes_PPU_set_mirroring(nes_PPU_Memory* mem, + nes_Nametable_Mirroring mirror) { + for (int i = 0; i < 4; ++i) { + mem->bank[nes_PPU_Nametable_Bank_Index + i] = + vram_page(mem, (int)mirror_schemes[mirror][0]); + } +} + diff --git a/src/ppu.h b/src/ppu.h new file mode 100644 index 0000000..8df7505 --- /dev/null +++ b/src/ppu.h @@ -0,0 +1,56 @@ +#ifndef NESE_PPU_H_ +#define NESE_PPU_H_ + +#include + + +#define nes_ppu_scanline_dots (341U) +#define nes_ppu_visible_line (1U) +#define nes_ppu_postrender_line (241U) +#define nes_ppu_vblank_line (242U) +#define nes_ppu_frame_end_line (262U) + +#define NES_CHR_ROM_PAGE_SIZE (0x0400U) +#define NES_VRAM_PAGE_SIZE (0x0400U) + +#define NES_PPU_CHR_SIZE (0x2000U) +#define NES_PPU_VRAM_SIZE (0x1000U) +#define NES_PPU_RAM_SIZE (0x4000U) +#define NES_PPU_OAM_SIZE (64U * 4U) + +typedef struct __attribute__ ((__packed__)) { + uint8_t* chr; + int n_chr_banks; + uint8_t* chr_ram; // Could this be a flag? + uint8_t* bank[16]; + uint8_t vram[NES_PPU_VRAM_SIZE]; + uint8_t pal_bank[NES_CHR_ROM_PAGE_SIZE]; // Mirrored in banks 12-15, also pal + uint8_t oam[NES_PPU_OAM_SIZE]; +} nes_PPU_Memory; + + +static inline uint8_t* chr_page(nes_PPU_Memory* mem, + int page) { + return &mem->chr[page * NES_CHR_ROM_PAGE_SIZE]; +} + +static inline uint8_t* vram_page(nes_PPU_Memory* mem, + int page) { + return &mem->vram[page * NES_VRAM_PAGE_SIZE]; +} + +#define nes_PPU_Nametable_Bank_Index (8U) + +typedef enum { + nes_Mirror_Horizontal = 0, + nes_Mirror_Vertical, + nes_Mirror_Only_0, + nes_Mirror_Only_1, + nes_Mirror_Four, +} nes_Nametable_Mirroring; + +void nes_PPU_set_mirroring(nes_PPU_Memory* mem, + nes_Nametable_Mirroring mirror); + + +#endif // NESE_PPU_H_