From 56232693ae4f8b556135324bfdd245b1dba740ab Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Sat, 4 Jul 2026 08:06:22 +0100 Subject: [PATCH] denise: early-DDF lo-res picture placement is linear in DDFSTRT Remove the lo-res FMODE=0 "-1 sample" phase correction for DDFSTRT values before the standard $38 slot. Real hardware places a DDF-$30 lo-res picture exactly one 8-cck fetch period (16 lo-res pixels) left of the $38 picture: in the vAmigaTS Agnus/DIW/OLDDIW/diw1 A500 photos (OCS and ECS) the block-1 staircase pairs DIWSTRT hstart values ($94,$95)($96,$97) ($98,$99)($9A,$9B) on shared first-visible-stripe cells with plateau lengths [6,6,6,6,3,4] lines, matching only the linear grid, and the first plateau's indent against the standard rows measures 40 framebuffer px where the corrected grid predicts 36. The correction was compensating for the display window edge sitting one lo-res pixel too far right (2H-194 instead of the hardware's 2H-196, measured on the sblit0_A500_ECS.jpeg partial swatch columns): the too-wide window overran the fetched row's right edge on early-DDF screens. The window-edge anchor is fixed separately. vAmigaTS scores (vs vAmiga 4.4 refs): OLDDIW diw1 59.4%->1.5%, diw7 67.2%->0.4%, diw9 58.6%->0.0%, diwvert1 64.5%->1.2%; DIWH minmax 9.1%->0.6%, diwtim1b-4b 14.2%->0.5%; Agnus/DDF lupo1 39.8%->9.2%, hwstop7 22.1%->2.3%; Denise/Sprites improve broadly (sprites7 36.7%->24.9%). Gen-X, Hamazing, kicka1200 (hires fm3) and Zool (AGA) screenshots are byte-identical to main; Inside the Machine's overscan bitmap moves exactly 2 px left as predicted. --- docs/internals/video.md | 14 ++++++------- src/video/bitplane.rs | 25 ++++++++++------------- src/video/bitplane/tests.rs | 40 +++++++++++++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 26 deletions(-) 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]);