package ppu import "crecher.net/gones/v2/cart" const ( ctrlNametableMask = 0x03 ctrlIncrement32 = 0x04 ctrlBGPatternHigh = 0x10 ctrlNMIEnable = 0x80 maskGreyscale = 0x01 maskShowBackgroundLeft = 0x02 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 // finished pixels as ARGB8888 framebuffer [ScreenWidth * ScreenHeight]uint32 // background pipeline, see https://www.nesdev.org/wiki/PPU_rendering // next* hold the bytes fetched for the tile after the one being drawn nextTileID uint8 nextAttr uint8 nextPatternLo uint8 nextPatternHi uint8 // shifters, the top bit (minus fine x) is the pixel being drawn patternLo, patternHi uint16 attrLo, attrHi uint16 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 func (p *PPU) Tick() { p.advanceDot() p.renderTick() 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++ } } }