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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
package ppu
import "crecher.net/gones/v2/cart"
const (
ctrlNametableMask = 0x03
ctrlIncrement32 = 0x04
ctrlSpritePatternHigh = 0x08
ctrlBGPatternHigh = 0x10
ctrlSpriteSize16 = 0x20
ctrlNMIEnable = 0x80
maskGreyscale = 0x01
maskShowBackgroundLeft = 0x02
maskShowSpritesLeft = 0x04
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
// finished pixels as ARGB8888
framebuffer [ScreenWidth * ScreenHeight]uint32
// background pipeline, see https://www.nesdev.org/wiki/PPU_rendering
// next* hold the bytes fetched for the tile after the one being drawn
nextTileID uint8
nextAttr uint8
nextPatternLo uint8
nextPatternHi uint8
// shifters, the top bit (minus fine x) is the pixel being drawn
patternLo, patternHi uint16
attrLo, attrHi uint16
// sprites for the line being drawn, picked at the end of the previous line
spriteLine [8]sprite
spriteCount int
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
func (p *PPU) Tick() {
p.advanceDot()
p.renderTick()
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++
}
}
}
|