aboutsummaryrefslogtreecommitdiff
path: root/ppu
diff options
context:
space:
mode:
Diffstat (limited to 'ppu')
-rw-r--r--ppu/memory.go46
-rw-r--r--ppu/ppu.go114
-rw-r--r--ppu/ppu_test.go108
-rw-r--r--ppu/registers.go141
4 files changed, 409 insertions, 0 deletions
diff --git a/ppu/memory.go b/ppu/memory.go
new file mode 100644
index 0000000..3379242
--- /dev/null
+++ b/ppu/memory.go
@@ -0,0 +1,46 @@
+package ppu
+
+import "crecher.net/gones/v2/cart"
+
+// maps a $2000-$3EFF address onto an offset into the 2KB of nametable RAM,
+// according to the cartridge's mirroring mode. four-screen carts will be
+// expected to use their own extra 2KB of RAM via the mapper, but since no
+// implemented mapper does that yet, four-screen just falls back to the vertical
+// mirroring as a placeholder
+
+func (p *PPU) nametableOffset(addr uint16) int {
+ addr &= 0x0FFF
+ table := addr / 0x400
+ offset := int(addr % 0x400)
+
+ var page uint16
+ switch p.mirroring {
+ case cart.Horizontal:
+ page = table / 2
+ case cart.SingleScreenLower:
+ page = 0
+ case cart.SingleScreenUpper:
+ page = 1
+ default: // TODO: add correct four-screen behavior once four-screen carts are added
+ page = table % 2
+ }
+ return int(page)*0x400 + offset
+}
+
+// paletteIndex applies the $3F10/$3F14/$3F18/$3F1C -> $3F00/$3F04/$3F08/$3F0C
+// mirroring
+func paletteIndex(addr uint16) uint16 {
+ idx := addr & 0x1F
+ if idx >= 0x10 && idx%4 == 0 {
+ idx -= 0x10
+ }
+ return idx
+}
+
+func (p *PPU) readPalette(addr uint16) uint8 {
+ return p.palette[paletteIndex(addr)]
+}
+
+func (p *PPU) writePalette(addr uint16, v uint8) {
+ p.palette[paletteIndex(addr)] = v
+}
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++
+ }
+ }
+}
diff --git a/ppu/ppu_test.go b/ppu/ppu_test.go
new file mode 100644
index 0000000..9426031
--- /dev/null
+++ b/ppu/ppu_test.go
@@ -0,0 +1,108 @@
+package ppu
+
+// more tests for the scroll registers since they can't be checked outside
+// the package without a $2006 write that would mess up the bits under test
+
+import (
+ "testing"
+
+ "crecher.net/gones/v2/cart"
+)
+
+// nilMapper is enough for these tests
+type nilMapper struct{}
+
+func (nilMapper) CPURead(addr uint16) uint8 { return 0 }
+func (nilMapper) CPUWrite(addr uint16, v uint8) {}
+func (nilMapper) PPURead(addr uint16) uint8 { return 0 }
+func (nilMapper) PPUWrite(addr uint16, v uint8) {}
+func (nilMapper) Mirroring() cart.Mirroring { return cart.Horizontal }
+
+func newPPU() *PPU { return New(nilMapper{}, cart.Horizontal) }
+
+func TestCtrlNametableBitsSetT(t *testing.T) {
+ p := newPPU()
+ p.WriteRegister(0x2000, 0x03) // nametable select = 3 -> t bits 10-11
+ if got := p.t & 0x0C00; got != 0x0C00 {
+ t.Errorf("t&0x0C00 = %#04x, want 0x0C00", got)
+ }
+
+ p.WriteRegister(0x2000, 0x00)
+ if got := p.t & 0x0C00; got != 0 {
+ t.Errorf("t&0x0C00 = %#04x, want 0 after clearing nametable select", got)
+ }
+}
+
+func TestScrollFirstWriteSetsCoarseXAndFineX(t *testing.T) {
+ p := newPPU()
+ p.WriteRegister(0x2005, 0b11101_011) // coarseX=0b11101=29, fineX=0b011=3
+
+ if got := p.t & 0x001F; got != 29 {
+ t.Errorf("coarse X in t = %d, want 29", got)
+ }
+ if p.x != 3 {
+ t.Errorf("fine X = %d, want 3", p.x)
+ }
+ if !p.w {
+ t.Error("w should be set after the first $2005 write")
+ }
+}
+
+func TestScrollSecondWriteSetsCoarseYAndFineY(t *testing.T) {
+ p := newPPU()
+ p.WriteRegister(0x2005, 0x00) // first write, establishes w=true
+ p.WriteRegister(0x2005, 0b10110_101) // coarseY=0b10110=22, fineY=0b101=5
+
+ if got := (p.t >> 5) & 0x1F; got != 22 {
+ t.Errorf("coarse Y in t = %d, want 22", got)
+ }
+ if got := (p.t >> 12) & 0x7; got != 5 {
+ t.Errorf("fine Y in t = %d, want 5", got)
+ }
+ if p.w {
+ t.Error("w should be cleared after the second $2005 write")
+ }
+}
+
+func TestScrollWriteDoesNotTouchNametableBits(t *testing.T) {
+ p := newPPU()
+ p.WriteRegister(0x2000, 0x03) // select nametable 3
+ p.WriteRegister(0x2005, 0xFF)
+ p.WriteRegister(0x2005, 0xFF)
+
+ if got := p.t & 0x0C00; got != 0x0C00 {
+ t.Errorf("PPUSCROLL writes clobbered the nametable-select bits: t&0x0C00 = %#04x, want 0x0C00", got)
+ }
+}
+
+func TestAddrWriteSequenceSetsTThenV(t *testing.T) {
+ p := newPPU()
+ p.v = 0x1234 // poison v so we can tell the first write didn't touch it
+
+ p.WriteRegister(0x2006, 0x3D) // high byte: only the low 6 bits are kept, bit 14 cleared
+ if got := p.t & 0xFF00; got != 0x3D00 {
+ t.Errorf("t high byte = %#04x, want 0x3D00", got)
+ }
+ if p.v != 0x1234 {
+ t.Errorf("v changed after the first $2006 write: %#04x", p.v)
+ }
+
+ p.WriteRegister(0x2006, 0x56)
+ if p.t != 0x3D56 {
+ t.Errorf("t = %#04x, want 0x3D56", p.t)
+ }
+ if p.v != 0x3D56 {
+ t.Errorf("v = %#04x, want 0x3D56 (should be copied from t on the second write)", p.v)
+ }
+}
+
+func TestAddrHighByteClearsBit14(t *testing.T) {
+ p := newPPU()
+ p.WriteRegister(0x2006, 0xFF) // top 2 bits of the written byte are discarded
+ if p.t&0x4000 != 0 {
+ t.Errorf("t bit 14 should always be cleared by a $2006 high-byte write, t = %#04x", p.t)
+ }
+ if got := p.t & 0xFF00; got != 0x3F00 {
+ t.Errorf("t high byte = %#04x, want 0x3F00 (0xFF masked to 6 bits)", got)
+ }
+}
diff --git a/ppu/registers.go b/ppu/registers.go
new file mode 100644
index 0000000..5445619
--- /dev/null
+++ b/ppu/registers.go
@@ -0,0 +1,141 @@
+package ppu
+
+func (p *PPU) ReadRegister(addr uint16) uint8 {
+ switch addr & 0x7 {
+ case 2:
+ return p.readStatus()
+ case 4:
+ return p.OAM[p.OAMAddr]
+ case 7:
+ return p.readData()
+ default:
+ return p.busLatch // $2000,2001,2003,2005,2006 are write only: open bus
+ }
+}
+
+// handles cpu write to $2000-2007
+func (p *PPU) WriteRegister(addr uint16, v uint8) {
+ p.busLatch = v
+ switch addr & 0x7 {
+ case 0:
+ p.Ctrl = v
+ p.t = (p.t &^ 0x0C00) | (uint16(v&ctrlNametableMask) << 10)
+ p.nmiOutput = v&ctrlNMIEnable != 0
+ p.updateNMILine()
+ case 1:
+ p.Mask = v
+ case 3:
+ p.OAMAddr = v
+ case 4:
+ p.OAM[p.OAMAddr] = v
+ p.OAMAddr++
+ case 5:
+ p.writeScroll(v)
+ case 6:
+ p.writeAddr(v)
+ case 7:
+ p.writeData(v)
+ }
+}
+
+// implements the $2002 read, including the hardware race around the dot
+// vblank gets set (see https://www.nesdev.org/wiki/NMI)
+func (p *PPU) readStatus() uint8 {
+ result := p.busLatch & 0x1F
+ if p.vblank {
+ result |= statusVBlank
+ }
+ if p.sprite0Hit {
+ result |= statusSprite0
+ }
+ if p.spriteOverflow {
+ result |= statusOverflow
+ }
+
+ if p.Scanline == 241 {
+ switch p.Dot {
+ case 0:
+ p.suppressVBlankSet = true
+ p.suppressNMIForFrame = true
+ case 1, 2:
+ p.suppressNMIForFrame = true
+ p.nmiPending = false
+ }
+ }
+
+ p.vblank = false
+ p.w = false
+ p.updateNMILine()
+ return result
+}
+
+func (p *PPU) writeScroll(v uint8) {
+ if !p.w {
+ p.t = (p.t &^ 0x001F) | uint16(v>>3)
+ p.x = v & 0x07
+ } else {
+ p.t = (p.t &^ 0x73E0) | (uint16(v&0x07) << 12) | (uint16(v&0xF8) << 2)
+ }
+ p.w = !p.w
+}
+
+func (p *PPU) writeAddr(v uint8) {
+ if !p.w {
+ p.t = (p.t & 0x00FF) | (uint16(v&0x3F) << 8)
+ } else {
+ p.t = (p.t & 0xFF00) | uint16(v)
+ p.v = p.t
+ }
+ p.w = !p.w
+}
+
+// implements buffered $2007 and a quirk which i am too lazy to write down
+// right now :D
+// https://www.nesdev.org/wiki/PPU_registers#The_PPUDATA_data_port
+func (p *PPU) readData() uint8 {
+ addr := p.v & 0x3FFF
+ var result uint8
+ if addr >= 0x3F00 {
+ result = p.readPalette(addr)
+ p.readBuffer = p.readVRAM(addr & 0x2FFF)
+ } else {
+ result = p.readBuffer
+ p.readBuffer = p.readVRAM(addr)
+ }
+ p.incrementV()
+ return result
+}
+
+func (p *PPU) writeData(v uint8) {
+ addr := p.v & 0x3FFF
+ if addr >= 0x3F00 {
+ p.writePalette(addr, v)
+ } else {
+ p.writeVRAM(addr, v)
+ }
+ p.incrementV()
+}
+
+func (p *PPU) incrementV() {
+ if p.Ctrl&ctrlIncrement32 != 0 {
+ p.v += 32
+ } else {
+ p.v++
+ }
+ p.v &= 0x7FFF
+}
+
+func (p *PPU) readVRAM(addr uint16) uint8 {
+ if addr < 0x2000 {
+ return p.mapper.PPURead(addr)
+ }
+ return p.vram[p.nametableOffset(addr)]
+}
+
+func (p *PPU) writeVRAM(addr uint16, v uint8) {
+ if addr < 0x2000 {
+ p.mapper.PPUWrite(addr, v)
+ return
+ }
+ p.vram[p.nametableOffset(addr)] = v
+}