aboutsummaryrefslogtreecommitdiff
path: root/tests/ppu_test.go
blob: 82248d636fdea0ed704e6bf10d03e5f626de4174 (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
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
package tests

import (
	"testing"

	"crecher.net/gones/v2/cart"
	"crecher.net/gones/v2/ppu"
)

// fakeMapper gives the PPU 8KB of CHR-RAM and a fixed mirroring mode,
// without needing a real iNES file.
type fakeMapper struct {
	chr       [0x2000]uint8
	mirroring cart.Mirroring
}

func (m *fakeMapper) CPURead(addr uint16) uint8     { return 0 }
func (m *fakeMapper) CPUWrite(addr uint16, v uint8) {}
func (m *fakeMapper) PPURead(addr uint16) uint8     { return m.chr[addr&0x1FFF] }
func (m *fakeMapper) PPUWrite(addr uint16, v uint8) { m.chr[addr&0x1FFF] = v }
func (m *fakeMapper) Mirroring() cart.Mirroring     { return m.mirroring }

func newTestPPU(mirroring cart.Mirroring) (*ppu.PPU, *fakeMapper) {
	m := &fakeMapper{mirroring: mirroring}
	return ppu.New(m, mirroring), m
}

// runToDot ticks the PPU until it reaches the given scanline/dot.
func runToDot(p *ppu.PPU, scanline, dot int) {
	for !(p.Scanline == scanline && p.Dot == dot) {
		p.Tick()
	}
}

// PPUCTRL's nametable-select bits feeding into t are covered by the
// white-box tests in the ppu package itself (ppu/ppu_test.go), since the
// only way to observe t from outside is via $2006, which fully overwrites
// those same bit positions - not useful for isolating PPUCTRL's effect.

func TestStatusReadResetsWriteLatch(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)

	// start a $2006 write pair but only complete the first half.
	p.WriteRegister(0x2006, 0x12)
	// reading $2002 must reset the shared write latch, so the writes
	// below start a fresh pair instead of continuing this one.
	p.ReadRegister(0x2002)

	p.WriteRegister(0x2006, 0x24)
	p.WriteRegister(0x2006, 0x56)
	p.WriteRegister(0x2007, 0x9A)

	p.WriteRegister(0x2006, 0x24)
	p.WriteRegister(0x2006, 0x56)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got != 0x9A {
		t.Errorf("byte at $2456 = %#02x, want 0x9A (status read should have reset the write latch)", got)
	}
}

func TestPPUADDRWriteAndPPUDATAIncrement(t *testing.T) {
	p, _ := newTestPPU(cart.Vertical)

	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00) // v = $2000
	p.WriteRegister(0x2007, 0x11)
	p.WriteRegister(0x2007, 0x22) // increments by 1 (CTRL bit2 clear)

	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00)
	p.ReadRegister(0x2007) // buffered: returns old stale value
	if got := p.ReadRegister(0x2007); got != 0x11 {
		t.Errorf("byte at $2000 = %#02x, want 0x11", got)
	}
	if got := p.ReadRegister(0x2007); got != 0x22 {
		t.Errorf("byte at $2001 = %#02x, want 0x22", got)
	}
}

func TestPPUDATAIncrement32(t *testing.T) {
	p, _ := newTestPPU(cart.Vertical)
	p.WriteRegister(0x2000, 0x04) // VRAM increment = 32

	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00) // v = $2000
	p.WriteRegister(0x2007, 0x99) // writes $2000, v -> $2020
	p.WriteRegister(0x2007, 0x77) // writes $2020, v -> $2040

	// if the increment were wrongly 1 instead of 32, this second byte
	// would have landed at $2001, not $2020.
	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got != 0x99 {
		t.Errorf("byte at $2000 = %#02x, want 0x99", got)
	}

	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x20)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got != 0x77 {
		t.Errorf("byte at $2020 = %#02x, want 0x77 (increment should skip $2001-$201F)", got)
	}
}

