// // Created by william on 12/30/23. // https://www.reddit.com/r/EmuDev/comments/evu3u2/comment/fgr03ms/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button // // 1. Make sure you have NMI implemented on CPU (pretty straightforward) // 2. Implement PPUSTATUS vblank flag (simple) and PPUCTRL NMI flag + background address flag (simple) // 3. Implement PPUADDR/PPUDATA so that the nametables are filled out // 4. Now you have some data your PPU can actually read for rendering background. Render it scanline by scanline - just follow the wiki on this. Maybe the timing will be bad, it doesn't matter for this game. Start off with rendering tiles based on the pattern table ID, don't try and fetch patterns. // 5. Fix the inevitable bugs with your PPUDATA implementation until you see a blocky version of the Donkey Kong screen. // 6. Now fetch pattern table data using the nametable data. If it looks "wrong" make sure you are consuming the background address flag. Start off with black and white, then pick two colors to mix for the two bits. Now you should have something like https://i.imgur.com/7OIpHgd.png // 7. (Optional) implement palette reads (I'm skipping this for now). // 8. Implement OAMDMA (and OAMDATA I guess, I implemented one on top of the other) // 9. Now you should have sprite data to render. Implement the logic for copying from primary OAM to scanline OAM. I'm doing it all as one step (not smearing it over up to 256 cycles like the actual hardware). Skip the confusing sprite overflow junk. // 10. This is where I'm stuck. I think I need to read the "sprites" section of https://wiki.nesdev.com/w/index.php/PPU_rendering very carefully. #include #include #include "ppu.h" #include "../include/ppu.h" #include "../cpu/cpu.h" #include "../include/rom.h" #include "../gui/gui.h" #include "pattern_table.h" #define PPU_VISIBLE_FRAME_END 240 #define PPU_POST_RENDER_LINE_START PPU_VISIBLE_FRAME_END #define PPU_POST_RENDER_LINE_END 242 #define PPU_PRE_RENDER_LINE 261 #define PPU_LINE_END PPU_PRE_RENDER_LINE #define PPU_LINE_WIDTH 340 PPU ppu_state; void ppu_init() { memset(&ppu_state, 0, sizeof(PPU)); } PPU *ppu_get_state() { return &ppu_state; } void ppu_status_set(byte mask, bool enabled) { if (enabled) { ppu_state.registers[PPU_REGISTER_STATUS] |= mask; } else { ppu_state.registers[PPU_REGISTER_STATUS] &= ~mask; } } void ppu_trigger_vbl_nmi() { if (!ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_GEN_VBLANK_NMI)) { // VBlank NMI generation is disabled return; } cpu_trigger_nmi(); } /* d888888b d888888b db d88888b `~~88~~' `88' 88 88' 88 88 88 88ooooo 88 88 88 88~~~~~ 88 .88. 88booo. 88. YP Y888888P Y88888P Y88888P d8888b. d88888b d8b db d8888b. d88888b d8888b. d888888b d8b db d888b 88 `8D 88' 888o 88 88 `8D 88' 88 `8D `88' 888o 88 88' Y8b 88oobY' 88ooooo 88V8o 88 88 88 88ooooo 88oobY' 88 88V8o 88 88 88`8b 88~~~~~ 88 V8o88 88 88 88~~~~~ 88`8b 88 88 V8o88 88 ooo 88 `88. 88. 88 V888 88 .8D 88. 88 `88. .88. 88 V888 88. ~8~ 88 YD Y88888P VP V8P Y8888D' Y88888P 88 YD Y888888P VP V8P Y888P */ static inline unsigned int ppu_pixel_get_index(unsigned int scanline, unsigned int cycle) { return scanline * PPU_VISIBLE_FRAME_END + cycle; } static inline byte ppu_pixel_get_mask(unsigned int cycle) { byte tile_fine_x = (cycle - 1) % 8; return 1 << (PATTERN_TILE_SIZE - tile_fine_x - 1); } static inline void ppu_pixel_set_color(PpuPixel *pixel, byte pt_low, byte pt_high, byte bitmask) { byte p1_byte = pt_low & bitmask; byte p2_byte = pt_high & bitmask; if (p1_byte && p2_byte) { pixel->r = 255; pixel->g = 255; pixel->b = 255; } else if (p2_byte) { pixel->r = 255; pixel->g = 0; pixel->b = 0; } else if (p1_byte) { pixel->r = 0; pixel->g = 255; pixel->b = 255; } else { pixel->r = 0; pixel->g = 0; pixel->b = 0; } } void ppu_draw_tile() { PPUTileFetch tile_fetch = ppu_state.tile_fetch; unsigned int pixel_index = ppu_pixel_get_index(ppu_state.scanline, ppu_state.cycle); PpuPixel *pixel = &ppu_state.pixels[pixel_index]; byte pixel_mask = ppu_pixel_get_mask(ppu_state.cycle); ppu_pixel_set_color(pixel, tile_fetch.pattern_table_tile_low, tile_fetch.pattern_table_tile_high, pixel_mask); } void ppu_visible_frame(unsigned int cycle) { if (cycle == 0) { // Idle... } else if (cycle < 256) { if (ppu_read_flag(PPU_REGISTER_MASK, PPU_MASK_SHOW_BG)) { ppu_draw_tile(); if (cycle <= 248) { address read_addr; byte tile_fetch_cycle = (cycle - 1) % 8; switch (tile_fetch_cycle) { case 1: read_addr = 0x2000 + ((ppu_state.scanline / 8) * 0x20) + (ppu_state.cycle / 8); ppu_state.next_tile_fetch.nametable = ppu_read(read_addr); break; case 3: read_addr = 0x23c0 + (ppu_state.cycle % 8); ppu_state.next_tile_fetch.attribute_table = ppu_read(read_addr); break; case 5: read_addr = 0x1000 * ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_BG_PATTERN_TABLE_ADDR); read_addr += ppu_state.next_tile_fetch.nametable * 16 + ppu_state.scanline % 8; ppu_state.next_tile_fetch.pattern_table_tile_low = ppu_read(read_addr); break; case 7: read_addr = 0x1000 * ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_BG_PATTERN_TABLE_ADDR); read_addr += ppu_state.next_tile_fetch.nametable * 16 + ppu_state.scanline % 8 + 8; ppu_state.next_tile_fetch.pattern_table_tile_high = ppu_read(read_addr); ppu_state.tile_fetch = ppu_state.next_tile_fetch; break; default: break; } } } } else if (cycle <= 320) { // OAMADDR is cleared on sprite loading for pre-render and visible lines ppu_write_reg(PPU_REGISTER_OAM_ADDR, 0); } else if (cycle <= 336) { } } void ppu_pre_render(unsigned int x) { if (x == 1) { // VBlank clear ppu_status_set(PPU_STATUS_VBLANK, false); } if (x >= 257 && x <= 320) { // OAMADDR is cleared on sprite loading for pre-render and visible lines ppu_write_reg(PPU_REGISTER_OAM_ADDR, 0); } } void ppu_post_render(unsigned int x, unsigned int y) { if (x == 1 && y == 241) { // VBlank start ppu_status_set(PPU_STATUS_VBLANK, true); ppu_trigger_vbl_nmi(); } } void ppu_cycle() { if (ppu_state.scanline < PPU_VISIBLE_FRAME_END) { ppu_visible_frame(ppu_state.cycle); } else if (ppu_state.scanline >= PPU_POST_RENDER_LINE_START && ppu_state.scanline <= PPU_POST_RENDER_LINE_END) { ppu_post_render(ppu_state.cycle, ppu_state.scanline); } else if (ppu_state.scanline == PPU_PRE_RENDER_LINE) { ppu_pre_render(ppu_state.cycle); } int frame_width = PPU_LINE_WIDTH; int frame_height = PPU_LINE_END; bool rendering_enabled = ppu_read_flag(PPU_REGISTER_MASK, PPU_MASK_SHOW_BG | PPU_MASK_SHOW_SP); if (rendering_enabled && ppu_state.odd_frame) { // With rendering enabled, the odd frames are shorter // TODO: and doing the last cycle of the last dummy nametable fetch there instead frame_width = PPU_LINE_WIDTH - 2; frame_height = PPU_LINE_END - 1; } ppu_state.cycle++; if (ppu_state.cycle >= frame_width) { ppu_state.cycle = 0; ppu_state.scanline++; } if (ppu_state.scanline >= frame_height) { ppu_state.scanline = 0; ppu_state.frame++; ppu_state.odd_frame = !ppu_state.odd_frame; } } bool ppu_read_flag(size_t reg, byte mask) { return ppu_state.registers[reg] & mask; } byte ppu_read_reg(byte reg) { assert(reg >= 0); assert(reg <= PPU_REGISTER_SIZE); if (reg == PPU_REGISTER_STATUS) { ppu_state.w = false; byte status = ppu_state.registers[PPU_REGISTER_STATUS]; ppu_state.registers[PPU_REGISTER_STATUS] &= ~PPU_STATUS_VBLANK; return status; } if (reg == PPU_REGISTER_DATA) { // Access to VRAM memory is slow, so reading it a first time generally return the memory at the previous address. // So we get the data first, then update the register. byte data = ppu_state.registers[reg]; ppu_state.registers[reg] = ppu_read(ppu_state.ppu_address); if (ppu_state.ppu_address > 0x3eff) { // But the palette data is returned immediately data = ppu_state.registers[reg]; } // We then need to increment the VRAM address byte increment = 1; if (ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_VRAM_ADDR_INCREMENT)) { increment = 32; } ppu_state.ppu_address += increment; ppu_state.ppu_address %= PPU_VRAM_SIZE; return data; } return ppu_state.registers[reg]; } void ppu_write_reg(byte reg, byte data) { ppu_state.registers[reg] = data; if (reg == PPU_REGISTER_CTRL && ppu_read_flag(PPU_REGISTER_STATUS, PPU_STATUS_VBLANK) && !ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_GEN_VBLANK_NMI) && data & PPU_CTRL_GEN_VBLANK_NMI) { // The VBlank flag is still set, and the GEN_VBLANK_NMI was set from 0 to 1 cpu_trigger_nmi(); } else if (reg == PPU_REGISTER_SCROLL) { if (!ppu_state.w) { ppu_state.x = data; } else { ppu_state.t = data; } ppu_state.w = !ppu_state.w; } else if (reg == PPU_REGISTER_ADDR) { address addr = ppu_state.ppu_address; if (!ppu_state.w) { addr = data; } else { addr = data | (addr << 8); } ppu_state.ppu_address = addr % PPU_VRAM_SIZE; ppu_state.w = !ppu_state.w; } else if (reg == PPU_REGISTER_DATA) { address addr = ppu_state.ppu_address; ppu_write(addr, data); byte increment = 1; if (ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_VRAM_ADDR_INCREMENT)) { increment = 32; } addr += increment; ppu_state.ppu_address = addr % PPU_VRAM_SIZE; } else if (reg == PPU_REGISTER_OAM_DATA) { byte oam_addr = ppu_state.registers[PPU_REGISTER_OAM_ADDR]; ppu_write_reg(PPU_REGISTER_OAM_ADDR, oam_addr + 1); } ppu_state.registers[reg] = data; } void ppu_write_reg_oam_addr(byte data) { ppu_state.oam_dma_register = data; } void ppu_write(address addr, byte data) { assert(addr < PPU_VRAM_SIZE); address relative_addr; if (addr < 0x2000) { // TODO Unsupported ? } else if (addr < 0x2400) { relative_addr = addr - 0x2000; ppu_state.memory.nametable_0[relative_addr] = data; } else if (addr < 0x2800) { relative_addr = addr - 0x2400; byte *nametable; if (rom_get()->nametable_mirrored) { nametable = ppu_state.memory.nametable_1; } else { nametable = ppu_state.memory.nametable_0; } nametable[relative_addr] = data; } else if (addr < 0x2c00) { relative_addr = addr - 0x2800; byte *nametable; if (rom_get()->nametable_mirrored) { nametable = ppu_state.memory.nametable_0; } else { nametable = ppu_state.memory.nametable_1; } nametable[relative_addr] = data; } else if (addr < 0x3000) { relative_addr = addr - 0x2c00; ppu_state.memory.nametable_1[relative_addr] = data; } else if (addr >= 0x3f00) { relative_addr = (addr - 0x3f00) % PALETTE_TABLE_SIZE; ppu_state.memory.palette[relative_addr] = data; } } byte ppu_read(address addr) { assert(addr < PPU_VRAM_SIZE); address relative_addr; if (addr < 0x2000) { return *system_get_mapper()->ppu_read(addr); } else if (addr < 0x2400) { relative_addr = addr - 0x2000; return ppu_state.memory.nametable_0[relative_addr]; } else if (addr < 0x2800) { relative_addr = addr - 0x2400; byte *nametable; if (rom_get()->nametable_mirrored) { nametable = ppu_state.memory.nametable_1; } else { nametable = ppu_state.memory.nametable_0; } return nametable[relative_addr]; } else if (addr < 0x2c00) { relative_addr = addr - 0x2800; byte *nametable; if (rom_get()->nametable_mirrored) { nametable = ppu_state.memory.nametable_0; } else { nametable = ppu_state.memory.nametable_1; } return nametable[relative_addr]; } else if (addr < 0x3000) { relative_addr = addr - 0x2c00; return ppu_state.memory.nametable_1[relative_addr]; } else if (addr >= 0x3f00) { relative_addr = (addr - 0x3f00) % PALETTE_TABLE_SIZE; return ppu_state.memory.palette[relative_addr]; } // assert(false); return 0; }