190 lines
7 KiB
Rust
190 lines
7 KiB
Rust
//! Vertical mosaic phase for row capture. Size writes preserve four-bit counters.
|
|
//! Background phase follows the displayed row. OBJ phase runs one row ahead
|
|
//! for sprite preparation. Per-access size latching is not modeled.
|
|
|
|
use crate::display::{CYCLES_PER_FRAME, CYCLES_PER_LINE, VISIBLE_LINES};
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct VerticalMosaic {
|
|
pub background: u8,
|
|
pub object: u8,
|
|
started: bool,
|
|
}
|
|
|
|
impl VerticalMosaic {
|
|
pub fn advance(&mut self, phase: u32, cycles: u32, sizes: u16) {
|
|
if cycles == 0 {
|
|
return;
|
|
}
|
|
let object_height = u32::from(sizes >> 12) + 1;
|
|
if !self.started {
|
|
// Reset has no preceding row227. Row0 uses phase0; row1 uses the next phase.
|
|
self.object = u8::from(object_height != 1);
|
|
self.started = true;
|
|
}
|
|
self.background = progress(
|
|
phase,
|
|
cycles,
|
|
self.background,
|
|
u32::from((sizes >> 4) & 15) + 1,
|
|
)
|
|
.counter;
|
|
self.object = progress(
|
|
(phase + CYCLES_PER_LINE) % CYCLES_PER_FRAME,
|
|
cycles,
|
|
self.object,
|
|
object_height,
|
|
)
|
|
.counter;
|
|
}
|
|
}
|
|
|
|
pub(super) struct Progress {
|
|
pub counter: u8,
|
|
/// Number of zero transitions since the last frame reload, if any.
|
|
pub resets: u32,
|
|
pub first_reset: bool,
|
|
pub frame_reloaded: bool,
|
|
pub crossed_line: bool,
|
|
}
|
|
|
|
/// Summarize fixed-size line transitions without iterating over lines or frames.
|
|
/// A frame wrap discards earlier history because affine origins reload at row0.
|
|
pub(super) fn progress(phase: u32, cycles: u32, counter: u8, height: u32) -> Progress {
|
|
debug_assert!(counter < 16 && (1..=16).contains(&height));
|
|
let end = u64::from(phase) + u64::from(cycles);
|
|
let wrapped = end >= u64::from(CYCLES_PER_FRAME);
|
|
let first = if wrapped { 0 } else { phase / CYCLES_PER_LINE };
|
|
let last = (end % u64::from(CYCLES_PER_FRAME)) as u32 / CYCLES_PER_LINE;
|
|
let initial = if wrapped { 0 } else { u32::from(counter) };
|
|
let first_reset = first < VISIBLE_LINES
|
|
&& (first == VISIBLE_LINES - 1 || initial + 1 == height || initial == 15);
|
|
// Normal increments enter rows1..159. Entry to row160 unconditionally resets.
|
|
let lines = last.min(VISIBLE_LINES - 1) - first.min(VISIBLE_LINES - 1);
|
|
let until_zero = if initial < height {
|
|
height - initial
|
|
} else {
|
|
16 - initial
|
|
};
|
|
let (mut current, mut resets) = if lines < until_zero {
|
|
(initial + lines, 0)
|
|
} else {
|
|
let remaining = lines - until_zero;
|
|
(remaining % height, 1 + remaining / height)
|
|
};
|
|
if first < VISIBLE_LINES && last >= VISIBLE_LINES {
|
|
current = 0;
|
|
resets += 1;
|
|
}
|
|
Progress {
|
|
counter: current as u8,
|
|
resets,
|
|
first_reset,
|
|
frame_reloaded: wrapped,
|
|
crossed_line: wrapped || first != last,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn every_counter_height_and_visible_line_matches_iterated_transitions() {
|
|
for counter in 0..16 {
|
|
for height in 1..=16 {
|
|
for first in 0..160 {
|
|
let mut expected = counter;
|
|
let mut resets = 0;
|
|
for last in first + 1..=160 {
|
|
let next = expected + 1;
|
|
expected = if last == 160 || next == height {
|
|
0
|
|
} else {
|
|
next & 15
|
|
};
|
|
resets += u32::from(expected == 0);
|
|
let p = progress(
|
|
first * CYCLES_PER_LINE,
|
|
(last - first) * CYCLES_PER_LINE,
|
|
counter as u8,
|
|
height,
|
|
);
|
|
assert_eq!(
|
|
u32::from(p.counter),
|
|
expected,
|
|
"counter={counter} height={height} first={first} last={last}"
|
|
);
|
|
assert_eq!(p.resets, resets);
|
|
assert_eq!(
|
|
p.first_reset,
|
|
first == 159 || counter + 1 == height || counter == 15
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn random_clock_batches_and_maximum_batch_match_line_iteration() {
|
|
let mut seed = 0x3712_bc09u32;
|
|
for case in 0..500 {
|
|
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() % (7 * CYCLES_PER_FRAME)
|
|
};
|
|
let height = next() % 16 + 1;
|
|
let counter = if phase / CYCLES_PER_LINE >= 160 {
|
|
0
|
|
} else {
|
|
(next() & 15) as u8
|
|
};
|
|
let mut expected = counter;
|
|
let mut resets = 0;
|
|
for boundary in u64::from(phase / CYCLES_PER_LINE) + 1
|
|
..=(u64::from(phase) + u64::from(cycles)) / u64::from(CYCLES_PER_LINE)
|
|
{
|
|
let row = boundary % 228;
|
|
if row == 0 {
|
|
expected = 0;
|
|
resets = 0;
|
|
} else if row <= 160 {
|
|
let value = u32::from(expected) + 1;
|
|
expected = if row == 160 || value == height {
|
|
0
|
|
} else {
|
|
(value & 15) as u8
|
|
};
|
|
resets += u32::from(expected == 0);
|
|
}
|
|
}
|
|
let p = progress(phase, cycles, counter, height);
|
|
assert_eq!(p.counter, expected, "phase={phase} cycles={cycles}");
|
|
assert_eq!(p.resets, resets);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn size_shrink_preserves_counter_until_four_bit_wrap_and_zero_cycles_do_nothing() {
|
|
let mut mosaic = VerticalMosaic::default();
|
|
mosaic.advance(0, 6 * CYCLES_PER_LINE, 0x3070); // BG8, OBJ4.
|
|
assert_eq!((mosaic.background, mosaic.object), (6, 3)); // OBJ prepares row7.
|
|
mosaic.advance(6 * CYCLES_PER_LINE, 0, 0);
|
|
assert_eq!((mosaic.background, mosaic.object), (6, 3));
|
|
mosaic.advance(6 * CYCLES_PER_LINE, 9 * CYCLES_PER_LINE, 0x1010); // BG2, OBJ2.
|
|
assert_eq!((mosaic.background, mosaic.object), (15, 12));
|
|
mosaic.advance(15 * CYCLES_PER_LINE, CYCLES_PER_LINE, 0x1010);
|
|
assert_eq!((mosaic.background, mosaic.object), (0, 13));
|
|
mosaic.advance(16 * CYCLES_PER_LINE, 144 * CYCLES_PER_LINE, 0xffff);
|
|
assert_eq!((mosaic.background, mosaic.object), (0, 0));
|
|
mosaic.advance(160 * CYCLES_PER_LINE, 68 * CYCLES_PER_LINE, 0xffff);
|
|
assert_eq!((mosaic.background, mosaic.object), (0, 1)); // Row1 phase, row0 already prepared.
|
|
}
|
|
}
|