diff --git a/src/bus/tests.rs b/src/bus/tests.rs index f87da27..c05985b 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -4548,11 +4548,13 @@ fn beam_timed_bplcon0_hires_narrows_later_bitplane_pixels() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - // Content columns sit 2 fb px right of the hardware window edge - // (bitmap positions are beam-anchored; STANDARD_VISIBLE_X0 moved to 62). - assert_eq!(fb[STANDARD_VISIBLE_X0 + 34], rgb12_to_rgba8(0x0000)); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 36], rgb12_to_rgba8(0x0F00)); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 37], rgb12_to_rgba8(0x0000)); + // Content columns sit flush against the hardware window edge + // (STANDARD_VISIBLE_X0 = 62): standard hi-res places its first fetched + // sample at the window edge, so the set bit lands two fb px left of the + // old beam-anchored placement. + assert_eq!(fb[STANDARD_VISIBLE_X0 + 32], rgb12_to_rgba8(0x0000)); + assert_eq!(fb[STANDARD_VISIBLE_X0 + 34], rgb12_to_rgba8(0x0F00)); + assert_eq!(fb[STANDARD_VISIBLE_X0 + 35], rgb12_to_rgba8(0x0000)); } #[test] diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index c2e775f..47f2cc0 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -44,22 +44,27 @@ const PAL_VISIBLE_LINE0: i32 = 0x2C; // which show the bitmap's first lo-res pixel fully visible at the edge). const DIW_HSTART_FB0: i32 = 0x62; const STANDARD_DIW_HSTART: i32 = 0x81; -// Standard DIWSTRT $81 is the visible window edge. The first standard -// bitplane sample at DDFSTRT $38 is already one lowres native sample into the -// fetched word, so the fetch/output phase is referenced one color clock earlier. +// Standard DIWSTRT $81 is the visible window edge. Both a standard lo-res +// ($38 DDF) and standard hi-res ($3C DDF) picture start their first fetched +// sample flush at that edge: the references position bitplane sample 0 at +// framebuffer x = (reference - DIW_HSTART_FB0) * 2, and DIW_HSTART_FB0 (0x62) +// with reference 0x81 puts sample 0 at x = 62, exactly where the 2H-196 +// comparator opens the window. A standard 20-word lo-res / 40-word hi-res row +// then fills the window's 320 lo-res / 640 hi-res samples flush to both edges, +// matching vAmiga (verified on the vAmigaTS Denise/Registers/BPLCON0/modes +// A500 references, which reach 0.000% only when the hi-res picture sits flush). // -// The lo-res fetch/display phase sits 3 colour clocks later than the hi-res -// phase: a hi-res fetch slot delivers its word to Denise's shifter on a -// different beam edge than a lo-res slot, so the reference the renderer uses to -// place the first fetched pixel differs by resolution. The references position -// bitplane sample 0 at framebuffer x = (reference - DIW_HSTART_FB0) * 2, so -// they moved +1 in lockstep with the DIW_HSTART_FB0 comparator fix to keep the -// hardware-calibrated bitmap positions (lo-res sample 0 at x = 62, hi-res -// standard $81/$3C flush at x = 64). The standard $81 window edge now sits at -// x = 62 too, exposing the lo-res bitmap's first sample as on real hardware. +// Hi-res briefly used reference 0x82: that kept the picture "beam-anchored" at +// x = 64 after the comparator moved to x = 62 (2H-196), which left the picture +// two hi-res pixels inside the window and clipped its rightmost fetched pixel +// against the window's closing edge -- e.g. the AmigaDOS window's right border +// on KS1.3 (the "binary" demo, r.adf) vanished. Because hi-res has one +// framebuffer pixel per sample (lo-res has two), the +1 reference bump that +// kept lo-res flush over-shifted hi-res by two, so hi-res needs the same 0x81 +// reference as lo-res to stay flush at the window edge. // See `fetch_reference` below. const DIW_HSTART_FETCH_REFERENCE_LORES: i32 = 0x81; -const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x82; +const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x81; // Register/copper-write x=0 anchor, in colour clocks. Moved left by 8 colour // clocks in lockstep with DIW_HSTART_FB0 (16 lo-res pixels) so register writes // and bitplane pixels still register against each other after widening. @@ -637,26 +642,33 @@ fn enforce_h_window_closed_intervals( let mut sx = x; while sx < closed_end { let control = control_at_x(base_controls[y], &control_segments[y], sx); - let next_ctl = control_segments[y] + // The border colour follows COLOR00 per pixel, so a run ends at + // the next control OR palette (colour) segment -- not just the + // next control change. Splitting on control alone painted a + // whole control-run with COLOR00 sampled at its start, dropping + // any mid-run colour write (a copper COLOR00 change inside the + // left border, as in copper-chunky banners). + let next_bound = control_segments[y] .iter() .map(|seg| seg.x) + .chain(palette_segments[y].iter().map(|seg| seg.x)) .filter(|&b| b > sx) .min() .unwrap_or(FB_WIDTH) .min(closed_end); if !control.display_window_contains_line(y, visible_line0) { // Row is outside the vertical window here: already border. - sx = next_ctl; + sx = next_bound; continue; } if control.border_sprite_enabled() { - sx = next_ctl; + sx = next_bound; continue; } let palette = palette_at_x(base_palettes[y], &palette_segments[y], sx); let pixel = background_pixel(&control, palette[0], true); - row[sx..next_ctl].fill(pixel); - sx = next_ctl; + row[sx..next_bound].fill(pixel); + sx = next_bound; } x = closed_end; } diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index 0c06f42..24a9029 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -279,9 +279,11 @@ fn early_ddf_hires_origin_snaps_to_word_boundary() { assert_eq!(standard.native_x_offset(standard.diw_h_start(), repeat), 0); // Kickstart 2.05 insert-disk screen: hi-res but DDFSTRT=$40 (late), so - // there is no pre-fetch word. The late fetch keeps its beam-anchored - // position (absolute picture x is unchanged by the DIW comparator fix; - // the offset shrinks by the 2 hi-res px the window edge moved left). + // there is no pre-fetch word; the fetch origin sits inside the window and + // the left border shows the pre-window samples. With the hi-res fetch + // reference sharing the lo-res 0x81 anchor (both flush at the 2H-196 window + // edge), the window's first visible sample is 24 hi-res samples into the + // fetched row. The Agnus/DDF vAmigaTS references improve, none regress. let ks_boot = ControlState { diwstrt: 0x2C95, diwstop: 0x2CAD, @@ -289,7 +291,7 @@ fn early_ddf_hires_origin_snaps_to_word_boundary() { ddfstop: 0x00D0, ..xsysinfo }; - assert_eq!(ks_boot.native_x_offset(ks_boot.diw_h_start(), repeat), 22); + assert_eq!(ks_boot.native_x_offset(ks_boot.diw_h_start(), repeat), 24); } fn ocs_snapshot(diwstrt: u16, diwstop: u16, ddfstrt: u16, ddfstop: u16) -> RenderRegisterSnapshot { @@ -1013,6 +1015,58 @@ fn beam_timed_bplcon3_brdrblnk_latches_until_ecsena_enables_effect() { assert_eq!(fb[40], rgb12_to_rgba8_alpha(0, false)); } +#[test] +fn closed_interval_repaint_follows_copper_color00_writes() { + // Copper-chunky banners (e.g. the "binary" logo's red bar) paint colour + // bars by rewriting COLOR00 across the horizontal border, where the DIW + // flip-flop has closed the display window. The closed-interval border + // repaint must sample COLOR00 per colour-write, not once per control run: + // sampling once painted the whole left border with the frame-start COLOR00 + // and dropped the copper's mid-border colour change, clipping the bar's + // left edge back to the first control boundary. + let control = visible_lowres_control(0); + let mut palette = Palette::from_ocs([0; 32]); + palette.write_ocs(0, 0x0123); // dark frame-start COLOR00 + let base_palettes = [palette]; + // Copper writes COLOR00 = red partway across the closed left border. + let palette_segments = vec![vec![PaletteSegment { + x: 40, + entry: 0, + loct: false, + value: 0x0C00, + }]]; + let base_controls = [control]; + let control_segments = vec![Vec::new()]; + // Flip-flop closed over [0, 100); open to the right of it. + let h_window_rows = vec![HWindowRow { + open_runs: vec![(100, FB_WIDTH)], + comparator_anchor: Some(100), + }]; + let mut fb = vec![0u32; FB_WIDTH]; + + enforce_h_window_closed_intervals( + &mut fb, + &base_palettes, + &palette_segments, + &base_controls, + &control_segments, + &h_window_rows, + PAL_VISIBLE_LINE0, + 1, + ); + + let dark = background_pixel(&control, 0x0123, true); + let red = background_pixel(&control, 0x0C00, true); + // Border left of the copper write keeps the frame-start colour... + assert_eq!(fb[20], dark); + assert_eq!(fb[38], dark); + // ...and follows the red COLOR00 write for the rest of the closed border. + assert_eq!(fb[40], red); + assert_eq!(fb[98], red); + // The open interval to its right is left untouched by the repaint. + assert_eq!(fb[100], 0); +} + #[test] fn native_x_offset_accounts_for_diw_and_ddf_alignment() { let standard_hires = RenderState { @@ -1025,15 +1079,17 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { // FMODE=0: the fetch gulp equals the DDF granularity, so the picture // follows DDFSTRT continuously. KS 2.05's insert-disk screen (DDF - // $40, DIW $95) keeps the same late-DDF relation after the standard - // $81/$3C hi-res phase is aligned to the display-window edge. + // $40, DIW $95) is a late-DDF hi-res picture whose left border shows the + // samples fetched before the window opens: with the hi-res reference flush + // at the 2H-196 window edge the first visible sample is 24 hi-res samples + // into the row (Agnus/DDF vAmigaTS references improve, none regress). let kickstart_hires = RenderState { bplcon0: 0x8000, diwstrt: 0x6395, ddfstrt: 0x0040, ..blank_state() }; - assert_eq!(kickstart_hires.native_x_offset(true, 1), 22); + assert_eq!(kickstart_hires.native_x_offset(true, 1), 24); // Wide FMODE fetches quantize the displayed shifter origin to the gulp // grid: AGA system screens program DDFSTRT $38 or $3C interchangeably @@ -1066,10 +1122,10 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { // FS-UAE. DDFSTRT $38 and $3C are still equivalent because both align // to the same wide-FMODE gulp. assert_eq!(wb_hires_overscan_fetch.native_x_offset(true, 1), 0); - // The hi-res picture keeps its beam-anchored position (x = 64); the - // hardware window edge (62) now opens one lo-res pixel earlier, so the - // picture starts 2 hi-res px into the window. - assert_eq!(wb_hires_overscan_fetch.fetch_start_native_x(true, 1), 2); + // The hi-res picture sits flush against the 2H-196 window edge (x = 62), + // same as lo-res: its first fetched sample is the window's first visible + // pixel, so no samples are consumed in the border before it opens. + assert_eq!(wb_hires_overscan_fetch.fetch_start_native_x(true, 1), 0); // The placement gulp grid runs on absolute colour-clock multiples of // the fetch period. Lores FMODE=1 has a 16-cck gulp: DDFSTRT $30 is @@ -1109,9 +1165,13 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { assert_eq!(diagrom_hires.display_window_x().0, 62); assert_eq!(diagrom_hires.clipped_display_pixels_before_frame(), 0); assert_eq!(diagrom_hires.native_x_offset(true, 1), 0); - // Standard hi-res keeps its beam-anchored position (x = 64), 2 hi-res - // px inside the hardware window edge (62). - assert_eq!(diagrom_hires.fetch_start_native_x(true, 1), 2); + // Standard hi-res ($81/$3C) sits flush against the 2H-196 window edge + // (x = 62): its first fetched sample is the window's first visible pixel, + // so a full 40-word row fills the 640-sample window to both edges. When + // hi-res used a +1 (0x82) reference it started 2 hi-res px inside the + // window and clipped its rightmost fetched pixel -- the AmigaDOS window's + // right border vanished on KS1.3 (the "binary" demo, r.adf). + assert_eq!(diagrom_hires.fetch_start_native_x(true, 1), 0); let lores_extra_fetch_word = RenderState { bplcon0: 0,