aboutsummaryrefslogtreecommitdiff
path: root/ppu/ppu.go
diff options
context:
space:
mode:
Diffstat (limited to 'ppu/ppu.go')
-rw-r--r--ppu/ppu.go114
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++
+ }
+ }
+}