diff options
Diffstat (limited to 'ppu/ppu.go')
| -rw-r--r-- | ppu/ppu.go | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/ppu/ppu.go b/ppu/ppu.go new file mode 100644 index 0000000..cfc3620 --- /dev/null +++ b/ppu/ppu.go @@ -0,0 +1,114 @@ +package ppu + +import "crecher.net/gones/v2/cart" + +const ( + ctrlNametableMask = 0x03 + ctrlIncrement32 = 0x04 + ctrlNMIEnable = 0x80 + + maskShowBackground = 0x08 + maskShowSprites = 0x10 + + statusOverflow = 0x20 + statusSprite0 = 0x40 + statusVBlank = 0x80 +) + +type PPU struct { + mapper cart.Mapper + mirroring cart.Mirroring + + vram [0x800]uint8 // 2KB nametable RAM + palette [32]uint8 + + Ctrl uint8 // $2000, latched + Mask uint8 // $2001, latched + OAMAddr uint8 // $2003 + OAM [256]uint8 + + // scroll registers, see https://www.nesdev.org/wiki/PPU_scrolling + v, t uint16 // 15 bits used + x uint8 // fine X, 3 bits used + w bool // write toggle, shared by $2005/$2006 + + readBuffer uint8 // internal $2007 read buffer + busLatch uint8 // crude open-bus stand in last byte written to any register + + spriteOverflow bool + sprite0Hit bool + vblank bool // aka nmi_occurred + + Scanline int + Dot int + Frame uint64 + + nmiOutput bool // PPUCTRL bit 7 + nmiLine bool // vblank && nmiOutput, latched each tick for edge detection + nmiPending bool // set on nmiLine's rising edge, consumed by PollNMI + + // see the $2002 read/vblank race window handled in readStatus + suppressVBlankSet bool + suppressNMIForFrame bool +} + +func New(mapper cart.Mapper, mirroring cart.Mirroring) *PPU { + return &PPU{mapper: mapper, mirroring: mirroring} +} + +// reports if an NMI edge fired since the last call, clearing it +func (p *PPU) PollNMI() bool { + pending := p.nmiPending + p.nmiPending = false + return pending +} + +func (p *PPU) renderingEnabled() bool { + return p.Mask&(maskShowBackground|maskShowSprites) != 0 +} + +func (p *PPU) updateNMILine() { + current := p.vblank && p.nmiOutput + if current && !p.nmiLine && !p.suppressNMIForFrame { + p.nmiPending = true + } + p.nmiLine = current +} + +// advances PPU by one dot, right now only tracks the dot/scanline position and the vblank flag/NMI line, +// which is enough to just validate the timings. +func (p *PPU) Tick() { + p.advanceDot() + + switch { + case p.Scanline == 241 && p.Dot == 1: + if !p.suppressVBlankSet { + p.vblank = true + } + p.suppressVBlankSet = false + case p.Scanline == 261 && p.Dot == 1: + p.vblank = false + p.sprite0Hit = false + p.spriteOverflow = false + p.suppressVBlankSet = false + p.suppressNMIForFrame = false + } + p.updateNMILine() +} + +func (p *PPU) advanceDot() { + maxDot := 340 + if p.Scanline == 261 && p.renderingEnabled() && p.Frame%2 == 1 { + maxDot = 339 // pre-render line is one dot short every odd frame while rendering + } + + p.Dot++ + if p.Dot > maxDot { + p.Dot = 0 + p.Scanline++ + if p.Scanline > 261 { + p.Scanline = 0 + p.Frame++ + } + } +} |