summaryrefslogtreecommitdiff
path: root/ppu.c
diff options
context:
space:
mode:
authorPeter Fors <peter.fors@mindkiller.com>2025-03-29 19:57:00 +0100
committerPeter Fors <peter.fors@mindkiller.com>2025-03-29 19:57:00 +0100
commite8ff6bf2ab9982c5e5ab8d8f4e7adcc5207d079d (patch)
treea91ec214f5cd0e95c3d26ee0ba916cb187e3aede /ppu.c
first mknes commit
Diffstat (limited to 'ppu.c')
-rw-r--r--ppu.c490
1 files changed, 490 insertions, 0 deletions
diff --git a/ppu.c b/ppu.c
new file mode 100644
index 0000000..adfdc13
--- /dev/null
+++ b/ppu.c
@@ -0,0 +1,490 @@
+
+static uint8_t memory_read_dma(struct nes_state *state, uint32_t offset);
+
+static void ppu_reset_scroll_latch(struct nes_state *state) {
+ state->ppu.write_toggle = 0;
+}
+
+static uint32_t compute_nt_address(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+ return 0x2000 | ppu->nt_base_y | ppu->nt_base_x | ppu->coarse_y_offs | ppu->coarse_x_offs;
+}
+
+
+static uint32_t compute_at_address(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+ uint32_t nt_offset = ppu->nt_base_y | ppu->nt_base_x;
+ uint32_t attr_x = (ppu->coarse_x_offs >> 2) & 0x07;
+ uint32_t attr_y = (ppu->coarse_y_offs >> 2) & 0x38;
+ return 0x23c0 | nt_offset | attr_y | attr_x;
+}
+
+static void increment_x(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+
+ if(ppu->coarse_x_offs == 31 << 0) {
+ ppu->coarse_x_offs = 0;
+ ppu->nt_base_x ^= 0x400;
+ } else {
+ ppu->coarse_x_offs += 1 << 0;
+ }
+}
+
+static void increment_y(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+
+ if(ppu->fine_y < 7) {
+ ppu->fine_y++;
+ return;
+ }
+
+ ppu->fine_y = 0;
+
+ uint32_t coarse_y = ppu->coarse_y_offs >> 5;
+
+ if(coarse_y == 29) {
+ ppu->coarse_y_offs = 0;
+ ppu->nt_base_y ^= 0x800;
+ } else if(coarse_y == 31) {
+ ppu->coarse_y_offs = 0;
+ // Do not toggle nt_base_y — stays the same
+ } else {
+ ppu->coarse_y_offs += 1 << 5;
+ }
+}
+
+
+static void ppu_vram_write(struct nes_state *state, uint32_t addr, uint8_t value) {
+ // PPU address bus is 14 bits, wraps every 0x4000
+ addr &= 0x3fff;
+
+ if(addr < 0x2000) {
+ // Pattern table (CHR ROM is read-only, ignore write)
+ return;
+ }
+
+ if(addr < 0x3f00) {
+ // Nametable RAM (CIRAM), mirrored every 0x1000
+ uint32_t nt_index = addr & 0x0fff;
+ state->ciram[nt_index] = value;
+ return;
+ }
+
+ if(addr < 0x4000) {
+ // Palette RAM (0x3F00–0x3FFF), mirrored every 32 bytes
+ addr = 0x3f00 + (addr & 0x1f);
+
+ // Palette mirroring: $3F10/$3F14/$3F18/$3F1C mirror $3F00/$3F04/$3F08/$3F0C
+ if((addr & 0x13) == 0x10) {
+ addr &= ~0x10;
+ }
+
+ state->palette[addr & 0x1f] = value;
+ }
+}
+
+static uint8_t ppu_vram_read(struct nes_state *state, uint32_t addr) {
+ // PPU address bus is 14 bits, wraps every 0x4000
+ addr &= 0x3fff;
+
+ if(addr < 0x2000) {
+ // Pattern table (CHR ROM)
+ return state->chrrom[addr];
+ }
+
+ if(addr < 0x3f00) {
+ // Nametables (0x2000–0x2FFF mirrored every 0x1000)
+ uint32_t nt_index = addr & 0x0fff;
+ return state->ciram[nt_index];
+ }
+
+ if(addr < 0x4000) {
+ // Palette RAM (0x3F00–0x3FFF, mirrored every 32 bytes)
+ addr = 0x3f00 + (addr & 0x1f);
+
+ // Palette mirroring: $3F10/$3F14/$3F18/$3F1C mirror $3F00/$3F04/$3F08/$3F0C
+ if((addr & 0x13) == 0x10) {
+ addr &= ~0x10;
+ }
+
+ return state->palette[addr & 0x1f];
+ }
+
+ return 0;
+}
+
+static uint8_t ppu_read_2002(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+ uint8_t result = ppu->status;
+
+ ppu->status &= ~PPU_STATUS_VBLANK;
+ ppu->write_toggle = 0;
+
+ return result;
+}
+
+static void ppu_write_2003(struct nes_state *state, uint8_t value) {
+ state->ppu.oam_addr = value;
+}
+
+static uint8_t ppu_read_2004(struct nes_state *state) {
+ return state->ppu.oam[state->ppu.oam_addr];
+}
+
+static void ppu_write_2004(struct nes_state *state, uint8_t value) {
+ state->ppu.oam[state->ppu.oam_addr] = value;
+ state->ppu.oam_addr++;
+}
+
+
+static void ppu_write_2005(struct nes_state *state, uint8_t value)
+{
+ struct ppu_state *ppu = &state->ppu;
+
+ if(ppu->write_toggle == 0) {
+ ppu->temp_fine_x = value & 7;
+ ppu->temp_coarse_x_offs = (value >> 3) & 0x1f;
+ ppu->temp_coarse_x_offs <<= 0;
+
+ ppu->write_toggle = 1;
+ } else {
+ ppu->temp_fine_y = value & 7;
+ uint8_t coarse_y = (value >> 3) & 0x1f;
+ ppu->temp_coarse_y_offs = coarse_y << 5;
+
+ if(coarse_y >= 30) {
+ // vertical nametable select
+ ppu->temp_nt_base_y = 0x800;
+ } else {
+ ppu->temp_nt_base_y = 0x000;
+ }
+
+ ppu->write_toggle = 0;
+ }
+}
+
+static void ppu_write_2006(struct nes_state *state, uint8_t value)
+{
+ struct ppu_state *ppu = &state->ppu;
+
+ if(ppu->write_toggle == 0) {
+ // High byte: bits 14–8 of VRAM address
+ ppu->temp_nt_base_y = (value & 0x0c) << 8;
+ ppu->temp_nt_base_x = (value & 0x01) << 10;
+ ppu->temp_coarse_y_offs = (value & 0x03) << 3;
+ ppu->temp_coarse_y_offs <<= 5;
+
+ ppu->write_toggle = 1;
+ } else {
+ // Low byte: bits 7–0 of VRAM address
+ ppu->temp_coarse_x_offs = (value & 0x1f) << 0;
+ ppu->temp_fine_y = (value >> 2) & 7;
+
+ // On second write, transfer temp -> current scroll state
+ ppu->coarse_x_offs = ppu->temp_coarse_x_offs;
+ ppu->coarse_y_offs = ppu->temp_coarse_y_offs;
+ ppu->fine_x = ppu->temp_fine_x;
+ ppu->fine_y = ppu->temp_fine_y;
+ ppu->nt_base_x = ppu->temp_nt_base_x;
+ ppu->nt_base_y = ppu->temp_nt_base_y;
+
+ ppu->write_toggle = 0;
+ }
+}
+
+static uint8_t ppu_read_2007(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+
+ uint32_t addr = compute_nt_address(state);
+ if(ppu->fine_y) {
+ addr += ppu->fine_y;
+ }
+
+ uint8_t result;
+
+ if(addr < 0x3f00) {
+ // Return buffered value, load new one
+ result = ppu->read_buffer;
+ ppu->read_buffer = ppu_vram_read(state, addr);
+ } else {
+ // Palette reads bypass buffer
+ result = ppu_vram_read(state, addr);
+ ppu->read_buffer = ppu_vram_read(state, addr & 0x2fff); // mirror to pattern/nametable area
+ }
+
+ // Auto-increment address
+ if(ppu->ctrl & PPU_CTRL_INCREMENT) {
+ increment_y(state);
+ } else {
+ increment_x(state);
+ }
+
+ return result;
+}
+
+
+static void ppu_write_2007(struct nes_state *state, uint8_t value) {
+ struct ppu_state *ppu = &state->ppu;
+ uint32_t addr = compute_nt_address(state);
+
+ if(ppu->fine_y)
+ addr += ppu->fine_y;
+
+ ppu_vram_write(state, addr, value);
+
+ if(ppu->ctrl & PPU_CTRL_INCREMENT) {
+ increment_y(state);
+ } else {
+ increment_x(state);
+ }
+}
+
+static uint8_t ppu_pattern_read(struct nes_state *state, uint8_t tile_index, uint8_t fine_y, uint8_t plane) {
+ struct ppu_state *ppu = &state->ppu;
+ uint32_t base = (ppu->ctrl & PPU_CTRL_BG_TABLE) ? 0x1000 : 0x0000;
+ uint32_t addr = base + (tile_index << 4) + fine_y;
+
+ if(plane) {
+ addr += 8;
+ }
+
+ return ppu_vram_read(state, addr);
+}
+
+static void ppu_eval_sprites(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+
+ uint8_t scanline = ppu->scanline;
+ uint8_t height = (ppu->ctrl & PPU_CTRL_SPRITE_HEIGHT) ? 16 : 8;
+
+ uint8_t count = 0;
+
+ for(uint8_t i = 0; i < 64; i++) {
+ uint8_t *sprite = &ppu->oam[i * 4];
+ uint8_t sprite_y = sprite[0];
+
+ if(scanline >= sprite_y && scanline < sprite_y + height) {
+ if(count < 8) {
+ uint8_t *dest = &ppu->sec_oam[count * 4];
+ dest[0] = sprite[0];
+ dest[1] = sprite[1];
+ dest[2] = sprite[2];
+ dest[3] = sprite[3];
+ count++;
+ } else {
+ ppu->status |= PPU_STATUS_OVERFLOW;
+ break;
+ }
+ }
+ }
+}
+
+static void ppu_clear_sec_oam(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+ for(int i=0; i<32; i++) {
+ ppu->sec_oam[i] = 0xff;
+ }
+}
+
+static void ppu_render_sprites(struct nes_state *state)
+{
+ struct ppu_state *ppu = &state->ppu;
+
+ // Clear sprite pixel buffer
+ for(int x=0;x<256;x++) {
+ ppu->sprite_pixels[x] = 0;
+ ppu->sprite_zero_flags[x] = 0;
+ }
+
+ for(int i=0;i<8;i++) {
+ uint8_t *sprite = &ppu->sec_oam[i * 4];
+ uint8_t y = sprite[0];
+ uint8_t tile = sprite[1];
+ uint8_t attr = sprite[2];
+ uint8_t x = sprite[3];
+
+ uint8_t scanline = ppu->scanline;
+ uint8_t height = (ppu->ctrl & PPU_CTRL_SPRITE_HEIGHT) ? 16 : 8;
+ int sprite_y = scanline - y;
+
+ if(attr & 0x80) {
+ // Vertical flip
+ sprite_y = height - 1 - sprite_y;
+ }
+
+ uint32_t base = (ppu->ctrl & PPU_CTRL_SPRITE_TABLE) ? 0x1000 : 0x0000;
+ uint32_t addr = base + tile * 16 + sprite_y;
+
+ uint8_t lo = ppu_vram_read(state, addr);
+ uint8_t hi = ppu_vram_read(state, addr + 8);
+
+ for(int j=0;j<8;j++) {
+ int sx = x + (attr & 0x40 ? j : (7 - j)); // horizontal flip
+ if(sx >= 256)
+ continue;
+
+ uint8_t bit = 1 << j;
+ uint8_t p0 = (lo & (1 << (7 - j))) ? 1 : 0;
+ uint8_t p1 = (hi & (1 << (7 - j))) ? 1 : 0;
+ uint8_t color = (attr & 0x03) << 2 | (p1 << 1) | p0;
+
+ if((p0 | p1) == 0)
+ continue;
+
+ if(ppu->sprite_pixels[sx] == 0) {
+ ppu->sprite_pixels[sx] = color | 0x10;
+ if(i == 0)
+ ppu->sprite_zero_flags[sx] = 1;
+ }
+ }
+ }
+}
+
+
+static void ppu_tick(struct nes_state *state) {
+ struct ppu_state *ppu = &state->ppu;
+
+ if(ppu->scanline == 241 && ppu->dot == 1) {
+ ppu->status |= PPU_STATUS_VBLANK;
+ ppu->nmi_occurred = 1;
+ }
+
+ if(ppu->scanline == 261 && ppu->dot == 1) {
+ ppu->status &= ~PPU_STATUS_VBLANK;
+ ppu->status &= ~PPU_STATUS_SPRITE0_HIT;
+ ppu->status &= ~PPU_STATUS_OVERFLOW;
+ ppu->nmi_occurred = 0;
+ }
+
+ if((ppu->scanline < 240 || ppu->scanline == 261) && (ppu->dot >= 1 && ppu->dot <= 256)) {
+ uint32_t cycle = (ppu->dot - 1) % 8;
+
+ switch(cycle) {
+ case 0: {
+ ppu->bg_tile_lsb = (ppu->bg_tile_lsb & 0xffff0000) | ppu->tile_lsb;
+ ppu->bg_tile_msb = (ppu->bg_tile_msb & 0xffff0000) | ppu->tile_msb;
+ ppu->bg_attr_lsb = (ppu->attr_byte & 1) ? 0xff : 0x00;
+ ppu->bg_attr_msb = (ppu->attr_byte & 2) ? 0xff : 0x00;
+ } break;
+
+ case 1: {
+ ppu->nt_byte = ppu_vram_read(state, compute_nt_address(state));
+ } break;
+
+ case 3: {
+ ppu->attr_byte = ppu_vram_read(state, compute_at_address(state));
+ } break;
+
+ case 5: {
+ ppu->tile_lsb = ppu_pattern_read(state, ppu->nt_byte, ppu->fine_y, 0);
+ } break;
+
+ case 7: {
+ ppu->tile_msb = ppu_pattern_read(state, ppu->nt_byte, ppu->fine_y, 1);
+ } break;
+ }
+
+ ppu->bg_tile_lsb <<= 1;
+ ppu->bg_tile_msb <<= 1;
+ ppu->bg_attr_lsb <<= 1;
+ ppu->bg_attr_msb <<= 1;
+
+ if(ppu->scanline < 240) {
+ uint8_t p0 = (ppu->bg_tile_lsb & 0x8000) ? 1 : 0;
+ uint8_t p1 = (ppu->bg_tile_msb & 0x8000) ? 1 : 0;
+ uint8_t a0 = (ppu->bg_attr_lsb & 0x80) ? 1 : 0;
+ uint8_t a1 = (ppu->bg_attr_msb & 0x80) ? 1 : 0;
+ uint8_t color = (a1 << 3) | (a0 << 2) | (p1 << 1) | p0;
+
+ uint32_t index = ppu->scanline * 256 + (ppu->dot - 1);
+
+ uint8_t bg_color = color;
+ uint8_t sprite_color = ppu->sprite_pixels[ppu->dot - 1];
+
+ uint8_t final_color = bg_color;
+
+ if(sprite_color && ((bg_color & 3) == 0 || !(ppu->mask & 0x04))) {
+ // Sprite above BG, or BG is transparent
+ final_color = sprite_color;
+ }
+
+ if(ppu->sprite_zero_flags[ppu->dot - 1] &&
+ (bg_color & 3) && (sprite_color & 0x1f) &&
+ (ppu->mask & 0x18)) {
+ ppu->status |= PPU_STATUS_SPRITE0_HIT;
+ }
+
+ ppu->pixels[index] = final_color;
+
+ }
+ }
+
+ if(ppu->scanline < 240 && ppu->dot == 257) {
+ ppu_render_sprites(state);
+ }
+
+ if(ppu->scanline < 240 || ppu->scanline == 261) {
+ if(ppu->dot == 256) {
+ increment_x(state);
+ increment_y(state);
+ } else if(ppu->dot == 328) {
+ increment_x(state);
+ }
+ }
+
+ ppu->dot++;
+
+ if(ppu->scanline == 261 && ppu->dot == 339 && ppu->frame_even) {
+ if(ppu->mask & (PPU_MASK_SHOW_BG | PPU_MASK_SHOW_SPRITES)) {
+ ppu->dot++;
+ }
+ }
+
+ if(ppu->dot > 340) {
+ ppu->dot = 0;
+ ppu->scanline++;
+
+ if(ppu->scanline > 261) {
+ ppu->scanline = 0;
+ ppu->frame_even ^= 1;
+ }
+ }
+
+ // Clear secondary OAM at start of scanline
+ if(ppu->scanline < 240 || ppu->scanline == 261) {
+ if(ppu->dot == 1) {
+ ppu_clear_sec_oam(state);
+ }
+ }
+
+ // Perform sprite evaluation
+ if(ppu->scanline < 240 && ppu->dot == 65) {
+ ppu_eval_sprites(state);
+ }
+}
+
+
+static void ppu_dma_4014(struct nes_state *state, uint8_t page) {
+ uint32_t base = page << 8;
+
+ for(int i = 0; i < 256; i++) {
+ uint8_t value = memory_read_dma(state, base + i);
+ state->ppu.oam[i] = value;
+
+ ppu_tick(state);
+ ppu_tick(state);
+ ppu_tick(state);
+ // apu_tick(state);
+ state->cycle++;
+ }
+
+ // DMA takes 513 cycles on even CPU cycles, 514 on odd
+ int extra = (state->cycle & 1) ? 514 : 513;
+ for(int i = 0; i < extra; i++) {
+ ppu_tick(state);
+ ppu_tick(state);
+ ppu_tick(state);
+ // apu_tick(state);
+ state->cycle++;
+ }
+}