@@ -10,6 +10,8 @@ use std::path::Path;
1010use std:: path:: PathBuf ;
1111
1212use image:: RgbaImage ;
13+ use sres_emulator:: common:: address:: AddressU24 ;
14+ use sres_emulator:: common:: bus:: BusDeviceU24 ;
1315use sres_emulator:: common:: image:: Image ;
1416use sres_emulator:: common:: image:: Rgba32 ;
1517use sres_emulator:: common:: logging;
@@ -62,6 +64,293 @@ pub fn test_krom_interlace_rpg() {
6264 run_framebuffer_test ( "krom_interlace_rpg" , 10 ) ;
6365}
6466
67+ /// Tests sprite rendering by directly programming the PPU (no ROM required).
68+ ///
69+ /// Exercises the following configurations in two rows of sprites:
70+ ///
71+ /// Row 1 (Y=8):
72+ /// Sprite 0: basic 8×8, palette 0
73+ /// Sprite 1: horizontal flip
74+ /// Sprite 2: vertical flip
75+ /// Sprite 3: H+V flip
76+ /// Sprite 4: large (16×16), composed of four distinct solid-colour tiles
77+ ///
78+ /// Row 2 (Y=32):
79+ /// Sprite 5: palette 1
80+ /// Sprite 6: palette 2
81+ /// Sprite 7: palette 3
82+ /// Sprites 8+9: priority overlap (priority-0 sprite partially covered by priority-3)
83+ /// Sprite 10: tile from nametable 1 (second sprite table)
84+ #[ test]
85+ pub fn test_sprites ( ) {
86+ logging:: test_init ( true ) ;
87+ let mut ppu = Ppu :: new ( ) ;
88+ setup_sprite_test ( & mut ppu) ;
89+ for scanline in 0 ..224 {
90+ ppu. draw_scanline ( scanline) ;
91+ }
92+ compare_to_golden (
93+ & ppu. framebuffer ( ) . to_rgba :: < TestImageImpl > ( ) ,
94+ & test_dir ( ) . join ( "sprites" ) ,
95+ ) ;
96+ }
97+
98+ /// Writes a single byte to a PPU register.
99+ fn ppu_write ( ppu : & mut Ppu , reg : u16 , value : u8 ) {
100+ ppu. write ( AddressU24 :: new ( 0 , reg) , value) ;
101+ }
102+
103+ /// Writes a complete 4bpp 8×8 tile to VRAM.
104+ ///
105+ /// `word_addr` is the VRAM word address for tile row 0.
106+ /// `planes[row]` = [plane0, plane1, plane2, plane3] byte values for that row.
107+ ///
108+ /// Requires VMAIN = 0x80 (increment after writing the high byte) to already be set.
109+ ///
110+ /// SNES 4bpp layout: 8 words of (plane0, plane1) at `word_addr + 0..7`, followed
111+ /// by 8 words of (plane2, plane3) at `word_addr + 8..15`.
112+ /// Bit 7 of each plane byte is the leftmost pixel; bit 0 is the rightmost.
113+ fn write_tile_4bpp ( ppu : & mut Ppu , word_addr : u16 , planes : [ [ u8 ; 4 ] ; 8 ] ) {
114+ // Low bit-planes (0 & 1), rows 0-7
115+ ppu_write ( ppu, 0x2116 , ( word_addr & 0xFF ) as u8 ) ;
116+ ppu_write ( ppu, 0x2117 , ( word_addr >> 8 ) as u8 ) ;
117+ for row in & planes {
118+ ppu_write ( ppu, 0x2118 , row[ 0 ] ) ; // plane 0
119+ ppu_write ( ppu, 0x2119 , row[ 1 ] ) ; // plane 1 – address increments after this
120+ }
121+ // High bit-planes (2 & 3) sit 8 words later in VRAM
122+ let hi_addr = word_addr + 8 ;
123+ ppu_write ( ppu, 0x2116 , ( hi_addr & 0xFF ) as u8 ) ;
124+ ppu_write ( ppu, 0x2117 , ( hi_addr >> 8 ) as u8 ) ;
125+ for row in & planes {
126+ ppu_write ( ppu, 0x2118 , row[ 2 ] ) ; // plane 2
127+ ppu_write ( ppu, 0x2119 , row[ 3 ] ) ; // plane 3
128+ }
129+ }
130+
131+ /// Returns a tile where every pixel has the given 4bpp colour index (1-15).
132+ fn solid_tile ( color : u8 ) -> [ [ u8 ; 4 ] ; 8 ] {
133+ let row = [
134+ if color & 1 != 0 { 0xFF } else { 0x00 } ,
135+ if color & 2 != 0 { 0xFF } else { 0x00 } ,
136+ if color & 4 != 0 { 0xFF } else { 0x00 } ,
137+ if color & 8 != 0 { 0xFF } else { 0x00 } ,
138+ ] ;
139+ [ row; 8 ]
140+ }
141+
142+ /// Programs the PPU with sprite data for `test_sprites`.
143+ fn setup_sprite_test ( ppu : & mut Ppu ) {
144+ // Enable OBJ on the main screen (TM bit 4).
145+ ppu_write ( ppu, 0x212C , 0x10 ) ;
146+
147+ // OBJSEL: small = 8×8, large = 16×16.
148+ // Nametable 0 base = word 0x0000; nametable 1 base = word 0x1000
149+ // (name-select bits = 0b00, so offset = (0+1)×0x1000).
150+ ppu_write ( ppu, 0x2101 , 0x00 ) ;
151+
152+ // -----------------------------------------------------------------
153+ // CGRAM – four sprite palettes (CGRAM indices 128-191)
154+ // SNES colour format: 0b.BBBBB_GGGGG_RRRRR (15-bit BGR, little-endian)
155+ // -----------------------------------------------------------------
156+
157+ // Backdrop (index 0): black
158+ ppu_write ( ppu, 0x2121 , 0 ) ;
159+ ppu_write ( ppu, 0x2122 , 0x00 ) ;
160+ ppu_write ( ppu, 0x2122 , 0x00 ) ;
161+
162+ // Palette 0 (indices 128-132): transparent, red, green, blue, yellow
163+ ppu_write ( ppu, 0x2121 , 128 ) ;
164+ for ( lo, hi) in [
165+ ( 0x00u8 , 0x00u8 ) , // colour 0: transparent
166+ ( 0x1F , 0x00 ) , // colour 1: red (R=31)
167+ ( 0xE0 , 0x03 ) , // colour 2: green (G=31)
168+ ( 0x00 , 0x7C ) , // colour 3: blue (B=31)
169+ ( 0xFF , 0x03 ) , // colour 4: yellow (R=31, G=31)
170+ ] {
171+ ppu_write ( ppu, 0x2122 , lo) ;
172+ ppu_write ( ppu, 0x2122 , hi) ;
173+ }
174+
175+ // Palette 1 (indices 144-148): transparent, cyan, magenta, white, gray
176+ ppu_write ( ppu, 0x2121 , 144 ) ;
177+ for ( lo, hi) in [
178+ ( 0x00u8 , 0x00u8 ) , // colour 0: transparent
179+ ( 0xE0 , 0x7F ) , // colour 1: cyan (G=31, B=31)
180+ ( 0x1F , 0x7C ) , // colour 2: magenta (R=31, B=31)
181+ ( 0xFF , 0x7F ) , // colour 3: white (R=31, G=31, B=31)
182+ ( 0x10 , 0x42 ) , // colour 4: gray (R=16, G=16, B=16)
183+ ] {
184+ ppu_write ( ppu, 0x2122 , lo) ;
185+ ppu_write ( ppu, 0x2122 , hi) ;
186+ }
187+
188+ // Palette 2 (indices 160-161): colour 0 = transparent, colour 1 = orange (R=31, G=16)
189+ ppu_write ( ppu, 0x2121 , 160 ) ;
190+ for ( lo, hi) in [ ( 0x00u8 , 0x00u8 ) , ( 0x1F , 0x02 ) ] {
191+ ppu_write ( ppu, 0x2122 , lo) ;
192+ ppu_write ( ppu, 0x2122 , hi) ;
193+ }
194+
195+ // Palette 3 (indices 176-177): colour 0 = transparent, colour 1 = pink (R=31, B=16)
196+ ppu_write ( ppu, 0x2121 , 176 ) ;
197+ for ( lo, hi) in [ ( 0x00u8 , 0x00u8 ) , ( 0x1F , 0x40 ) ] {
198+ ppu_write ( ppu, 0x2122 , lo) ;
199+ ppu_write ( ppu, 0x2122 , hi) ;
200+ }
201+
202+ // -----------------------------------------------------------------
203+ // VRAM – tile graphics
204+ // -----------------------------------------------------------------
205+
206+ // VMAIN = 0x80: increment VRAM address after writing the high byte.
207+ ppu_write ( ppu, 0x2115 , 0x80 ) ;
208+
209+ // Tile 0 at nametable 0 (word address 0x0000): asymmetric 4-quadrant pattern.
210+ //
211+ // Top-left 4×4 px: colour 1 Top-right 4×4 px: colour 2
212+ // Bot-left 4×4 px: colour 3 Bot-right 4×4 px: colour 4
213+ //
214+ // Bit 7 = leftmost pixel; bits 7-4 = left half (0xF0), bits 3-0 = right half (0x0F).
215+ //
216+ // Top rows: left = colour 1 (0b0001), right = colour 2 (0b0010)
217+ // plane0: left bits set → 0xF0
218+ // plane1: right bits set → 0x0F
219+ // plane2/3: zero
220+ //
221+ // Bottom rows: left = colour 3 (0b0011), right = colour 4 (0b0100)
222+ // plane0: left bits set → 0xF0
223+ // plane1: left bits set → 0xF0
224+ // plane2: right bits set → 0x0F
225+ // plane3: zero
226+ let top_row = [ 0xF0u8 , 0x0F , 0x00 , 0x00 ] ;
227+ let bot_row = [ 0xF0u8 , 0xF0 , 0x0F , 0x00 ] ;
228+ write_tile_4bpp (
229+ ppu,
230+ 0x0000 ,
231+ [
232+ top_row, top_row, top_row, top_row, bot_row, bot_row, bot_row, bot_row,
233+ ] ,
234+ ) ;
235+
236+ // For the 16×16 large sprite (sprite 4, tile index 0):
237+ // The SNES engine maps a 16×16 sprite starting at tile T as:
238+ // tile T (top-left), tile T+1 (top-right)
239+ // tile T+16 (bot-left), tile T+17 (bot-right)
240+ write_tile_4bpp ( ppu, 0x0010 , solid_tile ( 2 ) ) ; // tile 1 → solid green
241+ write_tile_4bpp ( ppu, 0x0100 , solid_tile ( 3 ) ) ; // tile 16 → solid blue
242+ write_tile_4bpp ( ppu, 0x0110 , solid_tile ( 4 ) ) ; // tile 17 → solid yellow
243+
244+ // Nametable 1, tile 0 (word address 0x1000): solid colour 1.
245+ // Used by sprite 10 to verify nametable selection.
246+ write_tile_4bpp ( ppu, 0x1000 , solid_tile ( 1 ) ) ;
247+
248+ // -----------------------------------------------------------------
249+ // OAM – sprite attributes
250+ // -----------------------------------------------------------------
251+
252+ // OAM attribute byte format (byte 3 of each 4-byte sprite record):
253+ // vflip[7] | hflip[6] | priority[5:4] | palette[3:1] | nametable[0]
254+
255+ // Initialise all 128 sprites to off-screen (Y=240) so unused slots are hidden.
256+ ppu_write ( ppu, 0x2102 , 0x00 ) ; // OAMADDL
257+ ppu_write ( ppu, 0x2103 , 0x00 ) ; // OAMADDH
258+ for _ in 0 ..128 {
259+ ppu_write ( ppu, 0x2104 , 0 ) ; // X
260+ ppu_write ( ppu, 0x2104 , 240 ) ; // Y = 240 (off-screen for 224-line display)
261+ ppu_write ( ppu, 0x2104 , 0 ) ; // tile
262+ ppu_write ( ppu, 0x2104 , 0 ) ; // attributes
263+ }
264+
265+ // Reset OAM write address and write the test sprites.
266+ ppu_write ( ppu, 0x2102 , 0x00 ) ;
267+ ppu_write ( ppu, 0x2103 , 0x00 ) ;
268+
269+ // -- Row 1 (Y=8): flip and size tests --------------------------------
270+
271+ // Sprite 0: basic 8×8, palette 0, priority 3, no flip, at (8, 8)
272+ ppu_write ( ppu, 0x2104 , 8 ) ;
273+ ppu_write ( ppu, 0x2104 , 8 ) ;
274+ ppu_write ( ppu, 0x2104 , 0 ) ;
275+ ppu_write ( ppu, 0x2104 , 0x30 ) ; // pri=3, pal=0
276+
277+ // Sprite 1: horizontal flip, at (24, 8)
278+ ppu_write ( ppu, 0x2104 , 24 ) ;
279+ ppu_write ( ppu, 0x2104 , 8 ) ;
280+ ppu_write ( ppu, 0x2104 , 0 ) ;
281+ ppu_write ( ppu, 0x2104 , 0x70 ) ; // hflip=1, pri=3, pal=0
282+
283+ // Sprite 2: vertical flip, at (40, 8)
284+ ppu_write ( ppu, 0x2104 , 40 ) ;
285+ ppu_write ( ppu, 0x2104 , 8 ) ;
286+ ppu_write ( ppu, 0x2104 , 0 ) ;
287+ ppu_write ( ppu, 0x2104 , 0xB0 ) ; // vflip=1, pri=3, pal=0
288+
289+ // Sprite 3: both flips, at (56, 8)
290+ ppu_write ( ppu, 0x2104 , 56 ) ;
291+ ppu_write ( ppu, 0x2104 , 8 ) ;
292+ ppu_write ( ppu, 0x2104 , 0 ) ;
293+ ppu_write ( ppu, 0x2104 , 0xF0 ) ; // hflip=vflip=1, pri=3, pal=0
294+
295+ // Sprite 4: large (16×16), tile 0, palette 0, priority 3, at (80, 8)
296+ // Top-left tile = tile 0 (quadrant pattern), top-right = tile 1 (green),
297+ // bottom-left = tile 16 (blue), bottom-right = tile 17 (yellow).
298+ ppu_write ( ppu, 0x2104 , 80 ) ;
299+ ppu_write ( ppu, 0x2104 , 8 ) ;
300+ ppu_write ( ppu, 0x2104 , 0 ) ;
301+ ppu_write ( ppu, 0x2104 , 0x30 ) ; // pri=3, pal=0
302+
303+ // -- Row 2 (Y=32): palette, priority, and nametable tests -----------
304+
305+ // Sprite 5: palette 1, at (8, 32)
306+ ppu_write ( ppu, 0x2104 , 8 ) ;
307+ ppu_write ( ppu, 0x2104 , 32 ) ;
308+ ppu_write ( ppu, 0x2104 , 0 ) ;
309+ ppu_write ( ppu, 0x2104 , 0x32 ) ; // pri=3, pal=1
310+
311+ // Sprite 6: palette 2, at (24, 32)
312+ ppu_write ( ppu, 0x2104 , 24 ) ;
313+ ppu_write ( ppu, 0x2104 , 32 ) ;
314+ ppu_write ( ppu, 0x2104 , 0 ) ;
315+ ppu_write ( ppu, 0x2104 , 0x34 ) ; // pri=3, pal=2
316+
317+ // Sprite 7: palette 3, at (40, 32)
318+ ppu_write ( ppu, 0x2104 , 40 ) ;
319+ ppu_write ( ppu, 0x2104 , 32 ) ;
320+ ppu_write ( ppu, 0x2104 , 0 ) ;
321+ ppu_write ( ppu, 0x2104 , 0x36 ) ; // pri=3, pal=3
322+
323+ // Priority overlap: sprite 8 (low priority) partially hidden by sprite 9 (high priority).
324+ // At pixels x=60..63 sprite 9 (which has a later OAM index and higher priority) wins.
325+
326+ // Sprite 8: priority 0, palette 0, at (56, 32)
327+ ppu_write ( ppu, 0x2104 , 56 ) ;
328+ ppu_write ( ppu, 0x2104 , 32 ) ;
329+ ppu_write ( ppu, 0x2104 , 0 ) ;
330+ ppu_write ( ppu, 0x2104 , 0x00 ) ; // pri=0, pal=0
331+
332+ // Sprite 9: priority 3, palette 1, at (60, 32) – overlaps sprite 8 by 4 pixels
333+ ppu_write ( ppu, 0x2104 , 60 ) ;
334+ ppu_write ( ppu, 0x2104 , 32 ) ;
335+ ppu_write ( ppu, 0x2104 , 0 ) ;
336+ ppu_write ( ppu, 0x2104 , 0x32 ) ; // pri=3, pal=1
337+
338+ // Sprite 10: nametable 1 tile 0 (solid colour 1), palette 0, priority 3, at (80, 32).
339+ // Tile data is at VRAM word 0x1000; nametable 0 tile 0 is at 0x0000 (quadrant pattern)
340+ // so the two will look different, confirming correct nametable selection.
341+ ppu_write ( ppu, 0x2104 , 80 ) ;
342+ ppu_write ( ppu, 0x2104 , 32 ) ;
343+ ppu_write ( ppu, 0x2104 , 0 ) ;
344+ ppu_write ( ppu, 0x2104 , 0x31 ) ; // pri=3, pal=0, nametable=1
345+
346+ // OAM high table (1 byte per 4 sprites; bits: size3,x8_3, size2,x8_2, size1,x8_1, size0,x8_0)
347+ // Only sprite 4 needs the large-size bit set (high-table byte 1, bit 1).
348+ ppu_write ( ppu, 0x2102 , 0x00 ) ;
349+ ppu_write ( ppu, 0x2103 , 0x01 ) ; // select high table (OAM address 0x200)
350+ ppu_write ( ppu, 0x2104 , 0x00 ) ; // sprites 0-3: all small
351+ ppu_write ( ppu, 0x2104 , 0x02 ) ; // sprites 4-7: sprite 4 large (bit 1), rest small
352+ }
353+
65354#[ test]
66355pub fn test_colourmath ( ) {
67356 logging:: test_init ( true ) ;
0 commit comments