diff --git a/src/bus.rs b/src/bus.rs index c3c0cab..7c111a3 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -262,7 +262,10 @@ const CLXDAT_SPRITE_PLAYFIELD_MASK: u16 = 0x01FE; const CLXDAT_SPRITE_SPRITE_MASK: u16 = 0x7E00; const BITPLANE_DDF_HARD_START: u16 = 0x0018; const BITPLANE_DDF_HARD_STOP: u16 = 0x00D8; -const SPRITE_DMA_PAIR_CAPTURE_HPOS: [u32; 4] = [0x018, 0x020, 0x028, 0x030]; +/// First DMA slot colour clock of each sprite channel (the POS/DATA word; +/// the CTL/DATB word follows two clocks later). Hardware slot chart (and +/// vAmiga's DAS table): sprite N fetches at $15+4N / $17+4N. +const SPRITE_DMA_SLOT1_HPOS: [u32; 8] = [0x15, 0x19, 0x1D, 0x21, 0x25, 0x29, 0x2D, 0x31]; const NANOS_PER_SECOND: u128 = 1_000_000_000; const VIDEO_FETCH_TIMING_SAMPLE_RATE: u128 = 128; const VIDEO_COLLISION_TIMING_SAMPLE_RATE: u128 = 16; diff --git a/src/bus/custom_regs.rs b/src/bus/custom_regs.rs index 9e559ab..ac07b29 100644 --- a/src/bus/custom_regs.rs +++ b/src/bus/custom_regs.rs @@ -966,7 +966,10 @@ impl Bus { self.agnus.set_ersy(val & 0x0002 != 0); if self.denise.bplcon0 != previous { self.record_bitplane_bplcon0_write(previous); - self.ddf_seq_record_bplcon0_write(self.denise.bplcon0, previous, 3); + // Agnus's sequencer copy of BPLCON0 updates four colour + // clocks after the write slot (vAmiga DMA_CYCLES(4); + // Denise's own interpretation switches earlier). + self.ddf_seq_record_bplcon0_write(self.denise.bplcon0, previous, 4); } false } diff --git a/src/bus/dma_slots.rs b/src/bus/dma_slots.rs index 450a5ef..563fe01 100644 --- a/src/bus/dma_slots.rs +++ b/src/bus/dma_slots.rs @@ -189,7 +189,12 @@ impl Bus { self.check_ui_beam_traps((old_vpos, old_hpos), old_frame_lines, tick.new_frames); } if tick.new_frames == 0 && tick.new_lines == 0 { - self.capture_sprite_dma_words_if_due(old_vpos, old_hpos, self.agnus.hpos); + self.capture_sprite_dma_words_if_due( + old_vpos, + old_hpos, + self.agnus.hpos, + old_emulated_cck, + ); self.capture_bitplane_dma_words_if_due( old_vpos, old_hpos, @@ -572,36 +577,26 @@ impl Bus { pub(super) fn sprite_slot_active_at(&self, hpos: u32) -> bool { // Real OCS sprite DMA fetches only on lines where a sprite is actually - // active (within its vstart..vstop), not on every line. The reserved - // slots map to sprite pairs by `SPRITE_DMA_PAIR_CAPTURE_HPOS` - // (0x18->sprites 0/1, 0x20->2/3, 0x28->4/5, 0x30->6/7), so reserve a - // pair's slot only when one of its sprites is fetching data this line -- - // gating on the same `data_dma_active` the renderer uses, so the bus - // model and the captured image agree. Parked/off-screen sprites free - // their slot for the CPU/blitter; previously they were reserved - // unconditionally whenever SPREN was on (~2504 cck/frame of phantom DMA - // stolen from the blitter). + // active (within its vstart..vstop), not on every line. Sprite N owns + // the two odd slots $15+4N and $17+4N (the hardware slot chart / + // vAmiga's DAS table), so reserve them only when that sprite is + // fetching data this line -- gating on the same `data_dma_active` the + // renderer uses, so the bus model and the captured image agree. + // Parked/off-screen sprites free their slots for the CPU/blitter. if self.agnus.dmacon & DMACON_SPREN == 0 { return false; } // Sprite DMA slots sit on ODD color clocks (same parity as refresh/ // disk/audio -- the HRM chart's fixed-DMA band), so they never block - // the Copper's even-clock fetches. Each active sprite pair reserves - // the two odd slots of its 8-cck band (0x19/0x1B, 0x21/0x23, ...). - if !(0x019..=0x037).contains(&hpos) || hpos & 1 == 0 { - return false; - } - let rel = hpos - 0x019; - if rel % 8 >= 4 { + // the Copper's even-clock fetches. + if !(0x015..=0x033).contains(&hpos) || hpos & 1 == 0 { return false; } - let pair = (rel / 8) as usize; - if pair >= 4 { + let sprite = ((hpos - 0x015) / 4) as usize; + if sprite >= 8 { return false; } - let first = pair * 2; - self.display_dma_sprite_state[first].data_dma_active - || self.display_dma_sprite_state[first + 1].data_dma_active + self.display_dma_sprite_state[sprite].data_dma_active } pub(super) fn record_bitplane_dmacon_write(&mut self, previous: u16) { diff --git a/src/bus/frame_capture.rs b/src/bus/frame_capture.rs index 46a3a26..684d890 100644 --- a/src/bus/frame_capture.rs +++ b/src/bus/frame_capture.rs @@ -320,7 +320,7 @@ impl Bus { vpos >= self.current_frame_visible_start_vpos && beam_y >= vstart && beam_y < vstop - && hpos >= SPRITE_DMA_PAIR_CAPTURE_HPOS[sprite / 2] + && hpos >= SPRITE_DMA_SLOT1_HPOS[sprite] } pub(super) fn latch_display_sprite_register_data_stream_at( @@ -334,7 +334,7 @@ impl Bus { if beam_y >= frame_lines { return; } - let fetch_slot = SPRITE_DMA_PAIR_CAPTURE_HPOS[sprite / 2]; + let fetch_slot = SPRITE_DMA_SLOT1_HPOS[sprite]; let stream_start = if hpos >= fetch_slot { beam_y.saturating_add(1) } else { @@ -470,9 +470,8 @@ impl Bus { let in_window = beam_y >= control.vstart && beam_y < control.vstop; let sprite_dma_enabled = dmacon & (DMACON_DMAEN | DMACON_SPREN) == (DMACON_DMAEN | DMACON_SPREN); - let reaches_current_fetch_slot = beam_y == control.vstart - && hpos <= SPRITE_DMA_PAIR_CAPTURE_HPOS[sprite / 2] - && sprite_dma_enabled; + let reaches_current_fetch_slot = + beam_y == control.vstart && hpos <= SPRITE_DMA_SLOT1_HPOS[sprite] && sprite_dma_enabled; let keep_held_line = !sprite_dma_enabled && in_window && state.data_dma_active && state.last_line.is_some(); let keep_active_dma_line = @@ -545,7 +544,7 @@ impl Bus { } else { (beam_y - control.vstart) as u32 }; - if beam_y >= control.vstart && hpos > SPRITE_DMA_PAIR_CAPTURE_HPOS[sprite / 2] { + if beam_y >= control.vstart && hpos > SPRITE_DMA_SLOT1_HPOS[sprite] { line = line.saturating_add(1); } let line = if sprite_scan_doubled(self.agnus.fmode()) { @@ -748,7 +747,7 @@ impl Bus { .collect(); let mut idx = 0; for vpos in 0..display_start { - for (pair, &capture_hpos) in SPRITE_DMA_PAIR_CAPTURE_HPOS.iter().enumerate() { + for (sprite, &capture_hpos) in SPRITE_DMA_SLOT1_HPOS.iter().enumerate() { while idx < writes.len() && (writes[idx].0 < vpos || (writes[idx].0 == vpos && writes[idx].1 < capture_hpos)) @@ -770,7 +769,7 @@ impl Bus { if self.sprite_dma_inhibited_by_vertical_blank_at(vpos) { continue; } - for sprite in pair * 2..pair * 2 + 2 { + { if sprite_dma_disabled_by_bitplane_ddf( sprite, self.agnus.revision(), @@ -840,26 +839,21 @@ impl Bus { vpos: u32, old_hpos: u32, new_hpos: u32, + old_emulated_cck: u64, ) { - // No sprite DMA pair slot lies in [old_hpos, new_hpos): nothing below - // can run (the per-pair loop checks the same window), so skip the + // No sprite DMA slot lies in [old_hpos, new_hpos): nothing below can + // run (the per-sprite loop checks the same window), so skip the // sprite-state scan on the vast majority of beam advances. - if old_hpos > SPRITE_DMA_PAIR_CAPTURE_HPOS[3] || new_hpos <= SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - { + if old_hpos > SPRITE_DMA_SLOT1_HPOS[7] || new_hpos <= SPRITE_DMA_SLOT1_HPOS[0] { return; } if self.sprite_dma_inhibited_by_vertical_blank_at(vpos) { return; } - let sprite_dma_enabled = - self.agnus.dmacon & (DMACON_DMAEN | DMACON_SPREN) == (DMACON_DMAEN | DMACON_SPREN); let sprite_vertical_bar_active = self .display_dma_sprite_state .iter() .any(|state| state.data_dma_active && state.last_line.is_some()); - if !sprite_dma_enabled && !sprite_vertical_bar_active { - return; - } let Some(fb_y) = visible_framebuffer_y( vpos, self.current_frame_visible_start_vpos, @@ -875,15 +869,27 @@ impl Bus { let mut fetched_lines = 0usize; let bitplane_bplcon0 = self.effective_bitplane_bplcon0(); let bitplane_dmacon = self.effective_bitplane_dmacon(); - for (pair, &capture_hpos) in SPRITE_DMA_PAIR_CAPTURE_HPOS.iter().enumerate() { + for (sprite, &capture_hpos) in SPRITE_DMA_SLOT1_HPOS.iter().enumerate() { if old_hpos > capture_hpos || new_hpos <= capture_hpos { continue; } + // SPREN is sampled by each sprite's own DMA slot (the sprena/ + // sprdis vAmigaTS sweeps step DMACON writes in two-colour-clock + // increments around individual slots), honouring the DMACON + // write commit delay. + let slot_cck = + old_emulated_cck.saturating_add(u64::from(capture_hpos.saturating_sub(old_hpos))); + let sprite_dma_enabled = self.effective_bitplane_dmacon_at(slot_cck) + & (DMACON_DMAEN | DMACON_SPREN) + == (DMACON_DMAEN | DMACON_SPREN); + if !sprite_dma_enabled && !sprite_vertical_bar_active { + continue; + } if sprite_dma_enabled { pair_slots += 1; } let mut captured_line = false; - for sprite in pair * 2..pair * 2 + 2 { + { if sprite_dma_disabled_by_bitplane_ddf( sprite, self.agnus.revision(), diff --git a/src/bus/tests.rs b/src/bus/tests.rs index 9d39688..f3d32a8 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -18,7 +18,7 @@ use super::{ DENISE_HPOS_LAG_CCK, DMACON_AUD_MASK, DMACON_BLTEN, DMACON_BLTPRI, DMACON_BPLEN, DMACON_SPREN, PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS, RENDER_COPPER_WAIT_HPOS_FB0, RENDER_DIW_HSTART_FB0, RENDER_MIN_OVERSCAN_START_VPOS, RENDER_VISIBLE_LINES, RENDER_VISIBLE_START_VPOS, - SPRITE_DMA_PAIR_CAPTURE_HPOS, + SPRITE_DMA_SLOT1_HPOS, }; use crate::audio::AudioSink; use crate::chipset::agnus::{ @@ -4716,7 +4716,7 @@ fn sprite_dma_capture_samples_line_words_at_beam_time() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -4767,7 +4767,7 @@ fn inactive_sprite_pointer_write_before_pair_slot_seeds_next_descriptor_fetch() bus.agnus.vpos = 0x2C; bus.agnus.hpos = 0; bus.capture_current_frame_display_start(); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -4816,7 +4816,7 @@ fn vertical_blank_sprite_pointer_write_reloads_descriptor_in_offscreen_replay() bus.agnus.vpos = RENDER_VISIBLE_START_VPOS; bus.capture_current_frame_display_start(); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -4844,14 +4844,14 @@ fn post_vertical_blank_sprite_pointer_write_retargets_pending_descriptor() { bus.sprite_dma_frame_start_ptr[0] = descriptor_ptr as u32; bus.current_frame_render_events.push(BeamRegisterWrite { vpos: PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS + 11, - hpos: SPRITE_DMA_PAIR_CAPTURE_HPOS[0], + hpos: SPRITE_DMA_SLOT1_HPOS[0], offset: 0x120, value: (data_ptr >> 16) as u16, source: BeamWriteSource::Copper, }); bus.current_frame_render_events.push(BeamRegisterWrite { vpos: PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS + 11, - hpos: SPRITE_DMA_PAIR_CAPTURE_HPOS[0] + 2, + hpos: SPRITE_DMA_SLOT1_HPOS[0] + 2, offset: 0x122, value: data_ptr as u16, source: BeamWriteSource::Copper, @@ -4859,7 +4859,7 @@ fn post_vertical_blank_sprite_pointer_write_retargets_pending_descriptor() { bus.agnus.vpos = RENDER_VISIBLE_START_VPOS; bus.capture_current_frame_display_start(); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -4889,10 +4889,10 @@ fn manual_sprite_control_write_fetches_data_from_sprpt() { assert!(!bus.write_custom_word_from(0x142, ctl, BeamWriteSource::Copper)); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -4931,15 +4931,15 @@ fn pending_register_control_sprite_pointer_write_retargets_data_stream() { assert!(!bus.write_custom_word_from(0x142, ctl, BeamWriteSource::Copper)); bus.agnus.vpos = 0x24; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0]; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0]; let _ = bus.write_custom_word_from(0x120, (new_data_ptr >> 16) as u16, BeamWriteSource::Copper); let _ = bus.write_custom_word_from(0x122, new_data_ptr as u16, BeamWriteSource::Copper); bus.agnus.vpos = 0x40; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x41; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -4972,22 +4972,22 @@ fn pending_descriptor_sprite_pointer_write_retargets_data_stream() { bus.display_dma_sprpt[0] = old_ptr as u32; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let pending = bus.display_dma_sprite_state[0]; assert_eq!(pending.control.map(|control| control.vstart), Some(0x60)); assert!(!pending.data_dma_active); bus.agnus.vpos = 0x30; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0]; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0]; let _ = bus.write_custom_word_from(0x120, (new_data_ptr >> 16) as u16, BeamWriteSource::Copper); let _ = bus.write_custom_word_from(0x122, new_data_ptr as u16, BeamWriteSource::Copper); bus.agnus.vpos = 0x60; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x61; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5031,12 +5031,12 @@ fn armed_pointer_reload_before_vstart_fetches_descriptor_words() { }; bus.agnus.vpos = 0x24; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0]; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0]; let _ = bus.write_custom_word_from(0x120, (sprite_ptr >> 16) as u16, BeamWriteSource::Copper); let _ = bus.write_custom_word_from(0x122, sprite_ptr as u16, BeamWriteSource::Copper); bus.agnus.vpos = 0x40; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5056,7 +5056,7 @@ fn after_slot_armed_sprite_pointer_write_seeds_dma_data_stream() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x40; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0]; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0]; bus.denise.sprpos[0] = pos; bus.denise.sprctl[0] = ctl; bus.denise.spr_armed[0] = true; @@ -5064,7 +5064,7 @@ fn after_slot_armed_sprite_pointer_write_seeds_dma_data_stream() { let _ = bus.write_custom_word_from(0x120, (data_ptr >> 16) as u16, BeamWriteSource::Copper); let _ = bus.write_custom_word_from(0x122, data_ptr as u16, BeamWriteSource::Copper); bus.agnus.vpos = 0x41; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5093,15 +5093,15 @@ fn active_sprite_control_rewrite_preserves_descriptor_data_origin() { bus.display_dma_sprpt[0] = sprite_ptr as u32; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 8; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 8; assert!(!bus.write_custom_word_from(0x140, moved_pos, BeamWriteSource::Copper)); assert!(!bus.write_custom_word_from(0x142, moved_ctl, BeamWriteSource::Copper)); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5140,7 +5140,7 @@ fn sprite_pointer_write_after_pair_slot_seeds_next_descriptor_fetch() { bus.denise.sprpt[2] = old_ptr as u32; bus.display_dma_sprpt[2] = old_ptr as u32; - let slot = SPRITE_DMA_PAIR_CAPTURE_HPOS[1]; + let slot = SPRITE_DMA_SLOT1_HPOS[2]; bus.agnus.vpos = 0; bus.agnus.hpos = slot - 1; bus.advance_chipset(2); @@ -5180,7 +5180,7 @@ fn sprite_dma_inverted_vstop_runs_to_frame_bottom() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -5217,7 +5217,7 @@ fn fmode_wide_sprite_dma_captures_extension_words() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -5264,7 +5264,7 @@ fn fmode_sscan2_sprite_dma_doubles_each_data_line() { for vpos in 0x2C..=0x32u32 { bus.agnus.vpos = vpos; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); } @@ -5442,7 +5442,7 @@ fn sprite_dma_capture_preserves_sprite_started_before_visible_area() { bus.sprite_dma_frame_start_ptr[0] = sprite_ptr as u32; bus.capture_current_frame_display_start(); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5486,7 +5486,7 @@ fn pending_sprite_control_rewrite_preserves_descriptor_data_origin() { assert!(!bus.write_custom_word_from(0x142, moved_ctl, BeamWriteSource::Copper)); bus.agnus.vpos = vstart as u32; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5518,7 +5518,7 @@ fn active_sprite_pos_write_retimes_hstart_without_clearing_dma_stream() { bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); // Rewriting SPRxPOS while DMA is already enabled changes the horizontal @@ -5528,11 +5528,11 @@ fn active_sprite_pos_write_retimes_hstart_without_clearing_dma_stream() { bus.agnus.vpos = 0x2D; bus.agnus.hpos = 0; assert!(!bus.write_custom_word_from(0x140, moved_pos, BeamWriteSource::Copper)); - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x2E; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let words: Vec<(i32, i32, u16, u16)> = bus @@ -5630,7 +5630,7 @@ fn sprite_dma_capture_treats_zero_pointer_as_chip_address() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = 0; bus.denise.sprpt[1] = 0x20; bus.display_dma_sprpt[0] = 0; @@ -5821,7 +5821,7 @@ fn sprite_dma_zero_height_descriptor_terminates_stream() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -5845,7 +5845,7 @@ fn sprite_dma_capture_wraps_control_words_at_chip_ram_end() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -5876,7 +5876,7 @@ fn sprite_dma_zero_height_descriptor_terminates_after_chip_address_wrap() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; bus.denise.sprpt[1] = 0x0200; @@ -5904,7 +5904,7 @@ fn sprite_dma_capture_latches_control_words_until_stop() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; @@ -5914,7 +5914,7 @@ fn sprite_dma_capture_latches_control_words_until_stop() { write_chip_word(&mut bus, sprite_ptr, rewritten_pos); write_chip_word(&mut bus, sprite_ptr + 2, rewritten_ctl); bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -5946,7 +5946,7 @@ fn sprite_dma_capture_samples_later_pairs_at_their_fetch_slot() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite0_ptr as u32; bus.denise.sprpt[6] = sprite6_ptr as u32; bus.display_dma_sprpt[0] = sprite0_ptr as u32; @@ -5955,7 +5955,7 @@ fn sprite_dma_capture_samples_later_pairs_at_their_fetch_slot() { bus.advance_chipset(2); write_chip_word(&mut bus, sprite0_ptr + 4, 0xAAAA); write_chip_word(&mut bus, sprite6_ptr + 4, 0xBBBB); - let remaining = SPRITE_DMA_PAIR_CAPTURE_HPOS[3] + 1 - bus.agnus.hpos; + let remaining = SPRITE_DMA_SLOT1_HPOS[6] + 1 - bus.agnus.hpos; bus.advance_chipset(remaining); let lines = bus.frame_captured_sprite_lines(); @@ -5987,7 +5987,7 @@ fn sprite_pointer_write_at_pair_slot_seeds_next_line_descriptor_fetch() { bus.display_dma_sprpt[0] = old_ptr as u32; bus.agnus.vpos = 0x24; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(1); let _ = bus.write_custom_word_from(0x120, (new_ptr >> 16) as u16, BeamWriteSource::Copper); let _ = bus.write_custom_word_from(0x122, new_ptr as u16, BeamWriteSource::Copper); @@ -5999,7 +5999,7 @@ fn sprite_pointer_write_at_pair_slot_seeds_next_line_descriptor_fetch() { ); bus.agnus.vpos = 0x40; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -6014,7 +6014,7 @@ fn empty_sprite_dma_slot_does_not_mark_frame_dma_observed() { let mut bus = empty_bus(); bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = RENDER_VISIBLE_START_VPOS; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); @@ -6044,7 +6044,7 @@ fn sprite_dma_capture_blocks_sprite_seven_when_ddfstrt_uses_early_fetch_slot() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_BPLEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[3] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[6] - 1; bus.denise.bplcon0 = 0x1000; bus.denise.ddfstrt = 0x0028; bus.denise.ddfstop = 0x0038; @@ -6053,7 +6053,7 @@ fn sprite_dma_capture_blocks_sprite_seven_when_ddfstrt_uses_early_fetch_slot() { bus.display_dma_sprpt[6] = sprite6_ptr as u32; bus.display_dma_sprpt[7] = sprite7_ptr as u32; - bus.advance_chipset(2); + bus.advance_chipset(SPRITE_DMA_SLOT1_HPOS[7] + 1 - bus.agnus.hpos); let lines = bus.frame_captured_sprite_lines(); assert!(lines.iter().any(|line| line.sprite == 6)); @@ -6079,7 +6079,7 @@ fn sprite_dma_capture_keeps_sprite_seven_when_ddfstrt_matches_sprite_slot() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_BPLEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[3] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[6] - 1; bus.denise.bplcon0 = 0x3000; bus.denise.ddfstrt = 0x0030; bus.denise.ddfstop = 0x0038; @@ -6088,7 +6088,7 @@ fn sprite_dma_capture_keeps_sprite_seven_when_ddfstrt_matches_sprite_slot() { bus.display_dma_sprpt[6] = sprite6_ptr as u32; bus.display_dma_sprpt[7] = sprite7_ptr as u32; - bus.advance_chipset(2); + bus.advance_chipset(SPRITE_DMA_SLOT1_HPOS[7] + 1 - bus.agnus.hpos); let lines = bus.frame_captured_sprite_lines(); assert!(lines.iter().any(|line| line.sprite == 6)); @@ -6111,17 +6111,17 @@ fn sprite_dma_capture_repeats_last_fetched_line_after_dma_disable_until_vstop() bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; bus.advance_chipset(2); bus.agnus.dmacon = DMACON_DMAEN; bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x2E; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -6156,13 +6156,13 @@ fn sprite_dma_capture_does_not_start_descriptor_at_or_before_current_vpos() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprite_state[0].next_ptr = Some(sprite_ptr as u32); bus.advance_chipset(2); bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); assert!(bus @@ -6188,16 +6188,16 @@ fn sprite_dma_reuse_skips_descriptor_with_vstart_before_current_vpos() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2B; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite_ptr as u32; bus.display_dma_sprpt[0] = sprite_ptr as u32; bus.advance_chipset(2); bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); bus.agnus.vpos = 0x2D; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); let lines = bus.frame_captured_sprite_lines(); @@ -6237,7 +6237,7 @@ fn sprite_dma_chained_descriptor_with_same_vstart_arms_after_control_fetch_line( for vpos in 0x2C..=0x31u32 { bus.agnus.vpos = vpos; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.advance_chipset(2); } @@ -6275,7 +6275,7 @@ fn visible_sprite_pixels_accumulate_live_sprite_sprite_clxdat() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.sprpt[0] = sprite0_ptr as u32; bus.denise.sprpt[2] = sprite2_ptr as u32; bus.display_dma_sprpt[0] = sprite0_ptr as u32; @@ -6285,7 +6285,7 @@ fn visible_sprite_pixels_accumulate_live_sprite_sprite_clxdat() { bus.advance_chipset(1); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); - let remaining = SPRITE_DMA_PAIR_CAPTURE_HPOS[1] + 1 - bus.agnus.hpos; + let remaining = SPRITE_DMA_SLOT1_HPOS[1] + 1 - bus.agnus.hpos; bus.advance_chipset(remaining); assert_eq!(bus.custom_read(0x00E, 2), 0x8000); @@ -6658,7 +6658,7 @@ fn bplcon3_spres_hires_narrows_live_sprite_sprite_clxdat() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.bplcon0 = 0x8000; bus.denise.bplcon3 = bplcon3; bus.denise.sprpt[0] = sprite0_ptr as u32; @@ -6698,7 +6698,7 @@ fn same_line_clxcon_odd_sprite_enable_does_not_retime_earlier_live_sprite_sprite bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.clxcon = initial_clxcon; bus.denise.sprpt[1] = sprite1_ptr as u32; bus.denise.sprpt[2] = sprite2_ptr as u32; @@ -6707,7 +6707,7 @@ fn same_line_clxcon_odd_sprite_enable_does_not_retime_earlier_live_sprite_sprite bus.current_frame_sprite_display_enable_x_by_y[0] = Some(0); bus.current_frame_render_base = bus.capture_render_snapshot(); - let after_pair_capture = SPRITE_DMA_PAIR_CAPTURE_HPOS[1] + 1 - bus.agnus.hpos; + let after_pair_capture = SPRITE_DMA_SLOT1_HPOS[1] + 1 - bus.agnus.hpos; bus.advance_chipset(after_pair_capture); if let Some(enable_hpos) = enable_hpos { @@ -7389,7 +7389,7 @@ fn captured_sprite_and_bitplane_rows_accumulate_live_sprite_playfield_clxdat() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN | DMACON_BPLEN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C81; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -7425,7 +7425,7 @@ fn explicit_bpl1dat_output_accumulates_live_sprite_playfield_clxdat() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.bplcon0 = 0x1000; @@ -7483,7 +7483,7 @@ fn same_line_bplcon1_scroll_increase_latches_later_live_sprite_playfield_clxdat( bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN | DMACON_BPLEN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -7524,7 +7524,7 @@ fn same_line_clxcon_odd_sprite_enable_does_not_retime_earlier_live_sprite_playfi bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN | DMACON_BPLEN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C83; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -7575,7 +7575,7 @@ fn bplcon3_spres_hires_narrows_live_sprite_playfield_clxdat() { bus.set_agnus_revision(AgnusRevision::Ecs8372Rev4); bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN | DMACON_BPLEN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C81; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -7621,7 +7621,7 @@ fn same_line_bplcon3_spres_write_does_not_retime_earlier_live_sprite_playfield_c bus.set_agnus_revision(AgnusRevision::Ecs8372Rev4); bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN | DMACON_BPLEN; bus.agnus.vpos = 0x2C; - bus.agnus.hpos = SPRITE_DMA_PAIR_CAPTURE_HPOS[0] - 1; + bus.agnus.hpos = SPRITE_DMA_SLOT1_HPOS[0] - 1; bus.denise.diwstrt = 0x2C81; bus.denise.diwstop = 0x2DC1; bus.denise.ddfstrt = 0x0038; @@ -8261,16 +8261,21 @@ fn fixed_agnus_dma_slot_bands_drive_owner_selection() { bus.agnus.dmacon = DMACON_DMAEN | DMACON_SPREN; // A sprite's slot is reserved only while that sprite is actually - // fetching (data_dma_active); a parked sprite frees it. Sprite slots - // sit on odd color clocks (0x019/0x01B for pair 0). - bus.agnus.hpos = 0x019; + // fetching (data_dma_active); a parked sprite frees it. Sprite N owns + // the odd color clocks $15+4N and $17+4N (hardware slot chart). + bus.agnus.hpos = 0x015; assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Idle); bus.display_dma_sprite_state[0].data_dma_active = true; assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Sprite); - // The even color clock inside the band stays free for the Copper/CPU. - bus.agnus.hpos = 0x018; + bus.agnus.hpos = 0x017; + assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Sprite); + // The even color clock inside the band stays free for the Copper/CPU, + // and another sprite's slot stays free while that sprite is parked. + bus.agnus.hpos = 0x016; + assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Idle); + bus.agnus.hpos = 0x019; assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Idle); - bus.agnus.hpos = 0x037; + bus.agnus.hpos = 0x035; assert_eq!(bus.scheduled_dma_owner(false), ChipBusOwner::Idle); bus.display_dma_sprite_state[0].data_dma_active = false; diff --git a/src/chipset/agnus.rs b/src/chipset/agnus.rs index fcb6f19..e01ff7e 100644 --- a/src/chipset/agnus.rs +++ b/src/chipset/agnus.rs @@ -536,15 +536,27 @@ pub fn bitplane_dma_planes(bplcon0: u16, aga: bool) -> usize { // OCS lowres BPU=7 overfetch quirk does not apply. return if bplcon0 & 0x0010 != 0 { 8 } else { code }; } - let display_planes = if bitplane_shres(bplcon0) { - code.min(2) - } else { - code.min(6) - }; - if code == 7 && !bitplane_hires(bplcon0) && !bitplane_shres(bplcon0) { + // OCS/ECS fetch-unit table (hardware-verified: the invplanes1 A500 + // photo shows no fetch at all for hi-res BPU>4): lo-res BPU=7 is the + // overprogrammed 4-plane fetch, hi-res schedules at most 4 plane + // streams (BPU 5-7 fetch nothing), SHRES at most 2 (BPU 3-7 fetch + // nothing). + if bitplane_shres(bplcon0) { + if code <= 2 { + code + } else { + 0 + } + } else if bitplane_hires(bplcon0) { + if code <= 4 { + code + } else { + 0 + } + } else if code == 7 { 4 } else { - display_planes + code } } diff --git a/src/chipset/denise.rs b/src/chipset/denise.rs index f41d7e2..f5ccb77 100644 --- a/src/chipset/denise.rs +++ b/src/chipset/denise.rs @@ -274,14 +274,29 @@ impl BitplaneMode { }; } let display_planes = if shres { code.min(2) } else { code.min(6) }; - let dma_planes = if code == 7 && !hires && !shres { - // OCS lowres BPU=7 is an overprogrammed fetch mode: Denise can - // still decode six BPLDAT latches, but Agnus only schedules the - // first four bitplane DMA streams. Higher planes display their - // current BPLDAT latch values until software updates them. + // Agnus's fetch-unit table schedules at most 4 plane streams in + // hi-res and 2 in SHRES; overprogrammed counts fetch NOTHING there + // (hardware-verified: the invplanes1 A500 photo shows no fetch for + // hi-res BPU=7). Lo-res BPU=7 is the overprogrammed 4-plane fetch: + // Denise still decodes six BPLDAT latches, but only the first four + // streams are fed by DMA; higher planes display their current + // latch values until software updates them. + let dma_planes = if shres { + if code <= 2 { + code + } else { + 0 + } + } else if hires { + if code <= 4 { + code + } else { + 0 + } + } else if code == 7 { 4 } else { - display_planes + code }; Self { display_planes, diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index 8228d97..ee46f28 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -3679,6 +3679,16 @@ pub fn render_from_input(input: &RenderInput, fb: &mut [u32]) -> RenderResult { if captured_row.is_none() && !control.bitplane_dma_enabled() { continue; } + // Denise's playfield output arms on BPL1DAT loads. A mode whose + // fetch table carries no plane streams at all (overprogrammed + // hi-res/SHRES BPU) never loads BPL1DAT, so nothing displays - + // the non-DMA planes' latches are not painted on their own + // (hardware-verified: the invplanes1 A500 photo shows black for + // hi-res BPU=7 despite armed BPL5DAT/BPL6DAT latches). Manual + // BPL1DAT writes still display through the manual-BPL replay. + if captured_row.is_none() && dma_planes == 0 { + continue; + } for (plane, words) in row_words.iter_mut().enumerate() { words.clear(); if plane < nplanes { diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index a28d837..16691dc 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -1752,7 +1752,10 @@ fn shres_limits_bitplane_depth_and_disables_ham() { }; assert_eq!(state.nplanes(), 2); - assert_eq!(state.dma_planes(), 2); + // Agnus's SHRES fetch-unit table schedules at most 2 plane streams; + // an overprogrammed BPU (here 6) fetches nothing at all (the same + // hardware rule as hi-res BPU>4, invplanes1 photo). + assert_eq!(state.dma_planes(), 0); assert!(!state.hold_and_modify()); }