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
18 changes: 11 additions & 7 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6530,7 +6530,7 @@ fn live_manual_bpl_word_collision_bits(
}
if word_active {
let collision =
live_playfield_collision_pixel(idx, nplanes, source_control.clxcon, dual_playfield);
live_playfield_collision_pixel(idx, source_control.clxcon, dual_playfield);
for dx in 0..pixel_repeat {
let x = x_cursor + dx;
if x < x_start || x >= x_stop {
Expand Down Expand Up @@ -6690,20 +6690,18 @@ fn live_bitplane_collision_pixel_at(
}
Some(live_playfield_collision_pixel(
idx,
nplanes,
clxcon,
bplcon0 & 0x0400 != 0,
))
}

fn live_playfield_collision_pixel(
idx: u8,
nplanes: usize,
clxcon: u16,
dual_playfield: bool,
) -> LivePlayfieldCollisionPixel {
let even_match = live_clxcon_planes_match(idx, nplanes, clxcon, 1);
let odd_match_raw = live_clxcon_planes_match(idx, nplanes, clxcon, 0);
let even_match = live_clxcon_planes_match(idx, clxcon, 1);
let odd_match_raw = live_clxcon_planes_match(idx, clxcon, 0);
let odd_match = odd_match_raw && (dual_playfield || even_match);
LivePlayfieldCollisionPixel {
pf1: dual_playfield && idx & 0b010101 != 0,
Expand All @@ -6717,9 +6715,15 @@ fn live_playfield_collision_pixel(
}
}

fn live_clxcon_planes_match(idx: u8, nplanes: usize, clxcon: u16, first_plane: usize) -> bool {
fn live_clxcon_planes_match(idx: u8, clxcon: u16, first_plane: usize) -> bool {
let mut matches = true;
for plane in (first_plane..nplanes.min(6)).step_by(2) {
// Every CLXCON-enabled plane participates in the match, not just the planes
// the display currently fetches: a plane enabled beyond the BPU count reads
// as 0 and still gates the collision (vAmiga checkS2PCollisions compares
// `(dBuffer & enbp) == (mvbp & enbp)` over all six planes). Regression:
// Denise/Sprites/collision/sprcoll* set CLXCON match bits for absent planes
// over a low-plane-count playfield.
for plane in (first_plane..6).step_by(2) {
if clxcon & (1 << (6 + plane)) == 0 {
continue;
}
Expand Down
25 changes: 25 additions & 0 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6612,6 +6612,10 @@ fn manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() {
bus.denise.ddfstrt = 0x0038;
bus.denise.ddfstop = 0x0038;
bus.denise.bplcon0 = 0x1000;
// One-bitplane playfield: match only bitplane 1 so the absent planes 2-6
// still match (CLXCON_RESET's all-six-planes=1 never matches one plane ->
// no collision on hardware).
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpos[0] = pos;
bus.denise.sprctl[0] = ctl;
bus.current_frame_sprite_display_enable_x_by_y[0] = Some(0);
Expand Down Expand Up @@ -7450,6 +7454,10 @@ fn captured_sprite_and_bitplane_rows_accumulate_live_sprite_playfield_clxdat() {
bus.denise.ddfstrt = 0x0038;
bus.denise.ddfstop = 0x0038;
bus.denise.bplcon0 = 0x1000;
// One-bitplane playfield: match only bitplane 1 so the absent (zero)
// planes 2-6 still match. The default CLXCON_RESET wants all six planes
// = 1, which one plane cannot satisfy -> no collision on hardware.
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpt[0] = sprite_ptr as u32;
bus.display_dma_sprpt[0] = sprite_ptr as u32;
bus.denise.bplpt[0] = 0x0100;
Expand Down Expand Up @@ -7488,6 +7496,11 @@ fn explicit_bpl1dat_output_accumulates_live_sprite_playfield_clxdat() {
bus.denise.diwstrt = 0x2C83;
bus.denise.diwstop = 0x2DC1;
bus.denise.bplcon0 = 0x1000;
// One-bitplane playfield: enable all plane-collision inputs but only match
// bitplane 1 (MVBP1), so the absent planes 2-6 (which read 0) still match.
// The default CLXCON_RESET (0x0FFF) demands all six planes = 1, which a
// one-plane playfield can never satisfy -> no collision on real hardware.
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpt[0] = sprite_ptr as u32;
bus.display_dma_sprpt[0] = sprite_ptr as u32;
bus.current_frame_render_base = bus.capture_render_snapshot();
Expand Down Expand Up @@ -7517,6 +7530,9 @@ fn manual_sprite_and_bpl1dat_writes_accumulate_live_sprite_playfield_clxdat() {
bus.denise.diwstrt = 0x2C83;
bus.denise.diwstop = 0x2DC1;
bus.denise.bplcon0 = 0x1000;
// One-bitplane playfield: match only bitplane 1 (absent planes 2-6 read 0
// and still match); CLXCON_RESET's all-six-planes=1 never matches.
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpos[0] = pos;
bus.denise.sprctl[0] = ctl;
bus.current_frame_render_base = bus.capture_render_snapshot();
Expand Down Expand Up @@ -7553,6 +7569,9 @@ fn same_line_bplcon1_scroll_increase_latches_later_live_sprite_playfield_clxdat(
bus.denise.ddfstop = 0x0038;
bus.denise.bplcon0 = 0x1000;
bus.denise.bplcon1 = 0;
// One-bitplane playfield: match only bitplane 1 (absent planes 2-6 read 0
// and still match); CLXCON_RESET's all-six-planes=1 never matches.
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpt[0] = sprite_ptr as u32;
bus.display_dma_sprpt[0] = sprite_ptr as u32;
bus.denise.bplpt[0] = 0x0100;
Expand Down Expand Up @@ -7654,6 +7673,9 @@ fn bplcon3_spres_hires_narrows_live_sprite_playfield_clxdat() {
// ECSENA/ENBPLCN3 set so the live SPRES write below latches.
bus.denise.bplcon0 = 0x9000 | BPLCON0_ECSENA;
bus.denise.bplcon3 = bplcon3;
// One-bitplane playfield: match only bitplane 1 so the absent planes
// 2-6 still match (CLXCON_RESET's all-six=1 never matches one plane).
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpt[0] = sprite_ptr as u32;
bus.display_dma_sprpt[0] = sprite_ptr as u32;
bus.denise.bplpt[0] = 0x0100;
Expand Down Expand Up @@ -7704,6 +7726,9 @@ fn same_line_bplcon3_spres_write_does_not_retime_earlier_live_sprite_playfield_c
// ECSENA/ENBPLCN3 set so the live SPRES write below latches.
bus.denise.bplcon0 = 0x9000 | BPLCON0_ECSENA;
bus.denise.bplcon3 = 0;
// One-bitplane playfield: match only bitplane 1 so the absent planes
// 2-6 still match (CLXCON_RESET's all-six=1 never matches one plane).
bus.denise.clxcon = 0x0FC1;
bus.denise.sprpt[0] = sprite_ptr as u32;
bus.display_dma_sprpt[0] = sprite_ptr as u32;
bus.denise.bplpt[0] = 0x0100;
Expand Down
30 changes: 11 additions & 19 deletions src/video/bitplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4291,7 +4291,6 @@ fn render_planned_playfield_line(
collision_table = std::array::from_fn(|idx| {
collision_pixel(
idx as u8,
nplanes,
pixel_control.clxcon,
pixel_control.clxcon2,
collision_dual,
Expand Down Expand Up @@ -4481,15 +4480,9 @@ impl CollisionPixel {
}
}

fn collision_pixel(
idx: u8,
nplanes: usize,
clxcon: u16,
clxcon2: u16,
dual_playfield: bool,
) -> CollisionPixel {
let even_match = clxcon_planes_match(idx, nplanes, clxcon, clxcon2, 1);
let odd_match_raw = clxcon_planes_match(idx, nplanes, clxcon, clxcon2, 0);
fn collision_pixel(idx: u8, clxcon: u16, clxcon2: u16, dual_playfield: bool) -> CollisionPixel {
let even_match = clxcon_planes_match(idx, clxcon, clxcon2, 1);
let odd_match_raw = clxcon_planes_match(idx, clxcon, clxcon2, 0);
let odd_match = odd_match_raw && (dual_playfield || even_match);
CollisionPixel {
pf1: dual_playfield && idx & 0b010101 != 0,
Expand All @@ -4503,15 +4496,15 @@ fn collision_pixel(
}
}

fn clxcon_planes_match(
idx: u8,
nplanes: usize,
clxcon: u16,
clxcon2: u16,
first_plane: usize,
) -> bool {
fn clxcon_planes_match(idx: u8, clxcon: u16, clxcon2: u16, first_plane: usize) -> bool {
let mut matches = true;
for plane in (first_plane..nplanes.min(8)).step_by(2) {
// Every CLXCON/CLXCON2-enabled plane participates in the match, not just
// the planes the display currently fetches: a plane enabled beyond the BPU
// count reads as 0 and still gates the match (vAmiga checkS2PCollisions
// compares `(dBuffer & enbp) == (mvbp & enbp)` over all six/eight planes).
// Regression: Denise/Sprites/collision/sprcoll* set CLXCON with match bits
// for absent planes over a low-plane-count playfield.
for plane in (first_plane..8).step_by(2) {
// Planes 1-6 take their enable/match bits from CLXCON; the AGA
// planes 7-8 from CLXCON2 (ENBP7/ENBP8 in bits 6-7, MVBP7/MVBP8 in
// bits 0-1).
Expand Down Expand Up @@ -4542,7 +4535,6 @@ fn record_generated_playfield_collision_pixel(
) {
let collision = collision_pixel(
sample.idx,
sample.nplanes,
control.clxcon,
control.clxcon2,
control.dual_playfield(),
Expand Down
39 changes: 29 additions & 10 deletions src/video/bitplane/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4913,22 +4913,22 @@ fn sprite_dma_reuse_stops_at_null_control_block() {

#[test]
fn collision_pixel_honors_clxcon_match_bits() {
let collision = collision_pixel(0b000011, 2, 0x00C3, 0, false);
let collision = collision_pixel(0b000011, 0x00C3, 0, false);
assert!(collision.pf1_match);
assert!(collision.pf2_match);

let mismatch = collision_pixel(0b000010, 2, 0x00C3, 0, false);
let mismatch = collision_pixel(0b000010, 0x00C3, 0, false);
assert!(!mismatch.pf1_match);
assert!(mismatch.pf2_match);
}

#[test]
fn collision_pixel_single_playfield_odd_match_requires_even_match() {
let single = collision_pixel(0b000001, 2, 0x0083, 0, false);
let single = collision_pixel(0b000001, 0x0083, 0, false);
assert!(!single.pf1_match);
assert!(!single.pf2_match);

let dual = collision_pixel(0b000001, 2, 0x0083, 0, true);
let dual = collision_pixel(0b000001, 0x0083, 0, true);
assert!(dual.pf1_match);
assert!(!dual.pf2_match);
}
Expand All @@ -4938,31 +4938,50 @@ fn collision_pixel_single_playfield_odd_match_requires_even_match() {
#[test]
fn collision_pixel_planes_7_and_8_use_clxcon2() {
// Plane 7 (bit 6) enabled, must be set: pixel with bit 6 matches.
let hit = collision_pixel(0b0100_0000, 8, 0, 0x0041, false);
let hit = collision_pixel(0b0100_0000, 0, 0x0041, false);
assert!(hit.pf1_match);
let miss = collision_pixel(0, 8, 0, 0x0041, false);
let miss = collision_pixel(0, 0, 0x0041, false);
assert!(!miss.pf1_match);

// Plane 8 (bit 7) enabled, must be clear: a set bit 7 mismatches.
let even_hit = collision_pixel(0, 8, 0, 0x0080, false);
let even_hit = collision_pixel(0, 0, 0x0080, false);
assert!(even_hit.pf2_match);
let even_miss = collision_pixel(0b1000_0000, 8, 0, 0x0080, false);
let even_miss = collision_pixel(0b1000_0000, 0, 0x0080, false);
assert!(!even_miss.pf2_match);

// With CLXCON2 clear, planes 7-8 never gate the match (and the
// sprite-enable bits of CLXCON are not misread for them).
let ignore = collision_pixel(0b1100_0000, 8, 0xF000, 0, false);
let ignore = collision_pixel(0b1100_0000, 0xF000, 0, false);
assert!(ignore.pf1_match && ignore.pf2_match);
}

#[test]
fn collision_pixel_disabled_planes_match_continuously() {
let collision = collision_pixel(0, 6, 0, 0, false);
let collision = collision_pixel(0, 0, 0, false);
assert!(collision.pf1_match);
assert!(collision.pf2_match);
assert_eq!(collision.clxdat_bits(), 1);
}

#[test]
fn collision_match_gates_on_enabled_planes_beyond_the_bpu_count() {
// A one-bitplane playfield pixel (only bitplane 1 set) with CLXCON
// enabling all six planes at match value 1 must NOT match: the absent
// planes 2-6 read 0, and every ENABLED plane participates in the compare
// regardless of the current BPU count (vAmiga checkS2PCollisions:
// `(dBuffer & enbp) == (mvbp & enbp)`). Copperline previously only checked
// planes up to the fetched count and so spuriously matched.
let all_planes_match_one = collision_pixel(0b000001, 0x0FFF, 0, false);
assert!(!all_planes_match_one.pf1_match);
assert!(!all_planes_match_one.pf2_match);

// Setting the absent planes' match value to 0 (CLXCON 0x0FC1) lets the
// zero-read absent planes match, so the one-plane pixel collides.
let only_plane1_match = collision_pixel(0b000001, 0x0FC1, 0, false);
assert!(only_plane1_match.pf1_match);
assert!(only_plane1_match.pf2_match);
}

#[test]
fn generated_playfield_pixels_feed_playfield_and_sprite_clxdat() {
let control = ControlState {
Expand Down
Loading