aboutsummaryrefslogtreecommitdiff
path: root/ppu/ppu.go
blob: 322cf3dd0a017bafd9295eedc429732937780d74 (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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package ppu

import "crecher.net/gones/v2/cart"

const (
	ctrlNametableMask = 0x03
	ctrlIncrement32   = 0x04
	ctrlBGPatternHigh = 0x10
	ctrlNMIEnable     = 0x80

	maskGreyscale          = 0x01
	maskShowBackgroundLeft = 0x02
	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

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