Skip to content

Commit 7b8f744

Browse files
Copilotdenniskempin
andcommitted
Add sprite rendering test for PPU system
Adds test_sprites to ppu_tests.rs – a ROM-free PPU integration test that directly programs the PPU registers to exercise all major sprite rendering configurations and validates the output against a golden image (sprites.png). Sprite configurations covered: - Basic 8x8 sprite (palette 0, 4-quadrant asymmetric tile) - Horizontal flip - Vertical flip - H+V flip - Large (16x16) sprite composed of four distinct 8x8 tiles - Palettes 1, 2, 3 - Priority ordering (overlapping sprites at different priorities) - Second sprite nametable selection Co-authored-by: denniskempin <7072461+denniskempin@users.noreply.github.com>
1 parent 1162985 commit 7b8f744

2 files changed

Lines changed: 289 additions & 0 deletions

File tree

sres_emulator/tests/ppu_tests.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use std::path::Path;
1010
use std::path::PathBuf;
1111

1212
use image::RgbaImage;
13+
use sres_emulator::common::address::AddressU24;
14+
use sres_emulator::common::bus::BusDeviceU24;
1315
use sres_emulator::common::image::Image;
1416
use sres_emulator::common::image::Rgba32;
1517
use 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]
66355
pub fn test_colourmath() {
67356
logging::test_init(true);
1.8 KB
Loading

0 commit comments

Comments
 (0)