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 | |