func TestPPUDATABufferedReadUnderPalette(t *testing.T) {
	p, _ := newTestPPU(cart.Vertical)

	// seed the nametable byte that sits "under" palette address $3F00,
	// i.e. $2F00 (since $3F00 & 0x2FFF == 0x2F00).
	p.WriteRegister(0x2006, 0x2F)
	p.WriteRegister(0x2006, 0x00)
	p.WriteRegister(0x2007, 0x7E)

	// write a palette entry and read it straight back: should return the
	// palette byte immediately (unbuffered for palette reads)...
	p.WriteRegister(0x2006, 0x3F)
	p.WriteRegister(0x2006, 0x00)
	p.WriteRegister(0x2007, 0x15)
	p.WriteRegister(0x2006, 0x3F)
	p.WriteRegister(0x2006, 0x00)
	if got := p.ReadRegister(0x2007); got != 0x15 {
		t.Errorf("palette readback = %#02x, want 0x15", got)
	}

	// ...but the read buffer underneath should now hold the nametable
	// byte from $2F00, not the palette byte. A non-palette read returns
	// the *previous* buffer, so point v somewhere else first.
	p.WriteRegister(0x2006, 0x21)
	p.WriteRegister(0x2006, 0x00)
	if got := p.ReadRegister(0x2007); got != 0x7E {
		t.Errorf("buffered byte after palette read = %#02x, want 0x7E (buffer should hold the $2F00 nametable byte)", got)
	}
}

func TestPalettePointerMirroring(t *testing.T) {
	p, _ := newTestPPU(cart.Vertical)

	p.WriteRegister(0x2006, 0x3F)
	p.WriteRegister(0x2006, 0x10) // $3F10 mirrors $3F00
	p.WriteRegister(0x2007, 0x30)

	p.WriteRegister(0x2006, 0x3F)
	p.WriteRegister(0x2006, 0x00)
	if got := p.ReadRegister(0x2007); got != 0x30 {
		t.Errorf("$3F00 = %#02x, want 0x30 (written via its $3F10 mirror)", got)
	}
}

func TestNametableMirroringHorizontal(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)

	// horizontal mirroring: NT0==NT1 (page A), NT2==NT3 (page B).
	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00)
	p.WriteRegister(0x2007, 0xAA)

	p.WriteRegister(0x2006, 0x24)
	p.WriteRegister(0x2006, 0x00)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got != 0xAA {
		t.Errorf("$2400 = %#02x, want 0xAA (should mirror $2000 under horizontal mirroring)", got)
	}

	p.WriteRegister(0x2006, 0x28)
	p.WriteRegister(0x2006, 0x00)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got == 0xAA {
		t.Errorf("$2800 should NOT mirror $2000 under horizontal mirroring, got 0xAA")
	}
}

func TestNametableMirroringVertical(t *testing.T) {
	p, _ := newTestPPU(cart.Vertical)

	// vertical mirroring: NT0==NT2 (page A), NT1==NT3 (page B).
	p.WriteRegister(0x2006, 0x20)
	p.WriteRegister(0x2006, 0x00)
	p.WriteRegister(0x2007, 0x55)

	p.WriteRegister(0x2006, 0x28)
	p.WriteRegister(0x2006, 0x00)
	p.ReadRegister(0x2007)
	if got := p.ReadRegister(0x2007); got != 0x55 {
		t.Errorf("$2800 = %#02x, want 0x55 (should mirror $2000 under vertical mirroring)", got)
	}
}

func TestOAMDataReadWrite(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)

	p.WriteRegister(0x2003, 0x10) // OAMADDR
	p.WriteRegister(0x2004, 0x42) // OAMDATA, also increments OAMADDR

	p.WriteRegister(0x2003, 0x10)
	if got := p.ReadRegister(0x2004); got != 0x42 {
		t.Errorf("OAM[0x10] = %#02x, want 0x42", got)
	}
}

func TestVBlankFlagSetAndClearedByRead(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)

	runToDot(p, 241, 1) // the tick that sets vblank already ran
	status := p.ReadRegister(0x2002)
	if status&0x80 == 0 {
		t.Fatal("vblank bit should be set at scanline 241, dot 1")
	}
	if status2 := p.ReadRegister(0x2002); status2&0x80 != 0 {
		t.Fatal("reading $2002 should clear the vblank flag")
	}
}

func TestVBlankClearedAtPreRender(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	// don't read $2002 here, that would clear vblank itself
	runToDot(p, 241, 1)
	runToDot(p, 261, 1)
	if got := p.ReadRegister(0x2002); got&0x80 != 0 {
		t.Fatal("vblank should be cleared at the pre-render line")
	}
}

