This repository has been archived on 2022-06-22. You can view files and clone it, but cannot push or open issues or pull requests.
LaiNES/src/ppu.cpp
2020-05-18 14:42:11 -05:00

357 lines
12 KiB
C++

#include "cartridge.hpp"
#include "cpu.hpp"
#include "gui.hpp"
#include "ppu.hpp"
namespace PPU {
#include "palette.inc"
Mirroring mirroring; // Mirroring mode.
u8 ciRam[0x800]; // VRAM for nametables.
u8 cgRam[0x20]; // VRAM for palettes.
u8 oamMem[0x100]; // VRAM for sprite properties.
Sprite oam[8], secOam[8]; // Sprite buffers.
u32 pixels[256 * 240]; // Video buffer.
Addr vAddr, tAddr; // Loopy V, T.
u8 fX; // Fine X.
u8 oamAddr; // OAM address.
Ctrl ctrl; // PPUCTRL ($2000) register.
Mask mask; // PPUMASK ($2001) register.
Status status; // PPUSTATUS ($2002) register.
// Background latches:
u8 nt, at, bgL, bgH;
// Background shift registers:
u8 atShiftL, atShiftH; u16 bgShiftL, bgShiftH;
bool atLatchL, atLatchH;
// Rendering counters:
int scanline, dot;
bool frameOdd;
inline bool rendering() { return mask.bg || mask.spr; }
inline int spr_height() { return ctrl.sprSz ? 16 : 8; }
/* Get CIRAM address according to mirroring */
u16 nt_mirror(u16 addr)
{
switch (mirroring)
{
case VERTICAL: return addr % 0x800;
case HORIZONTAL: return ((addr / 2) & 0x400) + (addr % 0x400);
case ONE_SCREEN_LO:
case ONE_SCREEN_HI:
return ((addr & 0x3ff) + ((mirroring == ONE_SCREEN_HI) ? 0x400 : 0x0)) - 0x2000;
default: return addr - 0x2000;
}
}
void set_mirroring(Mirroring mode) { mirroring = mode; }
/* Access PPU memory */
u8 rd(u16 addr)
{
switch (addr)
{
case 0x0000 ... 0x1FFF: return Cartridge::chr_access<0>(addr); // CHR-ROM/RAM.
case 0x2000 ... 0x3EFF: return ciRam[nt_mirror(addr)]; // Nametables.
case 0x3F00 ... 0x3FFF: // Palettes:
if ((addr & 0x13) == 0x10) addr &= ~0x10;
return cgRam[addr & 0x1F] & (mask.gray ? 0x30 : 0xFF);
default: return 0;
}
}
void wr(u16 addr, u8 v)
{
switch (addr)
{
case 0x0000 ... 0x1FFF: Cartridge::chr_access<1>(addr, v); break; // CHR-ROM/RAM.
case 0x2000 ... 0x3EFF: ciRam[nt_mirror(addr)] = v; break; // Nametables.
case 0x3F00 ... 0x3FFF: // Palettes:
if ((addr & 0x13) == 0x10) addr &= ~0x10;
cgRam[addr & 0x1F] = v; break;
}
}
/* Access PPU through registers. */
template <bool write> u8 access(u16 index, u8 v)
{
static u8 res; // Result of the operation.
static u8 buffer; // VRAM read buffer.
static bool latch; // Detect second reading.
/* Write into register */
if (write)
{
res = v;
switch (index)
{
case 0: ctrl.r = v; tAddr.nt = ctrl.nt; break; // PPUCTRL ($2000).
case 1: mask.r = v; break; // PPUMASK ($2001).
case 3: oamAddr = v; break; // OAMADDR ($2003).
case 4: oamMem[oamAddr++] = v; break; // OAMDATA ($2004).
case 5: // PPUSCROLL ($2005).
if (!latch) { fX = v & 7; tAddr.cX = v >> 3; } // First write.
else { tAddr.fY = v & 7; tAddr.cY = v >> 3; } // Second write.
latch = !latch; break;
case 6: // PPUADDR ($2006).
if (!latch) { tAddr.h = v & 0x3F; } // First write.
else { tAddr.l = v; vAddr.r = tAddr.r; } // Second write.
latch = !latch; break;
case 7: wr(vAddr.addr, v); vAddr.addr += ctrl.incr ? 32 : 1; // PPUDATA ($2007).
}
}
/* Read from register */
else
switch (index)
{
// PPUSTATUS ($2002):
case 2: res = (res & 0x1F) | status.r; status.vBlank = 0; latch = 0; break;
case 4: res = oamMem[oamAddr]; break; // OAMDATA ($2004).
case 7: // PPUDATA ($2007).
if (vAddr.addr <= 0x3EFF)
{
res = buffer;
buffer = rd(vAddr.addr);
}
else
res = buffer = rd(vAddr.addr);
vAddr.addr += ctrl.incr ? 32 : 1;
}
return res;
}
template u8 access<0>(u16, u8); template u8 access<1>(u16, u8);
/* Calculate graphics addresses */
inline u16 nt_addr() { return 0x2000 | (vAddr.r & 0xFFF); }
inline u16 at_addr() { return 0x23C0 | (vAddr.nt << 10) | ((vAddr.cY / 4) << 3) | (vAddr.cX / 4); }
inline u16 bg_addr() { return (ctrl.bgTbl * 0x1000) + (nt * 16) + vAddr.fY; }
/* Increment the scroll by one pixel */
inline void h_scroll() { if (!rendering()) return; if (vAddr.cX == 31) vAddr.r ^= 0x41F; else vAddr.cX++; }
inline void v_scroll()
{
if (!rendering()) return;
if (vAddr.fY < 7) vAddr.fY++;
else
{
vAddr.fY = 0;
if (vAddr.cY == 31) vAddr.cY = 0;
else if (vAddr.cY == 29) { vAddr.cY = 0; vAddr.nt ^= 0b10; }
else vAddr.cY++;
}
}
/* Copy scrolling data from loopy T to loopy V */
inline void h_update() { if (!rendering()) return; vAddr.r = (vAddr.r & ~0x041F) | (tAddr.r & 0x041F); }
inline void v_update() { if (!rendering()) return; vAddr.r = (vAddr.r & ~0x7BE0) | (tAddr.r & 0x7BE0); }
/* Put new data into the shift registers */
inline void reload_shift()
{
bgShiftL = (bgShiftL & 0xFF00) | bgL;
bgShiftH = (bgShiftH & 0xFF00) | bgH;
atLatchL = (at & 1);
atLatchH = (at & 2);
}
/* Clear secondary OAM */
void clear_oam()
{
for (int i = 0; i < 8; i++)
{
secOam[i].id = 64;
secOam[i].y = 0xFF;
secOam[i].tile = 0xFF;
secOam[i].attr = 0xFF;
secOam[i].x = 0xFF;
secOam[i].dataL = 0;
secOam[i].dataH = 0;
}
}
/* Fill secondary OAM with the sprite infos for the next scanline */
void eval_sprites()
{
int n = 0;
for (int i = 0; i < 64; i++)
{
int line = (scanline == 261 ? -1 : scanline) - oamMem[i*4 + 0];
// If the sprite is in the scanline, copy its properties into secondary OAM:
if (line >= 0 and line < spr_height())
{
secOam[n].id = i;
secOam[n].y = oamMem[i*4 + 0];
secOam[n].tile = oamMem[i*4 + 1];
secOam[n].attr = oamMem[i*4 + 2];
secOam[n].x = oamMem[i*4 + 3];
if (++n >= 8)
{
status.sprOvf = true;
break;
}
}
}
}
/* Load the sprite info into primary OAM and fetch their tile data. */
void load_sprites()
{
u16 addr;
for (int i = 0; i < 8; i++)
{
oam[i] = secOam[i]; // Copy secondary OAM into primary.
// Different address modes depending on the sprite height:
if (spr_height() == 16)
addr = ((oam[i].tile & 1) * 0x1000) + ((oam[i].tile & ~1) * 16);
else
addr = ( ctrl.sprTbl * 0x1000) + ( oam[i].tile * 16);
unsigned sprY = (scanline - oam[i].y) % spr_height(); // Line inside the sprite.
if (oam[i].attr & 0x80) sprY ^= spr_height() - 1; // Vertical flip.
addr += sprY + (sprY & 8); // Select the second tile if on 8x16.
oam[i].dataL = rd(addr + 0);
oam[i].dataH = rd(addr + 8);
}
}
/* Process a pixel, draw it if it's on screen */
void pixel()
{
u8 palette = 0, objPalette = 0;
bool objPriority = 0;
int x = dot - 2;
if (scanline < 240 and x >= 0 and x < 256)
{
if (mask.bg and not (!mask.bgLeft && x < 8))
{
// Background:
palette = (NTH_BIT(bgShiftH, 15 - fX) << 1) |
NTH_BIT(bgShiftL, 15 - fX);
if (palette)
palette |= ((NTH_BIT(atShiftH, 7 - fX) << 1) |
NTH_BIT(atShiftL, 7 - fX)) << 2;
}
// Sprites:
if (mask.spr and not (!mask.sprLeft && x < 8))
for (int i = 7; i >= 0; i--)
{
if (oam[i].id == 64) continue; // Void entry.
unsigned sprX = x - oam[i].x;
if (sprX >= 8) continue; // Not in range.
if (oam[i].attr & 0x40) sprX ^= 7; // Horizontal flip.
u8 sprPalette = (NTH_BIT(oam[i].dataH, 7 - sprX) << 1) |
NTH_BIT(oam[i].dataL, 7 - sprX);
if (sprPalette == 0) continue; // Transparent pixel.
if (oam[i].id == 0 && palette && x != 255) status.sprHit = true;
sprPalette |= (oam[i].attr & 3) << 2;
objPalette = sprPalette + 16;
objPriority = oam[i].attr & 0x20;
}
// Evaluate priority:
if (objPalette && (palette == 0 || objPriority == 0)) palette = objPalette;
pixels[scanline*256 + x] = nesRgb[rd(0x3F00 + (rendering() ? palette : 0))];
}
// Perform background shifts:
bgShiftL <<= 1; bgShiftH <<= 1;
atShiftL = (atShiftL << 1) | atLatchL;
atShiftH = (atShiftH << 1) | atLatchH;
}
/* Execute a cycle of a scanline */
template<Scanline s> void scanline_cycle()
{
static u16 addr;
if (s == NMI and dot == 1) { status.vBlank = true; if (ctrl.nmi) CPU::set_nmi(); }
else if (s == POST and dot == 0) GUI::new_frame(pixels);
else if (s == VISIBLE or s == PRE)
{
// Sprites:
switch (dot)
{
case 1: clear_oam(); if (s == PRE) { status.sprOvf = status.sprHit = false; } break;
case 257: eval_sprites(); break;
case 321: load_sprites(); break;
}
// Background:
switch (dot)
{
case 2 ... 255: case 322 ... 337:
pixel();
switch (dot % 8)
{
// Nametable:
case 1: addr = nt_addr(); reload_shift(); break;
case 2: nt = rd(addr); break;
// Attribute:
case 3: addr = at_addr(); break;
case 4: at = rd(addr); if (vAddr.cY & 2) at >>= 4;
if (vAddr.cX & 2) at >>= 2; break;
// Background (low bits):
case 5: addr = bg_addr(); break;
case 6: bgL = rd(addr); break;
// Background (high bits):
case 7: addr += 8; break;
case 0: bgH = rd(addr); h_scroll(); break;
} break;
case 256: pixel(); bgH = rd(addr); v_scroll(); break; // Vertical bump.
case 257: pixel(); reload_shift(); h_update(); break; // Update horizontal position.
case 280 ... 304: if (s == PRE) v_update(); break; // Update vertical position.
// No shift reloading:
case 1: addr = nt_addr(); if (s == PRE) status.vBlank = false; break;
case 321: case 339: addr = nt_addr(); break;
// Nametable fetch instead of attribute:
case 338: nt = rd(addr); break;
case 340: nt = rd(addr); if (s == PRE && rendering() && frameOdd) dot++;
}
// Signal scanline to mapper:
if (dot == 260 && rendering()) Cartridge::signal_scanline();
}
}
/* Execute a PPU cycle. */
void step()
{
switch (scanline)
{
case 0 ... 239: scanline_cycle<VISIBLE>(); break;
case 240: scanline_cycle<POST>(); break;
case 241: scanline_cycle<NMI>(); break;
case 261: scanline_cycle<PRE>(); break;
}
// Update dot and scanline counters:
if (++dot > 340)
{
dot %= 341;
if (++scanline > 261)
{
scanline = 0;
frameOdd ^= 1;
}
}
}
void reset()
{
frameOdd = false;
scanline = dot = 0;
ctrl.r = mask.r = status.r = 0;
memset(pixels, 0x00, sizeof(pixels));
memset(ciRam, 0xFF, sizeof(ciRam));
memset(oamMem, 0x00, sizeof(oamMem));
}
}