diff options
Diffstat (limited to 'tests/ppu_sprite_test.go')
| -rw-r--r-- | tests/ppu_sprite_test.go | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/tests/ppu_sprite_test.go b/tests/ppu_sprite_test.go new file mode 100644 index 0000000..13ba1d0 --- /dev/null +++ b/tests/ppu_sprite_test.go @@ -0,0 +1,291 @@ +package tests + +import ( + "testing" + + "crecher.net/gones/v2/ppu" +) + +// oam stores y - 1, so a sprite with y shows from scanline y + 1 +func setSprite(p *ppu.PPU, index int, y, tile, attr, x uint8) { + p.WriteRegister(0x2003, uint8(index*4)) + for _, b := range []uint8{y, tile, attr, x} { + p.WriteRegister(0x2004, b) + } + p.WriteRegister(0x2003, 0) +} + +// tile 1 from newSceneWithTile1: row 0 is color 1, row 1 is color 2 +// sprite palette 0: 1 = blue, 2 = red, 3 = white. palette 1: 1 = white +func newSpriteScene() *ppu.PPU { + p := newSceneWithTile1() + writeVRAM(p, 0x3F10, 0x0F, 0x21, 0x16, 0x30) + writeVRAM(p, 0x3F15, 0x30) + for i := 0; i < 64; i++ { + setSprite(p, i, 0xFF, 0, 0, 0) + } + return p +} + +func checkPixels(t *testing.T, p *ppu.PPU, tests []struct { + x, y int + want uint32 +}) { + t.Helper() + for _, tt := range tests { + if got := pixel(p, tt.x, tt.y); got != tt.want { + t.Errorf("pixel(%d,%d) = %#08x, want %#08x", tt.x, tt.y, got, tt.want) + } + } +} + +type px = struct { + x, y int + want uint32 +} + +func TestSpriteDrawsOneLineBelowOAMY(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 9, 1, 0, 16) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{ + {16, 9, black}, + {16, 10, blue}, + {23, 10, blue}, + {24, 10, black}, + {15, 10, black}, + {16, 11, red}, + {16, 12, black}, + }) +} + +func TestSpriteWithYZeroFirstShowsOnLineOne(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 0, 1, 0, 16) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{{16, 0, black}, {16, 1, blue}}) +} + +func TestSpriteFlipHorizontal(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x0020, 0x80) + setSprite(p, 0, 9, 2, 0, 16) + setSprite(p, 1, 9, 2, 0x40, 64) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{ + {16, 10, blue}, {23, 10, black}, + {64, 10, black}, {71, 10, blue}, + }) +} + +func TestSpriteFlipVertical(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 9, 1, 0x80, 16) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{{16, 10, black}, {16, 16, red}, {16, 17, blue}}) +} + +func TestSpritePaletteSelect(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 9, 1, 0x01, 16) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{{16, 10, white}}) +} + +func TestSpritePatternTableSelect(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x1018, 0xFF) + setSprite(p, 0, 9, 1, 0, 16) + p.WriteRegister(0x2000, 0x08) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + checkPixels(t, p, []px{{16, 10, red}}) +} + +func TestSprite8x16(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x0020, 0xFF) + writeVRAM(p, 0x0030, 0xFF) + writeVRAM(p, 0x0038, 0xFF) + setSprite(p, 0, 9, 2, 0, 16) + p.WriteRegister(0x2000, 0x20) + p.WriteRegister(0x2001, 0x10) + runFrames(p) + + // rows 0-7 come from tile 2, rows 8-15 from tile 3 + checkPixels(t, p, []px{{16, 10, blue}, {16, 11, black}, {16, 18, white}, {16, 26, black}}) +} + +func TestSpritePriority(t *testing.T) { + // background tile 1 at x 16-23: line 1 is color 2 (red), line 2 is transparent + scene := func(attr uint8) *ppu.PPU { + p := newSpriteScene() + writeVRAM(p, 0x2002, 1) + setSprite(p, 0, 0, 1, attr, 16) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + return p + } + + checkPixels(t, scene(0), []px{{16, 1, blue}, {16, 2, red}}) + // behind the background it only shows through transparent pixels + checkPixels(t, scene(0x20), []px{{16, 1, red}, {16, 2, red}}) +} + +func TestSpriteLimitOf8PerLine(t *testing.T) { + p := newSpriteScene() + for i := 0; i < 9; i++ { + setSprite(p, i, 9, 1, 0, uint8(i*12)) + } + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + checkPixels(t, p, []px{{84, 10, blue}, {96, 10, black}}) + if p.ReadRegister(0x2002)&0x20 == 0 { + t.Error("overflow flag not set with 9 sprites on a line") + } +} + +func TestSpriteOverflowNotSetWith8(t *testing.T) { + p := newSpriteScene() + for i := 0; i < 8; i++ { + setSprite(p, i, 9, 1, 0, uint8(i*12)) + } + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + if p.ReadRegister(0x2002)&0x20 != 0 { + t.Error("overflow flag set with only 8 sprites") + } +} + +func TestSpriteLeftClip(t *testing.T) { + scene := func(mask uint8) *ppu.PPU { + p := newSpriteScene() + setSprite(p, 0, 9, 1, 0, 0) + p.WriteRegister(0x2001, mask) + runFrames(p) + return p + } + + checkPixels(t, scene(0x10), []px{{0, 10, black}, {8, 10, black}}) + checkPixels(t, scene(0x14), []px{{0, 10, blue}}) +} + +func TestSpriteHiddenWhenSpritesDisabled(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 9, 1, 0, 16) + p.WriteRegister(0x2001, 0x08) + runFrames(p) + + checkPixels(t, p, []px{{16, 10, black}}) +} + +func sprite0Hit(p *ppu.PPU) bool { + return p.ReadRegister(0x2002)&0x40 != 0 +} + +func TestSprite0HitTiming(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x2002, 1) + setSprite(p, 0, 0, 1, 0, 16) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + + // first overlap is pixel x = 16 on line 1, which is dot 17 + runToDot(p, 1, 10) + if sprite0Hit(p) { + t.Error("hit set before the overlap") + } + runToDot(p, 1, 20) + if !sprite0Hit(p) { + t.Error("hit not set after the overlap") + } + runToDot(p, 261, 2) + if sprite0Hit(p) { + t.Error("hit not cleared on the pre-render line") + } +} + +func TestSprite0HitNeedsOverlap(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x2002, 1) + setSprite(p, 0, 0, 1, 0, 100) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + if sprite0Hit(p) { + t.Error("hit set without a background pixel under sprite 0") + } +} + +func TestSprite0HitOnlyForSprite0(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x2002, 1) + setSprite(p, 1, 0, 1, 0, 16) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + if sprite0Hit(p) { + t.Error("hit set by sprite 1") + } +} + +func TestSprite0HitWhenBehindBackground(t *testing.T) { + p := newSpriteScene() + writeVRAM(p, 0x2002, 1) + setSprite(p, 0, 0, 1, 0x20, 16) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + if !sprite0Hit(p) { + t.Error("hit not set for a sprite 0 behind the background") + } +} + +func TestSprite0HitRespectsLeftClip(t *testing.T) { + scene := func(mask uint8) *ppu.PPU { + p := newSpriteScene() + writeVRAM(p, 0x2000, 1) + setSprite(p, 0, 0, 1, 0, 0) + p.WriteRegister(0x2001, mask) + runFrames(p) + runToDot(p, 50, 0) + return p + } + + if sprite0Hit(scene(0x18)) { + t.Error("hit set inside the clipped left 8 pixels") + } + if !sprite0Hit(scene(0x1E)) { + t.Error("hit not set with the left 8 pixels shown") + } +} + +func TestSprite0HitNeverAtX255(t *testing.T) { + p := newSpriteScene() + setSprite(p, 0, 0, 1, 0, 255) + p.WriteRegister(0x2001, 0x1E) + runFrames(p) + runToDot(p, 50, 0) + + if sprite0Hit(p) { + t.Error("hit set at x = 255") + } +} |