aboutsummaryrefslogtreecommitdiff
path: root/ppu/render.go
blob: bfde9e4f790bf84150a62529e911e8ade61db503 (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package ppu

const (
	ScreenWidth  = 256
	ScreenHeight = 240

	preRenderLine = 261
)

// pixels of the last (or in progress) frame as ARGB8888
func (p *PPU) Framebuffer() []uint32 {
	return p.framebuffer[:]
}

// runs after Tick has moved to the new dot
func (p *PPU) renderTick() {
	line, dot := p.Scanline, p.Dot
	visibleLine := line < ScreenHeight

	if p.renderingEnabled() && (visibleLine || line == preRenderLine) {
		p.backgroundFetch(dot)
		if dot == 256 {
			p.incrementY()
		}
		if dot == 257 {
			p.loadBackgroundShifters()
			p.copyHorizontal()
		}
		if line == preRenderLine && dot >= 280 && dot <= 304 {
			p.copyVertical()
		}
	}

	if visibleLine && dot >= 1 && dot <= ScreenWidth {
		p.drawPixel(dot-1, line)
	}
}

// the fetch cycle, every tile takes 8 dots to fetch (nametable byte, attribute
// byte, pattern low, pattern high), 2 dots each. dots 321-336 fetch the first two
// tiles of the next line ahead of time
func (p *PPU) backgroundFetch(dot int) {
	if !((dot >= 2 && dot <= 257) || (dot >= 321 && dot <= 337)) {
		return
	}
	if p.Mask&maskShowBackground != 0 {
		p.shiftBackground()
	}

	switch (dot - 1) % 8 {
	case 0:
		p.loadBackgroundShifters()
		p.nextTileID = p.readVRAM(0x2000 | (p.v & 0x0FFF))
	case 2:
		// attribute byte holds 4 palettes (2 bits each) for a 4x4 tile area,
		// coarse y/x bit 1 pick which quadrant is ours
		addr := 0x23C0 | (p.v & 0x0C00) | ((p.v >> 4) & 0x38) | ((p.v >> 2) & 0x07)
		attr := p.readVRAM(addr)
		if p.v&0x40 != 0 {
			attr >>= 4
		}
		if p.v&0x02 != 0 {
			attr >>= 2
		}
		p.nextAttr = attr & 0x03
	case 4:
		p.nextPatternLo = p.readVRAM(p.backgroundPatternAddr())
	case 6:
		p.nextPatternHi = p.readVRAM(p.backgroundPatternAddr() + 8)
	case 7:
		p.incrementCoarseX()
	}
}

// address of the current row of the next tiles pattern
func (p *PPU) backgroundPatternAddr() uint16 {
	var base uint16
	if p.Ctrl&ctrlBGPatternHigh != 0 {
		base = 0x1000
	}
	fineY := (p.v >> 12) & 0x07
	return base + uint16(p.nextTileID)*16 + fineY
}

func (p *PPU) shiftBackground() {
	p.patternLo <<= 1
	p.patternHi <<= 1
	p.attrLo <<= 1
	p.attrHi <<= 1
}

// puts fetched tile into the low byte of the shifters. the attribute bits
// are the same for all 8 pixels of the tile, so they get repeated
func (p *PPU) loadBackgroundShifters() {
	p.patternLo = (p.patternLo & 0xFF00) | uint16(p.nextPatternLo)
	p.patternHi = (p.patternHi & 0xFF00) | uint16(p.nextPatternHi)
	p.attrLo = (p.attrLo & 0xFF00) | repeatBit(p.nextAttr&0x01)
	p.attrHi = (p.attrHi & 0xFF00) | repeatBit(p.nextAttr&0x02)
}

func repeatBit(b uint8) uint16 {
	if b != 0 {
		return 0x00FF
	}
	return 0
}

// scroll helpers, v layout is yyy NN YYYYY XXXXX
// https://www.nesdev.org/wiki/PPU_scrolling

func (p *PPU) incrementCoarseX() {
	if p.v&0x001F == 31 {
		p.v &^= 0x001F
		p.v ^= 0x0400 // wrap into the horizontally adjacent nametable
	} else {
		p.v++
	}
}

func (p *PPU) incrementY() {
	if p.v&0x7000 != 0x7000 {
		p.v += 0x1000 // fine y
		return
	}
	p.v &^= 0x7000
	y := (p.v & 0x03E0) >> 5
	switch y {
	case 29:
		y = 0
		p.v ^= 0x0800 // wrap into the vertically adjacent nametable
	case 31:
		y = 0
	default:
		y++
	}
	p.v = (p.v &^ 0x03E0) | (y << 5)
}

func (p *PPU) copyHorizontal() {
	p.v = (p.v &^ 0x041F) | (p.t & 0x041F)
}

func (p *PPU) copyVertical() {
	p.v = (p.v &^ 0x7BE0) | (p.t & 0x7BE0)
}

func (p *PPU) drawPixel(x, y int) {
	var pixel, palette uint8
	if p.Mask&maskShowBackground != 0 && (x >= 8 || p.Mask&maskShowBackgroundLeft != 0) {
		bit := uint16(0x8000) >> p.x
		pixel = bitAt(p.patternHi, bit)<<1 | bitAt(p.patternLo, bit)
		palette = bitAt(p.attrHi, bit)<<1 | bitAt(p.attrLo, bit)
	}

	// pixel 0 of every palette is the shared backdrop color at $3F00
	addr := uint16(0x3F00)
	if pixel != 0 {
		addr += uint16(palette)<<2 | uint16(pixel)
	}
	color := p.readPalette(addr)
	if p.Mask&maskGreyscale != 0 {
		color &= 0x30
	}
	p.framebuffer[y*ScreenWidth+x] = 0xFF000000 | systemPalette[color&0x3F]
}

func bitAt(v, bit uint16) uint8 {
	if v&bit != 0 {
		return 1
	}
	return 0
}