diff --git a/docs/internals/video.md b/docs/internals/video.md index abf7d70..110aa63 100644 --- a/docs/internals/video.md +++ b/docs/internals/video.md @@ -36,13 +36,13 @@ visible output one native sample into the fetched stream, so replay pre-advances that hidden sample before painting the DIW edge. Extra fetch groups from an earlier DDFSTRT are not decoded into the HAM hold colour before DIW opens; they are fetched by Agnus, but the first visible HAM history is -bounded to the display-phase samples. Single-word lo-res fetches that start -before the standard `$38` DDF slot expose complete 16-pixel groups; the -standard one-sample phase bias is trimmed when it would push a standard-width -DIW past the completed early-DDF row at the right edge. -Late single-word lo-res DDF keeps the standard DIW `$81` one-sample phase; -the renderer must not subtract an extra sample just to align the clipped -start to a fetch-unit boundary. +bounded to the display-phase samples. Single-word lo-res fetch placement is linear in DDFSTRT: each 8-cck fetch +period before the standard `$38` slot moves the picture exactly 16 lo-res +pixels left, keeping the standard one-sample phase bias (hardware-verified +against the vAmigaTS `Agnus/DIW/OLDDIW/diw1` A500 photos, OCS and ECS). +Early and late single-word lo-res DDF both keep the standard DIW `$81` +one-sample phase; the renderer must not add or subtract a sample just to +align the picture to a fetch-unit boundary. When DDFSTRT is late enough that DIW opens before DMA has delivered the first BPL1DAT word for the row, playfield output remains border-colour until that plane-0 fetch reaches Denise instead of sampling stale shifter contents. diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index 93a7194..06b8ef7 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -1200,22 +1200,17 @@ impl ControlState { // and lost its right edge off the framebuffer. let clamped_window_native = ((DIW_HSTART_FB0 - diw_h_start as i32).max(0) * 2) / pixel_repeat as i32; + // Lo-res FMODE=0 placement is linear in DDFSTRT: moving DDFSTRT one + // 8-cck fetch period earlier moves the picture exactly 16 lo-res + // pixels left. Real hardware confirms this (vAmigaTS + // Agnus/DIW/OLDDIW/diw1 A500 photos: the DDF-$30 stripe grid sits + // exactly 16 lo-res pixels left of the standard $38 grid, and the + // window-edge staircase steps pair on that grid). An earlier + // "-1 sample" phase correction here compensated for the display + // window edge sitting one lo-res pixel too far right, which made a + // standard DIW overrun the fetched row at the right edge on + // early-DDF screens; the picture phase itself was never non-linear. let mut origin_shift = display_native_shift - ddf_native_shift + clamped_window_native; - let ddf_start = effective_ddf_start_hpos( - self.agnus_revision, - self.hires() || self.shres(), - self.ddfstrt, - ); - if !self.hires() && !self.shres() && self.fetch_quantum() == 1 { - let ddf = i32::from(ddf_start); - if ddf < standard_ddf && origin_shift > 0 { - // Single-word lo-res fetches that start before the standard $38 - // slot expose whole 16-pixel groups. The standard one-sample - // lo-res phase bias must not push a standard-width DIW one sample - // past that completed early-DDF row at the right edge. - origin_shift -= 1; - } - } // A hi-res/SHRES FMODE=0 screen that starts DDFSTRT earlier than the // standard $3C slot pre-fetches whole word(s) before the display window // opens. On real hardware those words are shifted into the left border diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index 9d76f0c..f71f2df 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -1133,9 +1133,38 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { lores_early_fetch_standard_window.words_per_row(false, 0), 21 ); + // Linear DDFSTRT placement: $30 fetches one 8-cck period (16 lo-res + // pixels) earlier than the standard $38, on top of the standard + // one-sample lo-res phase bias. Hardware-verified via the vAmigaTS + // Agnus/DIW/OLDDIW/diw1 A500 photos (OCS and ECS). assert_eq!( lores_early_fetch_standard_window.native_x_offset(false, 2), - 16 + 17 + ); +} + +#[test] +fn lores_early_ddf_picture_sits_linearly_left_of_standard_ddf() { + // A lo-res FMODE=0 picture fetched at DDFSTRT $30 sits exactly 16 lo-res + // pixels (one 8-cck fetch period) left of the same picture fetched at + // $38; the DDF->position mapping is linear. Real hardware confirms + // (vAmigaTS Agnus/DIW/OLDDIW/diw1 photos: DDF-$30 stripe grid exactly + // one fetch period left of the standard grid). + let standard = RenderState { + bplcon0: 0, + diwstrt: ((PAL_VISIBLE_LINE0 as u16) << 8) | STANDARD_DIW_HSTART as u16, + ddfstrt: 0x0038, + ddfstop: 0x00D0, + ..blank_state() + }; + let early = RenderState { + ddfstrt: 0x0030, + ..standard + }; + assert_eq!( + early.native_x_offset(false, 2) - standard.native_x_offset(false, 2), + 16, + "DDFSTRT $30 must sit exactly 16 lo-res px left of $38" ); } @@ -5041,8 +5070,8 @@ fn planned_ham_dma_advances_hold_through_edge_fetch_phase() { fn planned_ham_dma_ignores_extra_early_ddf_history_before_diw() { let mut row_words = vec![vec![0; 2]; 6]; row_words[0][0] |= 0x8000; // native x 0: direct palette entry 1 - row_words[4][0] |= 0x0001; // native x 15: HAM blue := 0 - row_words[4][1] |= 0x8000; // native x 16: HAM blue := 0 + row_words[4][1] |= 0x8000; // native x 16: HAM blue := 0 (hidden, pre-DIW) + row_words[4][1] |= 0x4000; // native x 17: HAM blue := 0 (first visible) let line_plan = DenisePlannedPlayfieldLine::new(0, 64, 66, &row_words, 32); let mut control = visible_lowres_control(0x6800); control.diwstrt = ((PAL_VISIBLE_LINE0 as u16) << 8) | STANDARD_DIW_HSTART as u16; @@ -5077,7 +5106,10 @@ fn planned_ham_dma_ignores_extra_early_ddf_history_before_diw() { 0, ); - assert_eq!(control.native_x_offset(control.diw_h_start(), 2), 16); + // 17 = one 8-cck fetch period (16 lo-res px) before the standard $38 + // origin plus the standard one-sample phase bias: DDFSTRT placement is + // linear (hardware-verified, vAmigaTS Agnus/DIW/OLDDIW/diw1 photos). + assert_eq!(control.native_x_offset(control.diw_h_start(), 2), 17); assert_eq!(fb[64], rgb12_to_rgba8(0x0000)); assert_eq!(fb[65], rgb12_to_rgba8(0x0000)); assert_eq!(&playfield_mask[64..66], &[0x02, 0x02]);