diff options
Diffstat (limited to 'ppu')
| -rw-r--r-- | ppu/memory.go | 46 | ||||
| -rw-r--r-- | ppu/ppu.go | 114 | ||||
| -rw-r--r-- | ppu/ppu_test.go | 108 | ||||
| -rw-r--r-- | ppu/registers.go | 141 |
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 +} |