aboutsummaryrefslogtreecommitdiff
path: root/tests/ppu_sprite_test.go
diff options
context:
space:
mode:
authorcrecher <crecher@crecher.net>2026-10-13 01:13:13 +0200
committercrecher <crecher@crecher.net>2026-10-13 01:13:13 +0200
commit99cae5a5a37f41f878a84857361eda47910303ba (patch)
tree31c8665ca991f95e09e93bb2e6ec91d81d2427ff /tests/ppu_sprite_test.go
parent96ff6d8eda2ba434682909f4ff4ce350d78b2273 (diff)
downloadgones-99cae5a5a37f41f878a84857361eda47910303ba.tar.gz
gones-99cae5a5a37f41f878a84857361eda47910303ba.zip
sprites!!!!!!
Diffstat (limited to 'tests/ppu_sprite_test.go')
-rw-r--r--tests/ppu_sprite_test.go291
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")
+ }
+}