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++
}
}
}
|