| 1 | #include "cartridge.hpp" |
| 2 | #include "cpu.hpp" |
| 3 | #include "gui.hpp" |
| 4 | #include "ppu.hpp" |
| 5 | |
| 6 | namespace PPU { |
| 7 | #include "palette.inc" |
| 8 | |
| 9 | |
| 10 | Mirroring mirroring; // Mirroring mode. |
| 11 | u8 ciRam[0x800]; // VRAM for nametables. |
| 12 | u8 cgRam[0x20]; // VRAM for palettes. |
| 13 | u8 oamMem[0x100]; // VRAM for sprite properties. |
| 14 | Sprite oam[8], secOam[8]; // Sprite buffers. |
| 15 | u32 pixels[256 * 240]; // Video buffer. |
| 16 | |
| 17 | Addr vAddr, tAddr; // Loopy V, T. |
| 18 | u8 fX; // Fine X. |
| 19 | u8 oamAddr; // OAM address. |
| 20 | |
| 21 | Ctrl ctrl; // PPUCTRL ($2000) register. |
| 22 | Mask mask; // PPUMASK ($2001) register. |
| 23 | Status status; // PPUSTATUS ($2002) register. |
| 24 | |
| 25 | // Background latches: |
| 26 | u8 nt, at, bgL, bgH; |
| 27 | // Background shift registers: |
| 28 | u8 atShiftL, atShiftH; u16 bgShiftL, bgShiftH; |
| 29 | bool atLatchL, atLatchH; |
| 30 | |
| 31 | // Rendering counters: |
| 32 | int scanline, dot; |
| 33 | bool frameOdd; |
| 34 | |
| 35 | inline bool rendering() { return mask.bg || mask.spr; } |
| 36 | inline int spr_height() { return ctrl.sprSz ? 16 : 8; } |
| 37 | |
| 38 | /* Get CIRAM address according to mirroring */ |
| 39 | u16 nt_mirror(u16 addr) |
| 40 | { |
| 41 | switch (mirroring) |
| 42 | { |
| 43 | case VERTICAL: return addr % 0x800; |
| 44 | case HORIZONTAL: return ((addr / 2) & 0x400) + (addr % 0x400); |
| 45 | default: return addr - 0x2000; |
| 46 | } |
| 47 | } |
| 48 | void set_mirroring(Mirroring mode) { mirroring = mode; } |
| 49 | |
| 50 | /* Access PPU memory */ |
| 51 | u8 rd(u16 addr) |
| 52 | { |
| 53 | switch (addr) |
| 54 | { |
| 55 | case 0x0000 ... 0x1FFF: return Cartridge::chr_access<0>(addr); // CHR-ROM/RAM. |
| 56 | case 0x2000 ... 0x3EFF: return ciRam[nt_mirror(addr)]; // Nametables. |
| 57 | case 0x3F00 ... 0x3FFF: // Palettes: |
| 58 | if ((addr & 0x13) == 0x10) addr &= ~0x10; |
| 59 | return cgRam[addr & 0x1F] & (mask.gray ? 0x30 : 0xFF); |
| 60 | default: return 0; |
| 61 | } |
| 62 | } |
| 63 | void wr(u16 addr, u8 v) |
| 64 | { |
| 65 | switch (addr) |
| 66 | { |
| 67 | case 0x0000 ... 0x1FFF: Cartridge::chr_access<1>(addr, v); break; // CHR-ROM/RAM. |
| 68 | case 0x2000 ... 0x3EFF: ciRam[nt_mirror(addr)] = v; break; // Nametables. |
| 69 | case 0x3F00 ... 0x3FFF: // Palettes: |
| 70 | if ((addr & 0x13) == 0x10) addr &= ~0x10; |
| 71 | cgRam[addr & 0x1F] = v; break; |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | /* Access PPU through registers. */ |
| 76 | template <bool write> u8 access(u16 index, u8 v) |
| 77 | { |
| 78 | static u8 res; // Result of the operation. |
| 79 | static u8 buffer; // VRAM read buffer. |
| 80 | static bool latch; // Detect second reading. |
| 81 | |
| 82 | /* Write into register */ |
| 83 | if (write) |
| 84 | { |
| 85 | res = v; |
| 86 | |
| 87 | switch (index) |
| 88 | { |
| 89 | case 0: ctrl.r = v; tAddr.nt = ctrl.nt; break; // PPUCTRL ($2000). |
| 90 | case 1: mask.r = v; break; // PPUMASK ($2001). |
| 91 | case 3: oamAddr = v; break; // OAMADDR ($2003). |
| 92 | case 4: oamMem[oamAddr++] = v; break; // OAMDATA ($2004). |
| 93 | case 5: // PPUSCROLL ($2005). |
| 94 | if (!latch) { fX = v & 7; tAddr.cX = v >> 3; } // First write. |
| 95 | else { tAddr.fY = v & 7; tAddr.cY = v >> 3; } // Second write. |
| 96 | latch = !latch; break; |
| 97 | case 6: // PPUADDR ($2006). |
| 98 | if (!latch) { tAddr.h = v & 0x3F; } // First write. |
| 99 | else { tAddr.l = v; vAddr.r = tAddr.r; } // Second write. |
| 100 | latch = !latch; break; |
| 101 | case 7: wr(vAddr.addr, v); vAddr.addr += ctrl.incr ? 32 : 1; // PPUDATA ($2007). |
| 102 | } |
| 103 | } |
| 104 | /* Read from register */ |
| 105 | else |
| 106 | switch (index) |
| 107 | { |
| 108 | // PPUSTATUS ($2002): |
| 109 | case 2: res = (res & 0x1F) | status.r; status.vBlank = 0; latch = 0; break; |
| 110 | case 4: res = oamMem[oamAddr]; break; // OAMDATA ($2004). |
| 111 | case 7: // PPUDATA ($2007). |
| 112 | if (vAddr.addr <= 0x3EFF) |
| 113 | { |
| 114 | res = buffer; |
| 115 | buffer = rd(vAddr.addr); |
| 116 | } |
| 117 | else |
| 118 | res = buffer = rd(vAddr.addr); |
| 119 | vAddr.addr += ctrl.incr ? 32 : 1; |
| 120 | } |
| 121 | return res; |
| 122 | } |
| 123 | template u8 access<0>(u16, u8); template u8 access<1>(u16, u8); |
| 124 | |
| 125 | /* Calculate graphics addresses */ |
| 126 | inline u16 nt_addr() { return 0x2000 | (vAddr.r & 0xFFF); } |
| 127 | inline u16 at_addr() { return 0x23C0 | (vAddr.nt << 10) | ((vAddr.cY / 4) << 3) | (vAddr.cX / 4); } |
| 128 | inline u16 bg_addr() { return (ctrl.bgTbl * 0x1000) + (nt * 16) + vAddr.fY; } |
| 129 | /* Increment the scroll by one pixel */ |
| 130 | inline void h_scroll() { if (!rendering()) return; if (vAddr.cX == 31) vAddr.r ^= 0x41F; else vAddr.cX++; } |
| 131 | inline void v_scroll() |
| 132 | { |
| 133 | if (!rendering()) return; |
| 134 | if (vAddr.fY < 7) vAddr.fY++; |
| 135 | else |
| 136 | { |
| 137 | vAddr.fY = 0; |
| 138 | if (vAddr.cY == 31) vAddr.cY = 0; |
| 139 | else if (vAddr.cY == 29) { vAddr.cY = 0; vAddr.nt ^= 0b10; } |
| 140 | else vAddr.cY++; |
| 141 | } |
| 142 | } |
| 143 | /* Copy scrolling data from loopy T to loopy V */ |
| 144 | inline void h_update() { if (!rendering()) return; vAddr.r = (vAddr.r & ~0x041F) | (tAddr.r & 0x041F); } |
| 145 | inline void v_update() { if (!rendering()) return; vAddr.r = (vAddr.r & ~0x7BE0) | (tAddr.r & 0x7BE0); } |
| 146 | /* Put new data into the shift registers */ |
| 147 | inline void reload_shift() |
| 148 | { |
| 149 | bgShiftL = (bgShiftL & 0xFF00) | bgL; |
| 150 | bgShiftH = (bgShiftH & 0xFF00) | bgH; |
| 151 | |
| 152 | atLatchL = (at & 1); |
| 153 | atLatchH = (at & 2); |
| 154 | } |
| 155 | |
| 156 | /* Clear secondary OAM */ |
| 157 | void clear_oam() |
| 158 | { |
| 159 | for (int i = 0; i < 8; i++) |
| 160 | { |
| 161 | secOam[i].id = 64; |
| 162 | secOam[i].y = 0xFF; |
| 163 | secOam[i].tile = 0xFF; |
| 164 | secOam[i].attr = 0xFF; |
| 165 | secOam[i].x = 0xFF; |
| 166 | secOam[i].dataL = 0; |
| 167 | secOam[i].dataH = 0; |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /* Fill secondary OAM with the sprite infos for the next scanline */ |
| 172 | void eval_sprites() |
| 173 | { |
| 174 | int n = 0; |
| 175 | for (int i = 0; i < 64; i++) |
| 176 | { |
| 177 | int line = (scanline == 261 ? -1 : scanline) - oamMem[i*4 + 0]; |
| 178 | // If the sprite is in the scanline, copy its properties into secondary OAM: |
| 179 | if (line >= 0 and line < spr_height()) |
| 180 | { |
| 181 | secOam[n].id = i; |
| 182 | secOam[n].y = oamMem[i*4 + 0]; |
| 183 | secOam[n].tile = oamMem[i*4 + 1]; |
| 184 | secOam[n].attr = oamMem[i*4 + 2]; |
| 185 | secOam[n].x = oamMem[i*4 + 3]; |
| 186 | |
| 187 | if (++n >= 8) |
| 188 | { |
| 189 | status.sprOvf = true; |
| 190 | break; |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | /* Load the sprite info into primary OAM and fetch their tile data. */ |
| 197 | void load_sprites() |
| 198 | { |
| 199 | u16 addr; |
| 200 | for (int i = 0; i < 8; i++) |
| 201 | { |
| 202 | oam[i] = secOam[i]; // Copy secondary OAM into primary. |
| 203 | |
| 204 | // Different address modes depending on the sprite height: |
| 205 | if (spr_height() == 16) |
| 206 | addr = ((oam[i].tile & 1) * 0x1000) + ((oam[i].tile & ~1) * 16); |
| 207 | else |
| 208 | addr = ( ctrl.sprTbl * 0x1000) + ( oam[i].tile * 16); |
| 209 | |
| 210 | unsigned sprY = (scanline - oam[i].y) % spr_height(); // Line inside the sprite. |
| 211 | if (oam[i].attr & 0x80) sprY ^= spr_height() - 1; // Vertical flip. |
| 212 | addr += sprY + (sprY & 8); // Select the second tile if on 8x16. |
| 213 | |
| 214 | oam[i].dataL = rd(addr + 0); |
| 215 | oam[i].dataH = rd(addr + 8); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | /* Process a pixel, draw it if it's on screen */ |
| 220 | void pixel() |
| 221 | { |
| 222 | u8 palette = 0, objPalette = 0; |
| 223 | bool objPriority = 0; |
| 224 | int x = dot - 2; |
| 225 | |
| 226 | if (scanline < 240 and x >= 0 and x < 256) |
| 227 | { |
| 228 | if (mask.bg and not (!mask.bgLeft && x < 8)) |
| 229 | { |
| 230 | // Background: |
| 231 | palette = (NTH_BIT(bgShiftH, 15 - fX) << 1) | |
| 232 | NTH_BIT(bgShiftL, 15 - fX); |
| 233 | if (palette) |
| 234 | palette |= ((NTH_BIT(atShiftH, 7 - fX) << 1) | |
| 235 | NTH_BIT(atShiftL, 7 - fX)) << 2; |
| 236 | } |
| 237 | // Sprites: |
| 238 | if (mask.spr and not (!mask.sprLeft && x < 8)) |
| 239 | for (int i = 7; i >= 0; i--) |
| 240 | { |
| 241 | if (oam[i].id == 64) continue; // Void entry. |
| 242 | unsigned sprX = x - oam[i].x; |
| 243 | if (sprX >= 8) continue; // Not in range. |
| 244 | if (oam[i].attr & 0x40) sprX ^= 7; // Horizontal flip. |
| 245 | |
| 246 | u8 sprPalette = (NTH_BIT(oam[i].dataH, 7 - sprX) << 1) | |
| 247 | NTH_BIT(oam[i].dataL, 7 - sprX); |
| 248 | if (sprPalette == 0) continue; // Transparent pixel. |
| 249 | |
| 250 | if (oam[i].id == 0 && palette && x != 255) status.sprHit = true; |
| 251 | sprPalette |= (oam[i].attr & 3) << 2; |
| 252 | objPalette = sprPalette + 16; |
| 253 | objPriority = oam[i].attr & 0x20; |
| 254 | } |
| 255 | // Evaluate priority: |
| 256 | if (objPalette && (palette == 0 || objPriority == 0)) palette = objPalette; |
| 257 | |
| 258 | pixels[scanline*256 + x] = nesRgb[rd(0x3F00 + (rendering() ? palette : 0))]; |
| 259 | } |
| 260 | // Perform background shifts: |
| 261 | bgShiftL <<= 1; bgShiftH <<= 1; |
| 262 | atShiftL = (atShiftL << 1) | atLatchL; |
| 263 | atShiftH = (atShiftH << 1) | atLatchH; |
| 264 | } |
| 265 | |
| 266 | /* Execute a cycle of a scanline */ |
| 267 | template<Scanline s> void scanline_cycle() |
| 268 | { |
| 269 | static u16 addr; |
| 270 | |
| 271 | if (s == NMI and dot == 1) { status.vBlank = true; if (ctrl.nmi) CPU::set_nmi(); } |
| 272 | else if (s == POST and dot == 0) GUI::new_frame(pixels); |
| 273 | else if (s == VISIBLE or s == PRE) |
| 274 | { |
| 275 | // Sprites: |
| 276 | switch (dot) |
| 277 | { |
| 278 | case 1: clear_oam(); if (s == PRE) { status.sprOvf = status.sprHit = false; } break; |
| 279 | case 257: eval_sprites(); break; |
| 280 | case 321: load_sprites(); break; |
| 281 | } |
| 282 | // Background: |
| 283 | switch (dot) |
| 284 | { |
| 285 | case 2 ... 255: case 322 ... 337: |
| 286 | pixel(); |
| 287 | switch (dot % 8) |
| 288 | { |
| 289 | // Nametable: |
| 290 | case 1: addr = nt_addr(); reload_shift(); break; |
| 291 | case 2: nt = rd(addr); break; |
| 292 | // Attribute: |
| 293 | case 3: addr = at_addr(); break; |
| 294 | case 4: at = rd(addr); if (vAddr.cY & 2) at >>= 4; |
| 295 | if (vAddr.cX & 2) at >>= 2; break; |
| 296 | // Background (low bits): |
| 297 | case 5: addr = bg_addr(); break; |
| 298 | case 6: bgL = rd(addr); break; |
| 299 | // Background (high bits): |
| 300 | case 7: addr += 8; break; |
| 301 | case 0: bgH = rd(addr); h_scroll(); break; |
| 302 | } break; |
| 303 | case 256: pixel(); bgH = rd(addr); v_scroll(); break; // Vertical bump. |
| 304 | case 257: pixel(); reload_shift(); h_update(); break; // Update horizontal position. |
| 305 | case 280 ... 304: if (s == PRE) v_update(); break; // Update vertical position. |
| 306 | |
| 307 | // No shift reloading: |
| 308 | case 1: addr = nt_addr(); if (s == PRE) status.vBlank = false; break; |
| 309 | case 321: case 339: addr = nt_addr(); break; |
| 310 | // Nametable fetch instead of attribute: |
| 311 | case 338: nt = rd(addr); break; |
| 312 | case 340: nt = rd(addr); if (s == PRE && rendering() && frameOdd) dot++; |
| 313 | } |
| 314 | // Signal scanline to mapper: |
| 315 | if (dot == 260 && rendering()) Cartridge::signal_scanline(); |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | /* Execute a PPU cycle. */ |
| 320 | void step() |
| 321 | { |
| 322 | switch (scanline) |
| 323 | { |
| 324 | case 0 ... 239: scanline_cycle<VISIBLE>(); break; |
| 325 | case 240: scanline_cycle<POST>(); break; |
| 326 | case 241: scanline_cycle<NMI>(); break; |
| 327 | case 261: scanline_cycle<PRE>(); break; |
| 328 | } |
| 329 | // Update dot and scanline counters: |
| 330 | if (++dot > 340) |
| 331 | { |
| 332 | dot %= 341; |
| 333 | if (++scanline > 261) |
| 334 | { |
| 335 | scanline = 0; |
| 336 | frameOdd ^= 1; |
| 337 | } |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | void reset() |
| 342 | { |
| 343 | frameOdd = false; |
| 344 | scanline = dot = 0; |
| 345 | ctrl.r = mask.r = status.r = 0; |
| 346 | |
| 347 | memset(pixels, 0x00, sizeof(pixels)); |
| 348 | memset(ciRam, 0xFF, sizeof(ciRam)); |
| 349 | memset(oamMem, 0x00, sizeof(oamMem)); |
| 350 | } |
| 351 | |
| 352 | |
| 353 | } |
| 354 | |