/
ppu.go
361 lines (308 loc) · 9.97 KB
/
ppu.go
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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
package gb
import (
"github.com/Humpheh/goboy/pkg/bits"
)
// Update the state of the graphics.
func (gb *Gameboy) updateGraphics(cycles int) {
gb.setLCDStatus()
if !gb.isLCDEnabled() {
return
}
gb.scanlineCounter -= cycles
if gb.scanlineCounter <= 0 {
gb.Memory.HighRAM[0x44]++
if gb.Memory.HighRAM[0x44] > 153 {
gb.PreparedData = gb.screenData
gb.screenData = [160][144][3]uint8{}
gb.Memory.HighRAM[0x44] = 0
}
currentLine := gb.Memory.ReadHighRam(0xFF44)
gb.scanlineCounter += 456 * gb.getSpeed()
if currentLine == 144 {
gb.requestInterrupt(0)
}
}
}
// Set the status of the LCD based on the current state of memory.
func (gb *Gameboy) setLCDStatus() {
status := gb.Memory.ReadHighRam(0xFF41)
if !gb.isLCDEnabled() {
// TODO: Set screen to white in this instance
gb.scanlineCounter = 456
gb.Memory.HighRAM[0x44] = 0
status &= 252
// TODO: Check this is correct
// We aren't in a mode so reset the values
status = bits.Reset(status, 0)
status = bits.Reset(status, 1)
gb.Memory.Write(0xFF41, status)
return
}
currentLine := gb.Memory.ReadHighRam(0xFF44)
currentMode := status & 0x3
var mode byte
requestInterrupt := false
if currentLine >= 144 {
mode = 1
status = bits.Set(status, 0)
status = bits.Reset(status, 1)
requestInterrupt = bits.Test(status, 4)
} else {
mode2Bounds := 456 - 80
mode3Bounds := mode2Bounds - 172
if gb.scanlineCounter >= mode2Bounds {
if currentLine != gb.lastDrawnScanline {
// Draw the scanline at the start of the v-blank period
gb.drawScanline(gb.lastDrawnScanline)
gb.lastDrawnScanline = currentLine
}
mode = 2
status = bits.Reset(status, 0)
status = bits.Set(status, 1)
requestInterrupt = bits.Test(status, 5)
} else if gb.scanlineCounter >= mode3Bounds {
mode = 3
status = bits.Set(status, 0)
status = bits.Set(status, 1)
} else {
mode = 0
status = bits.Reset(status, 0)
status = bits.Reset(status, 1)
requestInterrupt = bits.Test(status, 3)
if mode != currentMode {
gb.Memory.hbHDMATransfer()
}
}
}
if requestInterrupt && mode != currentMode {
gb.requestInterrupt(1)
}
// Check is LYC == LY (coincidence flag)
if currentLine == gb.Memory.ReadHighRam(0xFF45) {
status = bits.Set(status, 2)
// If enabled request an interrupt for this
if bits.Test(status, 6) {
gb.requestInterrupt(1)
}
} else {
status = bits.Reset(status, 2)
}
gb.Memory.Write(0xFF41, status)
}
// Checks if the LCD is enabled by examining 0xFF40.
func (gb *Gameboy) isLCDEnabled() bool {
return bits.Test(gb.Memory.ReadHighRam(0xFF40), 7)
}
// Draw a single scanline to the graphics output.
func (gb *Gameboy) drawScanline(scanline byte) {
control := gb.Memory.ReadHighRam(0xFF40)
if bits.Test(control, 0) && !gb.Debug.HideBackground {
gb.renderTiles(control, scanline)
}
if bits.Test(control, 1) && !gb.Debug.HideSprites {
gb.renderSprites(control, int32(scanline))
}
}
// Get settings to be used when rendering tiles.
func (gb *Gameboy) getTileSettings(lcdControl byte, windowY byte) (
usingWindow bool,
unsigned bool,
tileData uint16,
backgroundMemory uint16,
) {
tileData = uint16(0x8800)
if bits.Test(lcdControl, 5) {
// Is current scanline we're drawing within windows Y position?
if windowY <= gb.Memory.ReadHighRam(0xFF44) {
usingWindow = true
}
}
// Test if we're using unsigned bytes
if bits.Test(lcdControl, 4) {
tileData = 0x8000
unsigned = true
}
// Work out where to look in background memory.
var testBit byte = 3
if usingWindow {
testBit = 6
}
backgroundMemory = uint16(0x9800)
if bits.Test(lcdControl, testBit) {
backgroundMemory = 0x9C00
}
return
}
// Render a scanline of the tile map to the graphics output based
// on the state of the lcdControl register.
func (gb *Gameboy) renderTiles(lcdControl byte, scanline byte) {
scrollY := gb.Memory.ReadHighRam(0xFF42)
scrollX := gb.Memory.ReadHighRam(0xFF43)
windowY := gb.Memory.ReadHighRam(0xFF4A)
windowX := gb.Memory.ReadHighRam(0xFF4B) - 7
usingWindow, unsigned, tileData, backgroundMemory := gb.getTileSettings(lcdControl, windowY)
// yPos is used to calc which of 32 v-lines the current scanline is drawing
var yPos byte
if !usingWindow {
yPos = scrollY + scanline
} else {
yPos = scanline - windowY
}
// which of the 8 vertical pixels of the current tile is the scanline on?
var tileRow = uint16(yPos/8) * 32
// Load the palette which will be used to draw the tiles
var palette = gb.Memory.ReadHighRam(0xFF47)
// start drawing the 160 horizontal pixels for this scanline
gb.tileScanline = [160]uint8{}
for pixel := byte(0); pixel < 160; pixel++ {
xPos := pixel + scrollX
// Translate the current x pos to window space if necessary
if usingWindow && pixel >= windowX {
xPos = pixel - windowX
}
// Which of the 32 horizontal tiles does this x_pox fall within?
tileCol := uint16(xPos / 8)
// Get the tile identity number
tileAddress := backgroundMemory + tileRow + tileCol
// Deduce where this tile id is in memory
tileLocation := tileData
if unsigned {
tileNum := int16(gb.Memory.VRAM[tileAddress-0x8000])
tileLocation = tileLocation + uint16(tileNum*16)
} else {
tileNum := int16(int8(gb.Memory.VRAM[tileAddress-0x8000]))
tileLocation = uint16(int32(tileLocation) + int32((tileNum+128)*16))
}
bankOffset := uint16(0x8000)
// Attributes used in CGB mode TODO: check in CGB mode
//
// Bit 0-2 Background Palette number (BGP0-7)
// Bit 5 Horizontal Flip (0=Normal, 1=Mirror horizontally)
// Bit 6 Vertical Flip (0=Normal, 1=Mirror vertically)
// Bit 7 BG-to-OAM Priority (0=Use OAM priority bit, 1=BG Priority
//
tileAttr := gb.Memory.VRAM[tileAddress-0x6000]
if gb.IsCGB() && bits.Test(tileAttr, 3) {
bankOffset = 0x6000
}
var line byte
if gb.IsCGB() && bits.Test(tileAttr, 6) {
// Vertical flip
line = ((7 - yPos) % 8) * 2
} else {
line = (yPos % 8) * 2
}
// Get the tile data from memory
data1 := gb.Memory.VRAM[tileLocation+uint16(line)-bankOffset]
data2 := gb.Memory.VRAM[tileLocation+uint16(line)+1-bankOffset]
if gb.IsCGB() && bits.Test(tileAttr, 5) {
// Horizontal flip
xPos = 7 - xPos
}
colourBit := byte(int8((xPos%8)-7) * -1)
colourNum := (bits.Val(data2, colourBit) << 1) | bits.Val(data1, colourBit)
gb.setTilePixel(pixel, scanline, tileAttr, colourNum, palette)
}
}
func (gb *Gameboy) setTilePixel(x, y, tileAttr, colourNum, palette byte) {
// Set the pixel
if gb.IsCGB() {
cgbPalette := tileAttr & 0x7
red, green, blue := gb.BGPalette.get(cgbPalette, colourNum)
gb.setPixel(x, y, red, green, blue, true)
} else {
red, green, blue := gb.getColour(colourNum, palette)
gb.setPixel(x, y, red, green, blue, true)
}
// Store for the current scanline so sprite priority can be managed
gb.tileScanline[x] = colourNum
}
// Get the RGB colour value for a colour num at an address using the current palette.
func (gb *Gameboy) getColour(colourNum byte, palette byte) (uint8, uint8, uint8) {
hi := colourNum<<1 | 1
lo := colourNum << 1
col := (bits.Val(palette, hi) << 1) | bits.Val(palette, lo)
return GetPaletteColour(col)
}
// Render the sprites to the screen on the current scanline using the lcdControl register.
func (gb *Gameboy) renderSprites(lcdControl byte, scanline int32) {
var ySize int32 = 8
if bits.Test(lcdControl, 2) {
ySize = 16
}
// Load the two palettes which sprites can be drawn in
var palette1 = gb.Memory.ReadHighRam(0xFF48)
var palette2 = gb.Memory.ReadHighRam(0xFF49)
for sprite := 0; sprite < 40; sprite++ {
// Load sprite data from memory.
index := sprite * 4
yPos := int32(gb.Memory.Read(uint16(0xFE00+index))) - 16
xPos := int32(gb.Memory.Read(uint16(0xFE00+index+1))) - 8
tileLocation := gb.Memory.Read(uint16(0xFE00 + index + 2))
attributes := gb.Memory.Read(uint16(0xFE00 + index + 3))
yFlip := bits.Test(attributes, 6)
xFlip := bits.Test(attributes, 5)
priority := !bits.Test(attributes, 7)
// Bank the sprite data in is (CGB only)
var bank uint16
if gb.IsCGB() {
bank = uint16((attributes & 0x8) >> 3)
}
// If this is true the scanline is out of the area we care about
if scanline < yPos || scanline >= (yPos+ySize) {
continue
}
// Set the line to draw based on if the sprite is flipped on the y
line := scanline - yPos
if yFlip {
line = ySize - line - 1
}
// Load the data containing the sprite data for this line
dataAddress := (uint16(tileLocation) * 16) + uint16(line*2) + (bank * 0x2000)
data1 := gb.Memory.VRAM[dataAddress]
data2 := gb.Memory.VRAM[dataAddress+1]
// Draw the line of the sprite
for tilePixel := byte(0); tilePixel < 8; tilePixel++ {
colourBit := tilePixel
if xFlip {
colourBit = byte(int8(colourBit-7) * -1)
}
// Find the colour value by combining the data bits
colourNum := (bits.Val(data2, colourBit) << 1) | bits.Val(data1, colourBit)
// Colour 0 is transparent for sprites
if colourNum == 0 {
continue
}
pixel := int16(xPos) + int16(7-tilePixel)
// Set the pixel if it is in bounds
if pixel >= 0 && pixel < 160 {
if gb.IsCGB() {
cgbPalette := attributes & 0x7
red, green, blue := gb.SpritePalette.get(cgbPalette, colourNum)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
} else {
// Determine the colour palette to use
var palette = palette1
if bits.Test(attributes, 4) {
palette = palette2
}
red, green, blue := gb.getColour(colourNum, palette)
gb.setPixel(byte(pixel), byte(scanline), red, green, blue, priority)
}
}
}
}
}
// Set a pixel in the graphics screen data.
func (gb *Gameboy) setPixel(x byte, y byte, r uint8, g uint8, b uint8, priority bool) {
// If priority is false then sprite pixel is only set if tile colour is 0
if priority || gb.tileScanline[x] == 0 {
gb.screenData[x][y][0] = r
gb.screenData[x][y][1] = g
gb.screenData[x][y][2] = b
}
}
// GetScanlineCounter returns the current value of the scanline counter.
func (gb *Gameboy) GetScanlineCounter() int {
return gb.scanlineCounter
}