aboutsummaryrefslogtreecommitdiff
path: root/ppu/ppu.go
blob: cfc3620293eac307d8e75dc24f6545d5bbe61546 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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++
		}
	}
}