diff --git a/src/bus.rs b/src/bus.rs index 73dbf8f..1970658 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -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 { @@ -6690,7 +6690,6 @@ fn live_bitplane_collision_pixel_at( } Some(live_playfield_collision_pixel( idx, - nplanes, clxcon, bplcon0 & 0x0400 != 0, )) @@ -6698,12 +6697,11 @@ fn live_bitplane_collision_pixel_at( 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, @@ -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; } diff --git a/src/bus/tests.rs b/src/bus/tests.rs index 7a5e433..dc5ca83 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -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); @@ -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; @@ -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(); @@ -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(); @@ -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; @@ -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; @@ -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; diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index ee46f28..9da556d 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -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, @@ -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, @@ -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). @@ -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(), diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index 250004e..f299f8d 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -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); } @@ -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 {