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