From 9039fa0ccfe7bc2437a0bea52d66cfd2be601329 Mon Sep 17 00:00:00 2001 From: Lauchmelder Date: Thu, 28 Oct 2021 16:57:37 +0200 Subject: [PATCH] added mappers (not working) --- NES Emulator/CMakeLists.txt | 2 +- NES Emulator/bus.c | 4 +- NES Emulator/cartridge.c | 70 +++++-------- NES Emulator/cartridge.h | 13 ++- NES Emulator/cpu.c | 14 +++ NES Emulator/main.c | 34 +++++-- NES Emulator/mapper.c | 47 +++++++++ NES Emulator/mapper.h | 31 ++++++ NES Emulator/mappers/mapper000.c | 105 +++++++++++++++++++ NES Emulator/mappers/mapper000.h | 27 +++++ NES Emulator/ppu.c | 168 +++++++++++++++++++++++-------- NES Emulator/ppu.h | 103 ++++++++++++++++++- roms/cpu.nes | Bin 0 -> 270352 bytes roms/donkeykong.nes | Bin 0 -> 24592 bytes 14 files changed, 512 insertions(+), 106 deletions(-) create mode 100644 NES Emulator/mapper.c create mode 100644 NES Emulator/mapper.h create mode 100644 NES Emulator/mappers/mapper000.c create mode 100644 NES Emulator/mappers/mapper000.h create mode 100644 roms/cpu.nes create mode 100644 roms/donkeykong.nes diff --git a/NES Emulator/CMakeLists.txt b/NES Emulator/CMakeLists.txt index aa29dc1..40e4070 100644 --- a/NES Emulator/CMakeLists.txt +++ b/NES Emulator/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required (VERSION 3.8) -add_executable (nesemu "main.c" "cpu.h" "types.h" "bus.h" "bus.c" "cartridge.h" "cpu.c" "cartridge.c" "opcodes.c" "log.c" "ppu.h" "ppu.c") +add_executable (nesemu "main.c" "cpu.h" "types.h" "bus.h" "bus.c" "cartridge.h" "cpu.c" "cartridge.c" "opcodes.c" "log.c" "ppu.h" "ppu.c" "mapper.h" "mappers/mapper000.h" "mappers/mapper000.c" "mapper.c") target_include_directories(nesemu PUBLIC SDL2-static) target_link_libraries(nesemu PUBLIC SDL2-static SDL2main) diff --git a/NES Emulator/bus.c b/NES Emulator/bus.c index 49c8ba3..8bf3c9b 100644 --- a/NES Emulator/bus.c +++ b/NES Emulator/bus.c @@ -78,7 +78,7 @@ Byte readBus(struct Bus* bus, Word addr) } else if (0x4020 <= addr && addr <= 0xFFFF) // Cartridge space { - val = readCartridge(bus->cartridge, addr); + val = readCartridgeCPU(bus->cartridge, addr); } else { @@ -106,7 +106,7 @@ void writeBus(struct Bus* bus, Word addr, Byte val) } else if (0x4020 <= addr && addr <= 0xFFFF) // Cartridge space { - writeCartridge(bus->cartridge, addr, val); + writeCartridgeCPU(bus->cartridge, addr, val); } else { diff --git a/NES Emulator/cartridge.c b/NES Emulator/cartridge.c index bc6c768..143f3e0 100644 --- a/NES Emulator/cartridge.c +++ b/NES Emulator/cartridge.c @@ -3,6 +3,7 @@ #include #include "bus.h" +#include "mapper.h" struct Cartridge* createCartridge(struct Bus* parent, const char* filepath) { @@ -17,63 +18,42 @@ struct Cartridge* createCartridge(struct Bus* parent, const char* filepath) fread(&cartridge->header, sizeof(cartridge->header), 1, fp); - cartridge->prg_rom = (Byte*)malloc(0x4000 * (size_t)cartridge->header.prg_rom_size); - if (cartridge->prg_rom == NULL) - { - fprintf(stderr, "Failed to allocate PRG memory for cartridge, aborting.\n"); - exit(1); - } - - cartridge->chr_rom = (Byte*)malloc(0x2000 * (size_t)cartridge->header.chr_rom_size); - if (cartridge->chr_rom == NULL) - { - fprintf(stderr, "Failed to allocate CHR memory for cartridge, aborting.\n"); - exit(1); - } - - fread(cartridge->prg_rom, 0x4000, cartridge->header.prg_rom_size, fp); - fread(cartridge->chr_rom, 0x2000, cartridge->header.chr_rom_size, fp); + Byte mapperID = (cartridge->header.Flags7.mapper_upper_nibble << 4) | cartridge->header.Flags6.mapper_lower_nibble; + cartridge->mapper = createMapper(mapperID, cartridge->header.prg_rom_size, cartridge->header.chr_rom_size, fp); cartridge->bus = parent; + + fclose(fp); return cartridge; } void destroyCartridge(struct Cartridge* cartridge) { - free(cartridge->chr_rom); - free(cartridge->prg_rom); - + destroyMapper(cartridge->mapper); free(cartridge); } -Byte readCartridge(struct Cartridge* cartridge, Word addr) +Byte readCartridgeCPU(struct Cartridge* cartridge, Word addr) { - // TODO: Force M000 mapper for now - Byte val = 0; - - if (0x6000 <= addr && addr <= 0x7FFF) // PRG RAM - { - // do nothing for now - } - else if (0x8000 <= addr && addr <= 0xFFFF) // PRG ROM - { - val = cartridge->prg_rom[addr % 0x4000]; - } - - return val; + return MapperReadCPU(cartridge->mapper, addr); } -void writeCartridge(struct Cartridge* cartridge, Word addr, Byte val) +Byte readCartridgePPU(struct Cartridge* cartridge, Word addr) { - // TODO: Force M000 mapper for now - - if (0x6000 <= addr && addr <= 0x7FFF) // PRG RAM - { - // do nothing for now - } - else if (0x8000 <= addr && addr <= 0xFFFF) // PRG ROM - { - cartridge->prg_rom[addr % 0x4000] = val; - } - + return MapperReadPPU(cartridge->mapper, addr); +} + +void writeCartridgeCPU(struct Cartridge* cartridge, Word addr, Byte val) +{ + MapperWriteCPU(cartridge->mapper, addr, val); +} + +void writeCartridgePPU(struct Cartridge* cartridge, Word addr, Byte val) +{ + MapperReadPPU(cartridge->mapper, addr, val); +} + +void getPatternTableTexture(struct Cartridge* cartridge, SDL_Texture* texture, int index) +{ + GetPatternTableTexture(cartridge->mapper, texture, index); } diff --git a/NES Emulator/cartridge.h b/NES Emulator/cartridge.h index 4650e01..922e006 100644 --- a/NES Emulator/cartridge.h +++ b/NES Emulator/cartridge.h @@ -2,8 +2,10 @@ #define _CARTRIDGE_H_ #include "types.h" +#include "SDL.h" struct Bus; +struct Mapper; struct Cartridge { @@ -34,8 +36,7 @@ struct Cartridge Byte unused[7]; } header; - Byte* prg_rom; - Byte* chr_rom; + struct Mapper* mapper; struct Bus* bus; }; @@ -44,7 +45,11 @@ struct Cartridge* createCartridge(struct Bus* parent, const char* filepath); void destroyCartridge(struct Cartridge* cartridge); // readCartridge/writeCartridge to and from the cartridge -Byte readCartridge(struct Cartridge* cartridge, Word addr); -void writeCartridge(struct Cartridge* cartridge, Word addr, Byte val); +Byte readCartridgeCPU(struct Cartridge* cartridge, Word addr); +Byte readCartridgePPU(struct Cartridge* cartridge, Word addr); +void writeCartridgeCPU(struct Cartridge* cartridge, Word addr, Byte val); +void writeCartridgePPU(struct Cartridge* cartridge, Word addr, Byte val); + +void getPatternTableTexture(struct Cartridge* cartridge, SDL_Texture* texture, int index); #endif //_CARTRIDGE_H_ \ No newline at end of file diff --git a/NES Emulator/cpu.c b/NES Emulator/cpu.c index 8ffe8bd..8746aec 100644 --- a/NES Emulator/cpu.c +++ b/NES Emulator/cpu.c @@ -68,6 +68,7 @@ int tickCPU(struct CPU* cpu) cpu->nmi = 0; + printf("NMI TRIGGERED FROM $%04x\n", cpu->pc.word); cpu->pc.lo = readBus(cpu->bus, 0xFFFA); cpu->pc.hi = readBus(cpu->bus, 0xFFFB); @@ -335,6 +336,19 @@ void execute(struct CPU* cpu) } } break; + case BRK: + { + cpu->pc.word++; + Push(cpu->bus, cpu->pc.hi); + Push(cpu->bus, cpu->pc.lo); + Push(cpu->bus, cpu->status.raw | 0b00110000); + + cpu->status.id = 1; + cpu->pc.hi = readBus(cpu->bus, 0xFFFF); + cpu->pc.lo = readBus(cpu->bus, 0xFFFE); + + } break; + case BVC: { if (!cpu->status.overflow) diff --git a/NES Emulator/main.c b/NES Emulator/main.c index f5bcc42..b367ed3 100644 --- a/NES Emulator/main.c +++ b/NES Emulator/main.c @@ -1,5 +1,6 @@ #include "bus.h" #include "ppu.h" +#include "cartridge.h" #include #include @@ -8,7 +9,7 @@ int main(int argc, char** argv) { SDL_Init(SDL_INIT_VIDEO); - SDL_Window* window = SDL_CreateWindow("NES Emulator", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 800, 800, SDL_WINDOW_SHOWN); + SDL_Window* window = SDL_CreateWindow("NES Emulator", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1400, 800, SDL_WINDOW_SHOWN); if (window == NULL) { fprintf(stderr, "Failed to create SDL_Window.\n"); @@ -24,6 +25,17 @@ int main(int argc, char** argv) struct Bus* bus = createBus(renderer); + SDL_Texture* patternTables[2]; + for (int i = 0; i < 2; i++) + { + patternTables[i] = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGB332, SDL_TEXTUREACCESS_STREAMING, 64, 64); + if (patternTables[i] == NULL) + { + fprintf(stderr, "Failed to create pattern table texture\n"); + exit(-1); + } + } + SDL_Event event; int running = 1; while(running) @@ -49,15 +61,15 @@ int main(int argc, char** argv) SDL_SetRenderDrawColor(renderer, 20, 0, 20, 0); SDL_RenderClear(renderer); - SDL_Texture* tableTexture = getPatternTableTexture(bus->ppu, 0); + getPatternTableTexture(bus->cartridge, patternTables[0], 0); SDL_Rect target = { 10, 10, 256, 256 }; - SDL_RenderCopy(renderer, tableTexture, NULL, &target); + SDL_RenderCopy(renderer, patternTables[0], NULL, &target); - tableTexture = getPatternTableTexture(bus->ppu, 1); + getPatternTableTexture(bus->cartridge, patternTables[1], 1); target.x = 256 + 10 + 10; - SDL_RenderCopy(renderer, tableTexture, NULL, &target); + SDL_RenderCopy(renderer, patternTables[1], NULL, &target); - tableTexture = getNameTableTexture(bus->ppu, 0); + SDL_Texture* tableTexture = getNameTableTexture(bus->ppu, 0); target.x = 10; target.y = 256 + 10 + 10; SDL_RenderCopy(renderer, tableTexture, NULL, &target); @@ -66,9 +78,19 @@ int main(int argc, char** argv) target.x = 256 + 10 + 10; SDL_RenderCopy(renderer, tableTexture, NULL, &target); + tableTexture = getScreenTexture(bus->ppu); + target.x = 10 + 256 + 10 + 256 + 10; + target.y = 10; + target.w = 256 * 3; + target.h = 240 * 3; + SDL_RenderCopy(renderer, tableTexture, NULL, &target); + SDL_RenderPresent(renderer); } + for (int i = 0; i < 2; i++) + SDL_DestroyTexture(patternTables[i]); + destroyBus(bus); SDL_DestroyRenderer(renderer); diff --git a/NES Emulator/mapper.c b/NES Emulator/mapper.c new file mode 100644 index 0000000..9174f40 --- /dev/null +++ b/NES Emulator/mapper.c @@ -0,0 +1,47 @@ +#include "mapper.h" + +#include +#include + +#include "mappers/mapper000.h" + +struct Mapper* createMapper(Byte id, Byte prg_rom_size, Byte chr_rom_size, FILE* fp) +{ + struct Mapper* mapper = (struct Mapper*)malloc(sizeof(struct Mapper)); + if (mapper == NULL) + { + fprintf(stderr, "Failed to create Mapper. Aborting\n"); + exit(-1); + } + + mapper->id = id; + + switch (id) + { + case 0: + { + struct Mapper000* mp = createMapper000(prg_rom_size, chr_rom_size, fp); + mapper->mapperStruct = (void*)mp; + + mapper->read_cpu = &Mapper000_ReadCPU; + mapper->read_ppu = &Mapper000_ReadPPU; + mapper->write_cpu = &Mapper000_WriteCPU; + mapper->write_ppu = &Mapper000_WritePPU; + mapper->get_pattern_table_texture = &Mapper000_GetPatternTableTexture; + } break; + + default: + fprintf(stderr, "Mapper with ID %d is not implemented\n", id); + exit(-1); + } +} + +void destroyMapper(struct Mapper* mapper) +{ + switch (mapper->id) + { + case 0: destroyMapper000((struct Mapper000*)mapper->mapperStruct); break; + } + + free(mapper); +} diff --git a/NES Emulator/mapper.h b/NES Emulator/mapper.h new file mode 100644 index 0000000..6abea0a --- /dev/null +++ b/NES Emulator/mapper.h @@ -0,0 +1,31 @@ +#ifndef _MAPPER_H_ +#define _MAPPER_H_ + +#include +#include "types.h" + +struct PPU; + +struct Mapper +{ + Byte id; + void* mapperStruct; + + Byte(*read_cpu)(void*, Word); + Byte(*read_ppu)(void*, Word); + void(*write_cpu)(void*, Word, Byte); + void(*write_ppu)(void*, Word, Byte); + + void(*get_pattern_table_texture)(void*, void*, int); +}; + +#define MapperReadCPU(mapper, addr) mapper->read_cpu(mapper->mapperStruct, addr) +#define MapperReadPPU(mapper, addr) mapper->read_ppu(mapper->mapperStruct, addr) +#define MapperWriteCPU(mapper, addr, val) mapper->write_cpu(mapper->mapperStruct, addr, val) +#define MapperWritePPU(mapper, addr, val) mapper->write_ppu(mapper->mapperStruct, addr, val) +#define GetPatternTableTexture(mapper, texture, index) mapper->get_pattern_table_texture(mapper->mapperStruct, texture, index) + +struct Mapper* createMapper(Byte id, Byte prg_rom_size, Byte chr_rom_size, FILE* fp); +void destroyMapper(struct Mapper* mapper); + +#endif // _MAPPER_H_ \ No newline at end of file diff --git a/NES Emulator/mappers/mapper000.c b/NES Emulator/mappers/mapper000.c new file mode 100644 index 0000000..3e7cdd4 --- /dev/null +++ b/NES Emulator/mappers/mapper000.c @@ -0,0 +1,105 @@ +#include "mapper000.h" + +#include +#include + +struct Mapper000* createMapper000(Byte prg_rom_size, Byte chr_rom_size, FILE* fp) +{ + struct Mapper000* mapper = (struct Mapper000*)malloc(sizeof(struct Mapper000)); + if (mapper == NULL) + { + fprintf(stderr, "Failed to create Mapper 000. Aborting\n"); + exit(-1); + } + + mapper->prg_rom_size = prg_rom_size; + mapper->prg_rom = (Byte*)calloc(prg_rom_size, 0x4000); + if (mapper->prg_rom == NULL) + { + fprintf(stderr, "Failed to allocate cartridge PRG ROM. Aborting\n"); + exit(-1); + } + + mapper->chr_rom_size = chr_rom_size; + mapper->chr_rom = (Byte*)calloc(chr_rom_size, 0x2000); + if (mapper->chr_rom == NULL) + { + fprintf(stderr, "Failed to allocate cartridge CHR ROM. Aborting\n"); + exit(-1); + } + + fread(mapper->prg_rom, 0x4000, prg_rom_size, fp); + fread(mapper->chr_rom, 0x2000, chr_rom_size, fp); + + return mapper; +} + +void destroyMapper000(struct Mapper000* mapper) +{ + free(mapper->chr_rom); + free(mapper->prg_rom); + + free(mapper); +} + +Byte Mapper000_ReadCPU(struct Mapper000* mapper, Word address) +{ + Byte val = 0x00; + + if (address >= 0x6000 && address < 0x8000) + { + fprintf(stderr, "This Mapper 000 implementation doesnt support FamilyBasic PRG RAM\n"); + exit(-1); + } + else if (address >= 0x8000) + { + Word effectiveAddress = address - 0x8000; + effectiveAddress %= 0x4000 * (mapper->prg_rom_size == 1); + + val = mapper->prg_rom[effectiveAddress]; + } + else + { + fprintf(stderr, "Cartridge read access violation by the CPU at $%04X\n", address); + exit(-1); + } + + return val; +} + +Byte Mapper000_ReadPPU(struct Mapper000* mapper, Word address) +{ + Byte val = 0x00; + + if (address >= 0x2000) + { + fprintf(stderr, "Cartridge read access violation by the PPU at $%04X\n", address); + exit(-1); + } + else + { + val = mapper->chr_rom[address]; + } + + return val; +} + +void Mapper000_WriteCPU(struct Mapper000* mapper, Word address, Byte value) +{ + // nothing +} + + +void Mapper000_WritePPU(struct Mapper000* mapper, Word address, Byte value) +{ + // nothing +} + +void Mapper000_GetPatternTableTexture(struct Mapper000* mapper, SDL_Texture* texture, int index) +{ + int pitch; + void* pixels; + SDL_LockTexture(texture, NULL, &pixels, &pitch); + SDL_memcpy(pixels, mapper->chr_rom + 0x1000 * index, 0x1000); + SDL_UnlockTexture(texture); +} diff --git a/NES Emulator/mappers/mapper000.h b/NES Emulator/mappers/mapper000.h new file mode 100644 index 0000000..88e2367 --- /dev/null +++ b/NES Emulator/mappers/mapper000.h @@ -0,0 +1,27 @@ +#ifndef _MAPPER_000_ +#define _MAPPER_000_ + +#include +#include "../types.h" +#include "SDL.h" + +struct Mapper000 +{ + Byte prg_rom_size; + Byte chr_rom_size; + + Byte* prg_rom; + Byte* chr_rom; +}; + +struct Mapper000* createMapper000(Byte prg_rom_size, Byte chr_rom_size, FILE* fp); +void destroyMapper000(struct Mapper000* mapper); + +Byte Mapper000_ReadCPU(struct Mapper000* mapper, Word address); +Byte Mapper000_ReadPPU(struct Mapper000* mapper, Word address); +void Mapper000_WriteCPU(struct Mapper000* mapper, Word address, Byte value); +void Mapper000_WritePPU(struct Mapper000* mapper, Word address, Byte value); + +void Mapper000_GetPatternTableTexture(struct Mapper000* mapper, SDL_Texture* texture, int index); + +#endif _MAPPER_000_ \ No newline at end of file diff --git a/NES Emulator/ppu.c b/NES Emulator/ppu.c index 0bb8c1a..e719996 100644 --- a/NES Emulator/ppu.c +++ b/NES Emulator/ppu.c @@ -18,24 +18,6 @@ struct PPU* createPPU(struct Bus* parent) ppu->bus = parent; - - - ppu->patternTables[0] = (Byte*)malloc(0x1000 * 2); - if (ppu->patternTables[0] == NULL) - { - fprintf(stderr, "Failed to allocate memory for PPU pattern tables.\n"); - exit(1); - } - memset(ppu->patternTables[0], 0, 0x1000 * 2); - - for (int i = 0; i < 2; i++) - { - ppu->patternTables[i] = ppu->patternTables[0] + ((size_t)0x1000 * i); - ppu->patternTableTextures[i] = SDL_CreateTexture(parent->screen, SDL_PIXELFORMAT_RGB332, SDL_TEXTUREACCESS_STREAMING, 64, 64); - } - - - ppu->nameTables[0] = (Byte*)malloc(0x0400 * 2); if (ppu->nameTables[0] == NULL) { @@ -68,6 +50,13 @@ struct PPU* createPPU(struct Bus* parent) exit(1); } + ppu->pixels = (struct Pixel*)malloc(256 * 240 * sizeof(struct Pixel)); + ppu->screen = SDL_CreateTexture(parent->screen, SDL_PIXELFORMAT_RGB24, SDL_TEXTUREACCESS_STREAMING, 256, 240); + if (ppu->screen == NULL) + { + fprintf(stderr, "Failed to create output screen texture\n"); + exit(1); + } ppu->mirroring = parent->cartridge->header.Flags6.mirror; @@ -83,6 +72,15 @@ struct PPU* createPPU(struct Bus* parent) ppu->ppuAddress.raw = 0x00; ppu->ppuAddressWriteTarget = 1; + ppu->nametablePos.x = 0; + ppu->nametablePos.y = 0; + ppu->tilePosY = 0; + + ppu->verticalPhase = Visible; + ppu->horizontalPhase = Idle; + ppu->fetchingPhase = Nametable; + ppu->remainingCycles = 1; + ppu->x = 0; ppu->y = 0; @@ -98,10 +96,6 @@ void destroyPPU(struct PPU* ppu) SDL_DestroyTexture(ppu->nameTableTextures[i]); free(ppu->nameTables[0]); - for (int i = 0; i < 2; i++) - SDL_DestroyTexture(ppu->patternTableTextures[i]); - free(ppu->patternTables[0]); - free(ppu); } @@ -120,6 +114,9 @@ Byte ppuRead(struct PPU* ppu, Word addr) val = ppu->ppuStatus.raw; break; + case 0x2003: + break; + case 0x2005: break; @@ -130,7 +127,7 @@ Byte ppuRead(struct PPU* ppu, Word addr) { if (ppu->ppuAddress.raw < 0x2000) { - val = ppu->patternTables[0][ppu->ppuAddress.raw]; + val = readCartridgePPU(ppu->bus->cartridge, ppu->ppuAddress.raw); } else if (0x2000 <= ppu->ppuAddress.raw && ppu->ppuAddress.raw < 0x3F00) { @@ -155,8 +152,9 @@ Byte ppuRead(struct PPU* ppu, Word addr) break; default: - fprintf(stderr, "Read access violation at PPU register $%04X", addr); - exit(1); + // fprintf(stderr, "Read access violation at PPU register $%04X", addr); + // exit(1); + break; } return val; @@ -191,7 +189,7 @@ void ppuWrite(struct PPU* ppu, Word addr, Byte val) { if (ppu->ppuAddress.raw < 0x2000) { - ppu->patternTables[0][ppu->ppuAddress.raw] = val; + writeCartridgePPU(ppu->bus->cartridge, ppu->ppuAddress.raw, val); } else if(0x2000 <= ppu->ppuAddress.raw && ppu->ppuAddress.raw < 0x3F00) { @@ -216,41 +214,112 @@ void ppuWrite(struct PPU* ppu, Word addr, Byte val) } default: - fprintf(stderr, "Write access violation at PPU register: $%04X", addr); - exit(1); + //fprintf(stderr, "Write access violation at PPU register: $%04X", addr); + //exit(1); + break; } } int tickPPU(struct PPU* ppu) { // Do stuff - if (ppu->x == 0 && ppu->y == 241) - NMI(ppu->bus->cpu); + if(ppu->ppuStatus.vBlank) + if (ppu->x == 1 && ppu->y == 241) + NMI(ppu->bus->cpu); + + switch (ppu->verticalPhase) + { + case PreRender: + break; + + case Visible: + { + // Fetching + switch (ppu->horizontalPhase) + { + case Idle: + break; + + case Fetching: + ppu->remainingCycles--; + if (ppu->remainingCycles == 0) + { + Byte tileY = ppu->y / 8; + Byte tileX = (ppu->x - 1) / 8 + 2; + + switch (ppu->fetchingPhase) + { + case Nametable: + { + if (ppu->x != 1) + { + ppu->hiPatternFIFO.lo = ppu->tileData.tile.hi; + ppu->loPatternFIFO.lo = ppu->tileData.tile.lo; + } + + ppu->tileData.nametable = ppu->nameTables[0][((size_t)0x2000 + (size_t)0x400 * ppu->ppuCtrl.nametable) + (size_t)0x20* tileY + tileX]; + break; + } + + case Attribute: + { + ppu->tileData.attribute = ppu->nameTables[0][0x3C0 + ((tileY >> 2) * 8) + (tileX >> 2)]; + break; + } + + case PatternLow: + { + ppu->tileData.tile.lo = readCartridgePPU(ppu->bus->cartridge, 0x1000 * ppu->ppuCtrl.bgTile + (ppu->tileData.nametable * 16) + (ppu->y % 8)); + break; + } + + case PatternHigh: + { + ppu->tileData.tile.hi = readCartridgePPU(ppu->bus->cartridge, 0x1000 * ppu->ppuCtrl.bgTile + (ppu->tileData.nametable * 16) + (ppu->y % 8) + 8); + break; + } + } + + ppu->remainingCycles = 1; + ppu->fetchingPhase = (ppu->fetchingPhase + 1) % FetchingPhaseSize; + } + + // Rendering (quick and dirty for now) + Byte color = ((ppu->hiPatternFIFO.hi & 0x80) >> 6) | ((ppu->loPatternFIFO.hi & 0x80) >> 7); + size_t index = (size_t)ppu->y * 256 + ppu->x - 1; + // printf("index %d,%d -> %d\n", ppu->y, ppu->x - 1, index); + ppu->pixels[index].r = 50 * color; + ppu->pixels[index].g = 50 * color; + ppu->pixels[index].b = 50 * color; + + ppu->loPatternFIFO.data <<= 1; + ppu->hiPatternFIFO.data <<= 1; + + break; + } + } break; + } // Increment counters ppu->x++; - + if (ppu->x == 341) { ppu->x = 0; ppu->y++; - if (ppu->y == 261) + if (ppu->y == 262) ppu->y = 0; + + if (ppu->y == 261 || ppu->y == 0 || ppu->y == 240 || ppu->y == 241) + ppu->verticalPhase = (ppu->verticalPhase + 1) % VerticalPhaseSize; } - return (ppu->x == 0 && ppu->y == 0); -} + if (ppu->x == 0 || ppu->x == 1 || ppu->x == 257 || ppu->x == 321 || ppu->x == 337) + ppu->horizontalPhase = (ppu->horizontalPhase + 1) % HorizontalPhaseSize; -SDL_Texture* getPatternTableTexture(struct PPU* ppu, int index) -{ - SDL_Texture* target = ppu->patternTableTextures[index]; - int pitch; - void* pixels; - SDL_LockTexture(target, NULL, &pixels, &pitch); - SDL_memcpy(pixels, ppu->patternTables[index], 0x1000); - SDL_UnlockTexture(target); - return target; + + return (ppu->x == 0 && ppu->y == 0); } SDL_Texture* getNameTableTexture(struct PPU* ppu, int index) @@ -262,4 +331,15 @@ SDL_Texture* getNameTableTexture(struct PPU* ppu, int index) SDL_memcpy(pixels, ppu->nameTables[index], 0x0400); SDL_UnlockTexture(target); return target; -} \ No newline at end of file +} + +SDL_Texture* getScreenTexture(struct PPU* ppu) +{ + void* pixels; + int stride; + SDL_LockTexture(ppu->screen, NULL, &pixels, &stride); + SDL_memcpy(pixels, ppu->pixels, 256 * 240 * sizeof(struct Pixel)); + SDL_UnlockTexture(ppu->screen); + + return ppu->screen; +} diff --git a/NES Emulator/ppu.h b/NES Emulator/ppu.h index 09af5ec..b27623d 100644 --- a/NES Emulator/ppu.h +++ b/NES Emulator/ppu.h @@ -6,9 +6,32 @@ struct Bus; +struct Pixel +{ + Byte r; + Byte g; + Byte b; +}; + +struct FIFO16 +{ + union + { + struct + { + Byte lo; + Byte hi; + }; + + Word data; + }; +}; + struct PPU { - // REGISTERS + //////////////////////////////////////// + /// REGISTERS /// + //////////////////////////////////////// union { struct @@ -59,6 +82,9 @@ struct PPU Byte oamaddr; Byte oamdata; + //////////////////////////////////////// + /// PSEUDO REGISTERS /// + //////////////////////////////////////// Byte scrollX, scrollY; Byte scrollWriteTarget; @@ -76,8 +102,9 @@ struct PPU Byte oamdma; - Byte* patternTables[2]; - SDL_Texture* patternTableTextures[2]; + //////////////////////////////////////// + /// VRAM /// + //////////////////////////////////////// Byte* nameTables[2]; SDL_Texture* nameTableTextures[2]; @@ -85,6 +112,72 @@ struct PPU Byte* paletteIndexes; + + //////////////////////////////////////// + /// PHASE TRACKERS /// + //////////////////////////////////////// + + enum + { + Visible, + PostRender, + VBlank, + PreRender, + VerticalPhaseSize + } verticalPhase; + + enum + { + Idle, + Fetching, + SpriteFetching, + NextLineFetching, + Unknown, + HorizontalPhaseSize + } horizontalPhase; + + Byte remainingCycles; + + enum + { + Nametable, + Attribute, + PatternLow, + PatternHigh, + FetchingPhaseSize + } fetchingPhase; + + //////////////////////////////////////// + /// TILE DATA /// + //////////////////////////////////////// + + struct + { + Byte x; + Byte y; + } nametablePos; + Byte tilePosY; + + struct + { + Byte nametable; + Byte attribute; + + union + { + struct { + Byte lo; + Byte hi; + }; + + Word raw; + + } tile; + } tileData; + + struct FIFO16 loPatternFIFO; + struct FIFO16 hiPatternFIFO; + union { struct @@ -100,6 +193,8 @@ struct PPU Word x, y; + struct Pixel* pixels; + SDL_Texture* screen; struct Bus* bus; }; @@ -111,7 +206,7 @@ void ppuWrite(struct PPU* ppu, Word addr, Byte val); int tickPPU(struct PPU* ppu); -SDL_Texture* getPatternTableTexture(struct PPU* ppu, int index); SDL_Texture* getNameTableTexture(struct PPU* ppu, int index); +SDL_Texture* getScreenTexture(struct PPU* ppu); #endif // _PPU_H_ \ No newline at end of file diff --git a/roms/cpu.nes b/roms/cpu.nes new file mode 100644 index 0000000000000000000000000000000000000000..63cb98ddff38249fb076fee168e17e7329564693 GIT binary patch literal 270352 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 literal 0 HcmV?d00001 diff --git a/roms/donkeykong.nes b/roms/donkeykong.nes new file mode 100644 index 0000000000000000000000000000000000000000..ecd6f0a740de52e799a814dd9f14aef4a811e7d2 GIT binary patch literal 24592 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 literal 0 HcmV?d00001