diff --git a/docs/internals/video.md b/docs/internals/video.md index 2bbe289..1565b50 100644 --- a/docs/internals/video.md +++ b/docs/internals/video.md @@ -46,6 +46,12 @@ against the vAmigaTS `Agnus/DIW/OLDDIW/diw1` A500 photos, OCS and ECS). Early and late single-word lo-res DDF keep the picture beam-anchored; the renderer must not add or subtract a sample just to align the picture to a fetch-unit boundary. +Hi-res early DDF is beam-anchored the same way: content fetched ahead of the +window edge is hidden by the window comparator alone (XSysInfo's DDFSTRT `$38` +panel clips exactly its one pre-fetch word), so when an extreme-overscan +screen opens the window early as well (KS 3.2 Overscan editor on ECS: +DDFSTRT `$28` with DIWSTRT h `$5D`), the early words are visible inside the +window rather than being snapped away (issue #186). 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 e085eb9..db4fc52 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -1342,27 +1342,17 @@ impl ControlState { // 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; - // 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 - // ahead of DIWSTRT and never appear inside the window; the negative - // `ddf_native_shift` is exactly that early pre-fetch width in pixels. - // Snap the picture origin up to the first fully-fetched word so the - // pre-fetch word is clipped on the left AND the real right edge (the - // last visible word) stays inside the window instead of being cropped. - // - // Confirmed against vAmiga for XSysInfo's hardware panel (hi-res, - // DDFSTRT=$38, DIWSTRT=$81, BPL1MOD/BPL2MOD=-4): a clean left edge with - // the >OVERVIEW box keeping its right border. With the negative modulo - // that pre-fetch word equals the previous row's right-edge word, so - // without this the right edge both bled into the left column one - // scanline down and was cropped off the right. Late DDFSTRT (e.g. the - // Kickstart insert-disk screen's $40, `ddf_native_shift >= 0`), lo-res, - // and wide-FMODE fetches are untouched. - if (self.hires() || self.shres()) && self.fetch_quantum() == 1 && ddf_native_shift < 0 { - origin_shift = origin_shift.max(-ddf_native_shift); - } + let origin_shift = display_native_shift - ddf_native_shift + clamped_window_native; + // No extra clipping of early-DDF hi-res pre-fetch words happens here: + // content fetched ahead of the window edge is exactly the positive + // part of `origin_shift`, so the window comparator already hides it + // (XSysInfo's hardware panel: DDFSTRT=$38, DIWSTRT=$81 -> the one + // pre-fetch word is the 16 skipped samples). When the display window + // itself opens left of the standard edge (ECS/AGA extreme-overscan + // screens: DDFSTRT=$28, DIWSTRT h=$5D), those early words ARE inside + // the window and must be shown - an unconditional snap to the early + // fetch width shifted the whole picture left and left a blank + // right-edge band (issue #186). // A playfield whose BPLCON1 scroll covers an off-grid fetch phase // catches the floor reload slot instead of the rounded-up one (see // `reload_advance_for_scroll`): extend the fetch span left by the diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index 937c89c..aaf2ed2 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -246,18 +246,16 @@ fn content_window_h_matches_standard_diw_for_stock_ddf() { } #[test] -fn early_ddf_hires_origin_snaps_to_word_boundary() { +fn early_ddf_hires_origin_clips_prefetch_against_the_window_edge() { // XSysInfo's hardware-information panel: hi-res, FMODE=0, DDFSTRT=$38 // (the lo-res "Normal" slot) - one 4-cck fetch word earlier than the // hi-res standard $3C - with DIWSTRT=$81 and BPL1MOD/BPL2MOD=-4. The // early pre-fetch word is clocked into the left border before the - // window opens, so the picture origin must snap up to the first - // fully-fetched word: native x-offset 16 (one 16-pixel word) instead of - // the unsnapped 12. That clips the pre-fetch word on the left AND keeps - // the real right edge inside the window. Without it the negative modulo - // made that word both bleed into the left column one scanline down and - // crop off the right edge. Confirmed against vAmiga (clean left edge, - // the >OVERVIEW box keeps its right border). + // window opens: native x-offset 16 (one 16-pixel word) skips it. With + // the negative modulo that word equals the previous row's right-edge + // word; showing it bled that edge into the left column one scanline + // down and cropped it off the right. Confirmed against vAmiga (clean + // left edge, the >OVERVIEW box keeps its right border). let xsysinfo = ControlState { agnus_revision: AgnusRevision::AgaAlice, bplcon0: 0x8200, @@ -292,6 +290,30 @@ fn early_ddf_hires_origin_snaps_to_word_boundary() { ..xsysinfo }; assert_eq!(ks_boot.native_x_offset(ks_boot.diw_h_start(), repeat), 24); + + // ECS extreme-overscan hi-res screen (KS 3.2 Overscan editor's edit + // display, issue #186): DDFSTRT=$28 fetches 80 hi-res px ahead of the + // standard slot, but DIWSTRT h=$5D opens the window 72 px early too, so + // nearly all of that early content is INSIDE the window. Only the part + // left of the framebuffer origin is skipped (10 px window clamp plus the + // 8 px between the window edge and the reference): origin 18, not the + // early-fetch width 80. Snapping to 80 shifted the whole picture left + // and left a blank band at the right window edge. + let overscan = ControlState { + agnus_revision: AgnusRevision::Ecs8375, + bplcon0: 0x9200, + diwstrt: 0x1D5D, + diwstop: 0x38C7, + diwhigh: DiwHigh::ecs_explicit(0x2100), + ddfstrt: 0x0028, + ddfstop: 0x00D8, + bplcon1: 0x0044, + ..xsysinfo + }; + assert_eq!(overscan.diw_h_start(), 0x5D); + // (0x5D - 0x81)*2 display shift, -(0x3C - 0x28)*4 DDF shift, +(0x62 - + // 0x5D)*2 framebuffer-origin clamp = 18. + assert_eq!(overscan.native_x_offset(overscan.diw_h_start(), repeat), 18); } fn ocs_snapshot(diwstrt: u16, diwstop: u16, ddfstrt: u16, ddfstop: u16) -> RenderRegisterSnapshot {