//! Row-at-once approximation of the one-line-ahead OBJ pipeline. //! OAM and VRAM sample at cycle40 of the preceding line. This does not model //! individual fetches, fetch budgets, or OAM/VRAM contention. use super::PreparedObjects; use crate::snapshot::{Reader, Snapshot, SnapshotError, Writer}; use crate::{display::CYCLES_PER_LINE, video::VideoError}; pub(crate) const PREPARE_CYCLE: u32 = 40; #[derive(Debug, Default, PartialEq, Eq)] pub(crate) struct SpritePipeline { rows: [Option<(usize, Result)>; 2], pub started: bool, } impl SpritePipeline { pub fn store(&mut self, row: usize, objects: Result) { self.rows[row % 2] = Some((row, objects)); } pub fn row(&self, row: usize) -> &Result { let (prepared_row, objects) = self.rows[row % 2] .as_ref() .expect("sprite row prepared before capture"); assert_eq!(*prepared_row, row); objects } } /// Only rows0..158 and227 prepare the following visible row. pub(crate) fn target(scanline: u16) -> Option { match scanline { 0..=158 => Some(usize::from(scanline) + 1), 227 => Some(0), _ => None, } } pub(crate) fn next_event(scanline: u16, cycle: u16) -> u32 { let (line, cycle) = (u32::from(scanline), u32::from(cycle)); if target(scanline).is_some() && cycle < PREPARE_CYCLE { return PREPARE_CYCLE - cycle; } let lines = if (158..227).contains(&line) { 227 - line } else { 1 }; lines * CYCLES_PER_LINE + PREPARE_CYCLE - cycle } /// Latest two events suffice when capture is disabled: fixed memory/registers /// cannot change inside a clock batch, and only two prepared rows are retained. /// Offsets are relative to the supplied starting phase, in chronological order. pub(crate) fn latest_events(phase: u32, cycles: u32) -> [Option<(u32, usize)>; 2] { let start = u64::from(phase); let end = start + u64::from(cycles); let latest = |end: u64| { let mut line = end.checked_sub(u64::from(PREPARE_CYCLE))? / u64::from(CYCLES_PER_LINE); let row = line % 228; if (159..227).contains(&row) { line -= row - 158; } let time = line * u64::from(CYCLES_PER_LINE) + u64::from(PREPARE_CYCLE); (time > start).then_some((time, ((line + 1) % 228) as usize)) }; let Some((last, row)) = latest(end) else { return [None; 2]; }; let previous = latest(last - 1).map(|(time, row)| ((time - start) as u32, row)); [previous, Some(((last - start) as u32, row))] } #[cfg(test)] mod tests { use super::*; use crate::display::CYCLES_PER_FRAME; #[test] fn event_selection_matches_independent_line_iteration() { let mut seed = 0x6328_93abu32; for case in 0..1000 { let mut next = || { seed = seed.wrapping_mul(1664525).wrapping_add(1013904223); seed }; let phase = next() % CYCLES_PER_FRAME; let cycles = if case == 0 { u32::MAX } else { next() % (CYCLES_PER_FRAME * 5) }; let end = u64::from(phase) + u64::from(cycles); let mut expected = [None; 2]; for line in u64::from(phase / CYCLES_PER_LINE)..=end / u64::from(CYCLES_PER_LINE) { let time = line * u64::from(CYCLES_PER_LINE) + 40; if time > u64::from(phase) && time <= end && (line % 228 < 159 || line % 228 == 227) { expected[0] = expected[1]; expected[1] = Some(( (time - u64::from(phase)) as u32, ((line + 1) % 228) as usize, )); } } assert_eq!(latest_events(phase, cycles), expected); } } #[test] fn next_event_handles_visible_end_hidden_lines_and_exact_boundary() { for line in 0..228 { for cycle in [0, 39, 40, 1006, 1231] { let phase = u32::from(line) * CYCLES_PER_LINE + u32::from(cycle); let distance = next_event(line, cycle); assert!(distance > 0); assert_eq!(latest_events(phase, distance - 1), [None; 2]); assert!(latest_events(phase, distance)[1].is_some()); } } } } impl Snapshot for SpritePipeline { fn save(&self, writer: &mut Writer) { let Self { rows, started } = self; writer.tag(b"SPRP"); writer.bool(*started); for row in rows { writer.option(row.as_ref(), |(index, result), writer| { if *index >= crate::video::HEIGHT { writer.fail(SnapshotError::Unsupported("invalid sprite row index")); } writer.u64(*index as u64); match result { Ok(objects) => { writer.u8(0); objects.save(writer); } Err(_) => { writer.u8(1); writer.fail(SnapshotError::Unsupported("deferred video diagnostic")); } } }); } } fn load(&mut self, reader: &mut Reader<'_>) -> Result<(), SnapshotError> { let Self { rows, started } = self; reader.tag(b"SPRP")?; *started = reader.bool()?; for (slot, destination) in rows.iter_mut().enumerate() { *destination = reader.option(|reader| { let index = usize::try_from(reader.u64()?) .map_err(|_| SnapshotError::Corrupt("sprite row index"))?; if index >= crate::video::HEIGHT || index % 2 != slot { return Err(SnapshotError::Corrupt("sprite row index")); } let result = match reader.u8()? { 0 => { let mut objects = PreparedObjects::default(); objects.load(reader)?; Ok(objects) } 1 => return Err(SnapshotError::Corrupt("deferred video diagnostic")), _ => return Err(SnapshotError::Corrupt("sprite row result")), }; Ok((index, result)) })?; } Ok(()) } } #[cfg(test)] mod snapshot_tests { use super::*; use crate::video::sprites::{ObjectPixel, Sample}; fn encoded(value: &SpritePipeline) -> Vec { let mut writer = Writer::new(); value.save(&mut writer); writer.finish().unwrap() } fn prepared(index: usize) -> PreparedObjects { PreparedObjects { samples: vec![ Sample { pixel: Some(ObjectPixel { palette_index: (index % 256) as u16, semi_transparent: true, }), priority: (index % 4) as u8, mosaic: true, }; crate::video::WIDTH ], window: vec![index % 2 == 0; crate::video::WIDTH], } } #[test] fn sprite_pipeline_round_trips_rows_and_checks_prefixes() { let source = SpritePipeline { rows: [Some((4, Ok(prepared(13)))), Some((5, Ok(prepared(17))))], started: true, }; let bytes = encoded(&source); let mut restored = SpritePipeline::default(); restored.load(&mut Reader::new(&bytes)).unwrap(); assert_eq!(restored, source); for end in 0..bytes.len() { assert!( SpritePipeline::default() .load(&mut Reader::new(&bytes[..end])) .is_err(), "prefix {end}" ); } let mut invalid = bytes.clone(); invalid[4] = 2; assert_eq!( SpritePipeline::default().load(&mut Reader::new(&invalid)), Err(SnapshotError::Corrupt("boolean")) ); invalid.copy_from_slice(&bytes); invalid[6] = crate::video::HEIGHT as u8; invalid[7..14].fill(0); assert_eq!( SpritePipeline::default().load(&mut Reader::new(&invalid)), Err(SnapshotError::Corrupt("sprite row index")) ); } #[test] fn deferred_sprite_diagnostic_cannot_be_saved() { let mut source = SpritePipeline::default(); source.rows[0] = Some((0, Err(VideoError::UnsupportedMode(7)))); let mut writer = Writer::new(); source.save(&mut writer); assert_eq!( writer.finish(), Err(SnapshotError::Unsupported("deferred video diagnostic")) ); } }