func TestNMIFiresOnVBlankWhenEnabled(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	p.WriteRegister(0x2000, 0x80) // enable NMI on vblank

	runToDot(p, 241, 1)
	if !p.PollNMI() {
		t.Fatal("expected an NMI edge at scanline 241, dot 1")
	}
	if p.PollNMI() {
		t.Fatal("PollNMI should clear the pending flag")
	}
}

func TestNMIDoesNotFireWhenDisabled(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	runToDot(p, 241, 10)
	if p.PollNMI() {
		t.Fatal("NMI should not fire with PPUCTRL bit 7 clear")
	}
}

func TestNMIRetriggersOnCTRLToggleDuringVBlank(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	p.WriteRegister(0x2000, 0x80)

	runToDot(p, 241, 1)
	if !p.PollNMI() {
		t.Fatal("expected first NMI edge")
	}

	// toggling NMI enable off then on again while still in vblank produces
	// a second rising edge on the internal NMI line, since it's edge- not
	// level-triggered, so it should fire a second NMI.
	p.WriteRegister(0x2000, 0x00)
	if p.PollNMI() {
		t.Fatal("clearing bit 7 must not itself signal an NMI")
	}
	p.WriteRegister(0x2000, 0x80)
	if !p.PollNMI() {
		t.Fatal("expected a second NMI edge after re-enabling mid-vblank")
	}
}

func TestVBlankReadRaceOneDotEarlySuppressesWholeFrame(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	p.WriteRegister(0x2000, 0x80)

	runToDot(p, 241, 0)
	if status := p.ReadRegister(0x2002); status&0x80 != 0 {
		t.Fatal("reading one dot early should see the vblank flag as clear")
	}

	// advance through where vblank would normally be set, and well beyond
	runToDot(p, 241, 10)
	if p.PollNMI() {
		t.Fatal("NMI must not fire this frame after the one-dot-early read")
	}
	if status := p.ReadRegister(0x2002); status&0x80 != 0 {
		t.Fatal("vblank flag should never get set this frame after the one-dot-early read")
	}

	// but the next frame should behave completely normally again
	runToDot(p, 241, 1)
	if status := p.ReadRegister(0x2002); status&0x80 == 0 {
		t.Fatal("vblank flag should set normally on the following frame")
	}
}

func TestVBlankReadRaceSameDotSuppressesNMIOnly(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)
	p.WriteRegister(0x2000, 0x80)

	runToDot(p, 241, 1)
	status := p.ReadRegister(0x2002)
	if status&0x80 == 0 {
		t.Fatal("reading on the exact set dot should still see the flag as set")
	}
	if p.PollNMI() {
		t.Fatal("NMI should be suppressed for this frame when $2002 is read on the set dot")
	}

	runToDot(p, 261, 1) // pre-render clears the suppression state
	// Read well clear of the race window this time (dot 1/2 would
	// re-trigger the very suppression we're testing past).
	runToDot(p, 241, 10)
	if !p.PollNMI() {
		t.Fatal("NMI should fire normally again next frame")
	}
	if status := p.ReadRegister(0x2002); status&0x80 == 0 {
		t.Fatal("vblank should set normally again next frame")
	}
}

func TestOddFrameDotSkipOnlyWhenRenderingEnabled(t *testing.T) {
	p, _ := newTestPPU(cart.Horizontal)

	// Rendering disabled: scanline 261 must have the full 341 dots (0..340).
	runToDot(p, 261, 340)
	p.Tick()
	if p.Scanline != 0 || p.Dot != 0 {
		t.Fatalf("with rendering disabled, dot 340 should advance to scanline 0 dot 0, got scanline %d dot %d", p.Scanline, p.Dot)
	}

	p2, _ := newTestPPU(cart.Horizontal)
	p2.WriteRegister(0x2001, 0x08) // show background -> rendering enabled
	// get to an odd frame's pre-render line
	for p2.Frame%2 == 0 {
		p2.Tick()
	}
	runToDot(p2, 261, 339)
	p2.Tick()
	if p2.Scanline != 0 || p2.Dot != 0 {
		t.Fatalf("on an odd frame with rendering enabled, dot 339 should skip straight to scanline 0 dot 0, got scanline %d dot %d", p2.Scanline, p2.Dot)
	}
}