use super::*;
#[test]
fn a_pixel_survives_every_format() {
for format in [
PixelFormat::RGBA8888,
PixelFormat::BGRA8888,
PixelFormat::RGB888,
] {
let mut surface = Surface::new(format, 3, 2);
surface.put(2, 1, [0x12, 0x34, 0x56]);
assert_eq!(
surface.get(2, 1),
Some([0x12, 0x34, 0x56]),
"{format} lost a pixel"
);
assert_eq!(surface.get(0, 0), Some([0, 0, 0]), "{format} smeared");
assert_eq!(surface.stride(), 3 * format.bytes_per_pixel());
assert_eq!(surface.len(), surface.stride() * 2);
}
}
#[test]
fn the_byte_order_is_the_one_the_name_promises() {
let mut rgba = Surface::new(PixelFormat::RGBA8888, 1, 1);
rgba.put(0, 0, [1, 2, 3]);
assert_eq!(rgba.pixels(), &[1, 2, 3, 0xff]);
let mut bgra = Surface::new(PixelFormat::BGRA8888, 1, 1);
bgra.put(0, 0, [1, 2, 3]);
assert_eq!(bgra.pixels(), &[3, 2, 1, 0xff]);
let mut rgb = Surface::new(PixelFormat::RGB888, 1, 1);
rgb.put(0, 0, [1, 2, 3]);
assert_eq!(rgb.pixels(), &[1, 2, 3]);
}
#[test]
fn a_pixel_outside_the_surface_is_ignored_rather_than_fatal() {
let mut surface = Surface::new(PixelFormat::RGBA8888, 2, 2);
surface.put(2, 0, [0xff, 0xff, 0xff]);
surface.put(0, 2, [0xff, 0xff, 0xff]);
surface.put(u32::MAX, u32::MAX, [0xff, 0xff, 0xff]);
assert_eq!(surface.get(9, 9), None);
assert!(surface.pixels().iter().all(|b| *b == 0));
}
#[test]
fn fill_paints_every_pixel_opaque() {
let mut surface = Surface::new(PixelFormat::RGBA8888, 4, 4);
surface.fill([10, 20, 30]);
for y in 0..4 {
for x in 0..4 {
assert_eq!(surface.get(x, y), Some([10, 20, 30]));
}
}
assert!(
surface
.pixels()
.as_chunks::<4>()
.0
.iter()
.all(|p| p[3] == 0xff)
);
}
#[test]
fn reshaping_to_the_same_shape_leaves_the_buffer_alone() {
let mut surface = Surface::new(PixelFormat::RGBA8888, 8, 8);
surface.fill([1, 2, 3]);
let before = surface.as_ptr();
surface.reshape(PixelFormat::RGBA8888, 8, 8);
assert_eq!(surface.as_ptr(), before, "a no-op reshape reallocated");
assert_eq!(surface.get(7, 7), Some([1, 2, 3]));
surface.reshape(PixelFormat::RGB888, 4, 4);
assert_eq!(surface.len(), 4 * 4 * 3);
assert_eq!(surface.format(), PixelFormat::RGB888);
}
#[test]
fn the_frame_hash_follows_the_pixels() {
let mut a = Surface::new(PixelFormat::RGBA8888, 4, 4);
let b = Surface::new(PixelFormat::RGBA8888, 4, 4);
assert_eq!(a.hash(), b.hash());
a.put(3, 3, [0, 0, 1]);
assert_ne!(a.hash(), b.hash(), "one changed pixel must move the hash");
assert_eq!(
Surface::new(PixelFormat::RGBA8888, 0, 0).hash(),
0xcbf2_9ce4_8422_2325
);
}
#[test]
fn a_row_is_stride_bytes_and_stops_at_the_bottom() {
let surface = Surface::new(PixelFormat::RGB888, 5, 3);
assert_eq!(surface.row(2).map(<[u8]>::len), Some(15));
assert!(surface.row(3).is_none());
}
#[cfg(feature = "dev-nes-ppu")]
#[test]
fn a_ppu_frame_becomes_host_pixels() {
use alloc::sync::Arc;
use crate::core::props::Props;
use crate::core::space::{AddressSpace, RamStore, Region as MmioRegion, UnassignedPolicy};
use crate::dev::ppu::{DOTS_PER_FRAME, NesPpu};
use crate::host::display::nes::NesScanout;
use crate::host::display::palette::nes_rgb;
let vram = AddressSpace::new("ppu", 14).with_unassigned(UnassignedPolicy::ONES);
{
let mut topo = vram.topology();
topo.map(
Arc::new(MmioRegion::ram("chr", Arc::new(RamStore::new(0x2000)))),
0x0000,
)
.expect("chr maps");
topo.map(
Arc::new(MmioRegion::ram(
"nametables",
Arc::new(RamStore::new(0x1000)),
)),
0x2000,
)
.expect("nametables map");
}
let ppu = Arc::new(NesPpu::new(&Props::new().with("region", "ntsc")).expect("a ppu"));
ppu.attach_bus(Arc::new(vram));
ppu.poke_palette(0x3f00, 0x21);
let scanout = NesScanout::new(Arc::clone(&ppu));
let mut surface = Surface::for_scanout(&scanout);
assert_eq!((surface.width(), surface.height()), (256, 240));
assert_eq!(scanout.frame_counter(), 0);
ppu.advance_by(DOTS_PER_FRAME);
let serial = scanout.capture(&mut surface);
assert!(serial >= 1, "a frame's worth of dots produced no frame");
assert_eq!(surface.serial(), serial);
let sky = nes_rgb(0x21);
assert_eq!(surface.get(0, 0), Some(sky));
assert_eq!(surface.get(255, 239), Some(sky));
assert_eq!(surface.get(128, 120), Some(sky));
assert_eq!(scanout.frame_period_ns(), 16_639_356);
}
#[cfg(feature = "dev-nes-ppu")]
#[test]
fn a_host_can_ask_for_a_different_byte_order() {
use alloc::sync::Arc;
use crate::core::props::Props;
use crate::dev::ppu::{DOTS_PER_FRAME, NesPpu};
use crate::host::display::nes::NesScanout;
use crate::host::display::palette::nes_rgb;
let ppu = Arc::new(NesPpu::new(&Props::new()).expect("a ppu"));
ppu.poke_palette(0x3f00, 0x16);
ppu.advance_by(DOTS_PER_FRAME);
let scanout = NesScanout::new(ppu);
let mut surface = Surface::new(PixelFormat::BGRA8888, 1, 1);
scanout.capture(&mut surface);
assert_eq!((surface.width(), surface.height()), (256, 240));
let red = nes_rgb(0x16);
assert_eq!(surface.get(10, 10), Some(red));
assert_eq!(&surface.pixels()[..4], &[red[2], red[1], red[0], 0xff]);
}
#[cfg(all(
feature = "machine-nes",
feature = "dev-nes-ppu",
feature = "display-png",
feature = "std"
))]
#[test]
fn a_nes_boots_renders_and_captures_a_png() {
use crate::core::clock::GlobalTime;
use crate::host::display::palette::nes_rgb;
use crate::host::display::{nes, png};
use crate::machine::catalog;
static MINIMAL_NROM: &[u8] = &{
let mut image = [0u8; 16 + 16384 + 8192];
image[0] = b'N';
image[1] = b'E';
image[2] = b'S';
image[3] = 0x1a;
image[4] = 1;
image[5] = 1;
image[16 + 0x3ffc] = 0x00;
image[16 + 0x3ffd] = 0xc0;
image[16] = 0x4c;
image[17] = 0x00;
image[18] = 0xc0;
image
};
let entry = catalog::machine("nes-ntsc").expect("machine-nes is on");
let registry = catalog::registry().expect("a registry");
let mut options = catalog::build_options().expect("build options");
options.realize.media.insert("cart", MINIMAL_NROM);
nes::capture::install(&mut options).expect("the bindings are intercepted");
let mut machine = crate::machine::build(entry.name, entry.source, ®istry, &options)
.expect("the nes-ntsc description builds");
let scanout = nes::capture::take(&options.realize.hosts).expect("the machine has a ppu");
scanout.ppu().poke_palette(0x3f00, 0x21);
let frames = 3;
let period = scanout.frame_period_ns();
assert!(period > 0);
machine
.run_for(GlobalTime::from_nanos(period * frames))
.expect("the machine runs");
let mut surface = Surface::for_scanout(&scanout);
let serial = scanout.capture(&mut surface);
assert!(
serial >= frames - 1,
"only {serial} frames after {frames} frame periods; is the ppu being advanced?"
);
let sky = nes_rgb(0x21);
assert_eq!(surface.get(0, 0), Some(sky));
assert_eq!(surface.get(200, 200), Some(sky));
let expected: u64 = {
let mut reference = Surface::new(surface.format(), surface.width(), surface.height());
reference.fill(sky);
reference.hash()
};
assert_eq!(
surface.hash(),
expected,
"the frame is not the flat backdrop it should be"
);
let bytes = png::encode(&surface).expect("the surface encodes");
assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n");
let decoded = oxideav_png::decode_png(&bytes).expect("what we wrote decodes");
assert_eq!((decoded.width, decoded.height), (256, 240));
assert_eq!(&decoded.data[..3], &sky[..]);
let dir = std::env::var("RSEMU_SCREENSHOT_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir());
let path = dir.join("rsemu-nes-ntsc.png");
std::fs::write(&path, &bytes).expect("the screenshot is written");
println!(
"wrote {} ({} bytes), frame {serial}, hash {:#018x}",
path.display(),
bytes.len(),
surface.hash()
);
let animation = png::encode_animation(&[surface.clone(), surface], 2).expect("apng encodes");
assert_eq!(&animation[..8], b"\x89PNG\r\n\x1a\n");
assert!(animation.len() > bytes.len() / 2);
}
#[cfg(all(
feature = "machine-nes",
feature = "dev-nes-ppu",
feature = "display-png",
feature = "std"
))]
#[test]
fn a_real_cartridge_draws_a_picture() {
use crate::core::clock::GlobalTime;
use crate::host::display::{nes, png};
use crate::machine::catalog;
let Ok(rom) = std::env::var("RSEMU_NES_TEST_ROM") else {
println!("SKIP: set RSEMU_NES_TEST_ROM to an iNES image to run this");
return;
};
let image = std::fs::read(&rom).expect("RSEMU_NES_TEST_ROM is readable");
let entry = catalog::machine("nes-ntsc").expect("machine-nes is on");
let registry = catalog::registry().expect("a registry");
let mut options = catalog::build_options().expect("build options");
options.realize.media.insert("cart", image.as_slice());
nes::capture::install(&mut options).expect("the bindings are intercepted");
let mut machine = crate::machine::build(entry.name, entry.source, ®istry, &options)
.unwrap_or_else(|e| panic!("{rom}: {e}"));
let scanout = nes::capture::take(&options.realize.hosts).expect("the machine has a ppu");
let period = scanout.frame_period_ns();
let mut surface = Surface::for_scanout(&scanout);
let mut frames = Vec::new();
let dir = std::env::var("RSEMU_SCREENSHOT_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir());
for frame in 0..180u64 {
machine
.run_for(GlobalTime::from_nanos(period))
.expect("the machine runs");
if frame % 30 == 29 {
scanout.capture(&mut surface);
let bytes = png::encode(&surface).expect("the surface encodes");
let path = dir.join(format!("rsemu-nes-frame{frame:03}.png"));
std::fs::write(&path, &bytes).expect("the screenshot is written");
println!(
"wrote {} ({} bytes), hash {:#018x}",
path.display(),
bytes.len(),
surface.hash()
);
frames.push(surface.clone());
}
}
assert!(scanout.frame_counter() >= 179, "the ppu is not advancing");
let animation = png::encode_animation(&frames, 50).expect("apng encodes");
let path = dir.join("rsemu-nes-frames.png");
std::fs::write(&path, &animation).expect("the animation is written");
println!("wrote {} ({} bytes)", path.display(), animation.len());
}