Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10011,6 +10011,87 @@ fn render_input_refill_from_bus_matches_fresh_snapshot() {
);
}

#[test]
fn line_without_captured_fetch_paints_no_playfield() {
// Agnus arms a line's fetch run at the DDFSTRT comparator match; a
// BPLCON0 raised mid-line only after that point starts no run until the
// next line. The DMA capture records which lines ran, so a line with no
// captured fetch fetched nothing and the renderer must not synthesize a
// picture for it from the register-derived window (regression: the
// Rampage bottom-scroller band entry painted a phantom, word-skewed
// copy of the first bitmap row one line above the real first fetch row).
let mut bus = empty_bus();
bus.agnus.dmacon = DMACON_DMAEN | DMACON_BPLEN;
bus.denise.diwstrt = 0x2C81;
bus.denise.diwstop = 0x2FC1;
bus.denise.ddfstrt = 0x0038;
bus.denise.ddfstop = 0x00D0;
bus.denise.bplcon0 = 0x1000;
bus.denise.palette.write_ocs(1, 0x0FFF);
bus.denise.bplpt[0] = 0x0100;
let words_per_row = bitplane_words_per_row(
bus.agnus.revision(),
bus.denise.bplcon0,
bus.agnus.fmode(),
bus.denise.ddfstrt,
bus.denise.ddfstop,
bus.harddis_active(),
);
// Non-zero bitmap behind the pointer so a synthesized re-fetch of the
// uncaptured line would visibly paint.
for word in 0..words_per_row {
write_chip_word(&mut bus, 0x0100 + word * 2, 0xFFFF);
}
bus.current_frame_render_base = bus.capture_render_snapshot();
let captured_row = || {
Some(CapturedBitplaneRow {
nplanes: 1,
words_per_row,
fetch_origin_cck: None,
planes: std::array::from_fn(|plane| {
if plane == 0 {
vec![0xFFFF; words_per_row]
} else {
vec![0; words_per_row]
}
}),
})
};

let lit_rows = |fb: &[u32]| -> Vec<usize> {
let background = fb[0];
(0..FB_HEIGHT)
.filter(|row| {
fb[row * FB_WIDTH..(row + 1) * FB_WIDTH]
.iter()
.any(|&px| px != background)
})
.collect()
};

// Only line 1 recorded a fetch: line 0's sequencer never ran.
bus.current_frame_bitplane_rows[1] = captured_row();
let mut fb_skipped = vec![0u32; FB_PIXELS];
bitplane::render_from_input(&bitplane::RenderInput::from_bus(&bus), &mut fb_skipped);

// Both lines recorded fetches: shows where line 0's picture would sit.
bus.current_frame_bitplane_rows[0] = captured_row();
let mut fb_both = vec![0u32; FB_PIXELS];
bitplane::render_from_input(&bitplane::RenderInput::from_bus(&bus), &mut fb_both);

let skipped = lit_rows(&fb_skipped);
let both = lit_rows(&fb_both);
assert!(!skipped.is_empty(), "the captured line must paint");
assert!(
both.first() < skipped.first(),
"the two-capture render must start a line above ({both:?} vs {skipped:?})"
);
assert!(
skipped.iter().all(|row| both.contains(row)),
"the uncaptured line must not add pixels of its own"
);
}

#[test]
fn beam_trap_fires_at_exact_position_and_one_shot_disarms() {
let mut bus = empty_bus();
Expand Down
19 changes: 19 additions & 0 deletions src/video/bitplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3807,6 +3807,25 @@ pub fn render_from_input(input: &RenderInput, fb: &mut [u32]) -> RenderResult {
if captured_row.is_none() && !control.bitplane_dma_enabled() {
continue;
}
// The DMA capture is the authority for whether the bitplane
// sequencer ran on a line. A line with no captured fetch at all
// fetched nothing - e.g. BPLCON0 raised mid-line only after the
// line's DDFSTRT comparator had already passed (the Rampage
// bottom-scroller band entry), where Agnus starts no run until
// the next line. Synthesizing a picture from the register-derived
// window would paint a phantom row that hardware never fetched.
// (A captured row that merely disagrees with the register-derived
// geometry still takes the re-fetch path below; frames rendered
// without any capture - COPPERLINE_RENDER_LIVE_CHIP_RAM - keep
// the synthesized path.)
if has_captured_bitplane_rows
&& captured_bitplane_rows
.get(y)
.and_then(Option::as_ref)
.is_none()
{
continue;
}
// A DDFSTRT comparator below the hardwired start window ($18)
// only arms a run when the sequencer's SHW latch survived from
// the previous line (OCS clears it when a fetch run completes,
Expand Down
Loading