diff --git a/docs/internals/video.md b/docs/internals/video.md index 110aa63..5c48eda 100644 --- a/docs/internals/video.md +++ b/docs/internals/video.md @@ -31,17 +31,20 @@ pipeline carries 24-bit colour end to end; OCS/ECS paths keep their exact under playfield priority, and CLXDAT collisions are accumulated. For DMA-fetched HAM playfields, the display window gates framebuffer output and collision recording, but the low-res Denise phase can still seed the HAM -component history just before DIW: standard `$38` DDF timing starts the first -visible output one native sample into the fetched stream, so replay -pre-advances that hidden sample before painting the DIW edge. Extra fetch +component history just before DIW: when the window opens to the right of the +fetch origin (a late DIWSTRT, or an early DDFSTRT), replay pre-advances the +hidden samples before painting the DIW edge. The standard `$81` window edge +is flush with the standard `$38` picture (both at framebuffer x 62, +hardware-verified on the sblit0 A500 photo), so a stock screen hides no +samples. 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 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 +pixels left (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 +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. 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 @@ -139,9 +142,11 @@ The mapping from beam coordinates to framebuffer x is anchored by constants that encode the hardware's fetch-to-display pipeline delays -- register writes, palette writes, and bitplane data each land at their own documented offset, and the bitplane fetch reference differs between lo-res -and hi-res. A standard hi-res `$81` DIW with `$3C` DDF starts its 640 fetched -pixels at the display-window edge; there is no four-pixel leading border -inside the window. Wide-FMODE DMA fetches start from the revision-masked +and hi-res. The display-window comparator maps a DIWSTRT hstart H to +framebuffer x = 2H - 196 (hardware-verified against the sblit0 A500 photo). +A standard lo-res `$81`/`$38` picture is flush with that edge; a standard +hi-res `$81`/`$3C` picture starts its 640 fetched pixels one lo-res pixel +inside the window (matching vAmiga), with no wider leading border. Wide-FMODE DMA fetches start from the revision-masked DDFSTRT comparator value and complete whole units, but the displayed shifter origin is still quantized by the FMODE fetch gulp; the renderer keeps those two effects separate. Denise's output line starts at the horizontal blanking diff --git a/src/bus.rs b/src/bus.rs index fcc7569..9b2afa0 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -228,20 +228,24 @@ const PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS: u32 = 0x19; const NTSC_SPRITE_DMA_FIRST_ACTIVE_VPOS: u32 = 0x14; const RENDER_VISIBLE_LINES: usize = FB_HEIGHT; const RENDER_FRAMEBUFFER_WIDTH: i32 = FB_WIDTH as i32; -// Capture-side twin of `bitplane::DIW_HSTART_FB0`; held 8 colour clocks (16 -// lo-res pixels) left of the standard display start so the captured window -// matches vAmiga's 716-wide cutout and includes the deep-left overscan. -const RENDER_DIW_HSTART_FB0: i32 = 0x61; +// Capture-side twin of `bitplane::DIW_HSTART_FB0`; held deep left of the +// standard display start so the captured window matches vAmiga's 716-wide +// cutout and includes the deep-left overscan. Like the renderer twin, this +// puts the hardware-verified standard $81 window edge (and the sprite +// comparator positions, which share Denise's counter) at framebuffer x = 62. +const RENDER_DIW_HSTART_FB0: i32 = 0x62; // 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. // // Capture-side twins of `bitplane::DIW_HSTART_FETCH_REFERENCE_*`. The hi-res // fetch/display phase sits 3 colour clocks earlier than lo-res, so the -// reference differs by resolution (lo-res $80, hi-res $83). See the bitplane -// constant docs for the vAmiga-verified rationale. -const RENDER_DIW_HSTART_FETCH_REFERENCE_LORES: i32 = 0x80; -const RENDER_DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x83; +// reference differs by resolution (lo-res $81, hi-res $84). Moved +1 in +// lockstep with RENDER_DIW_HSTART_FB0 so captured bitmap positions stay at +// their hardware-calibrated framebuffer columns. See the bitplane constant +// docs for the vAmiga-verified rationale. +const RENDER_DIW_HSTART_FETCH_REFERENCE_LORES: i32 = 0x81; +const RENDER_DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x84; // Capture-side twin of `bitplane::COPPER_WAIT_HPOS_FB0`; moved left by 8 colour // clocks in lockstep with RENDER_DIW_HSTART_FB0. const RENDER_COPPER_WAIT_HPOS_FB0: u32 = 0x28; diff --git a/src/bus/tests.rs b/src/bus/tests.rs index f488a68..698a076 100644 --- a/src/bus/tests.rs +++ b/src/bus/tests.rs @@ -3399,7 +3399,9 @@ fn captured_bitplane_rows_render_after_later_dmacon_clears_bplen() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - assert_eq!(fb[STANDARD_VISIBLE_X0], rgb12_to_rgba8(0x0F00)); + // 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 + 2], rgb12_to_rgba8(0x0F00)); } #[test] @@ -3442,8 +3444,13 @@ fn beam_timed_display_window_changes_clip_later_bitplane_rows() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - assert_eq!(fb[STANDARD_VISIBLE_X0], rgb12_to_rgba8(0x0F00)); - assert_eq!(fb[FB_WIDTH + STANDARD_VISIBLE_X0], rgb12_to_rgba8(0x0000)); + // 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 + 2], rgb12_to_rgba8(0x0F00)); + assert_eq!( + fb[FB_WIDTH + STANDARD_VISIBLE_X0 + 2], + rgb12_to_rgba8(0x0000) + ); } #[test] @@ -3563,8 +3570,10 @@ fn beam_timed_diwstrt_clips_hidden_bitplane_pixels_without_rebasing_fetch_origin let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 4], rgb12_to_rgba8(0x0000)); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 6], rgb12_to_rgba8(0x0F00)); + // 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 + 6], rgb12_to_rgba8(0x0000)); + assert_eq!(fb[STANDARD_VISIBLE_X0 + 8], rgb12_to_rgba8(0x0F00)); } #[test] @@ -3767,8 +3776,13 @@ fn beam_timed_bitplane_pointer_changes_later_fallback_fetch_rows() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - assert_eq!(fb[STANDARD_VISIBLE_X0], rgb12_to_rgba8(0x0F00)); - assert_eq!(fb[FB_WIDTH + STANDARD_VISIBLE_X0], rgb12_to_rgba8(0x0000)); + // 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 + 2], rgb12_to_rgba8(0x0F00)); + assert_eq!( + fb[FB_WIDTH + STANDARD_VISIBLE_X0 + 2], + rgb12_to_rgba8(0x0000) + ); } #[test] @@ -3800,7 +3814,9 @@ fn beam_timed_bitplane_pointer_changes_later_fallback_fetch_words() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - let x_start = STANDARD_VISIBLE_X0 + 64; + let x_start = STANDARD_VISIBLE_X0 + 66; + // 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[x_start], rgb12_to_rgba8(0x0F00)); assert_eq!(fb[x_start + 32], rgb12_to_rgba8(0x0000)); } @@ -4376,9 +4392,11 @@ fn beam_timed_bplcon0_hires_narrows_later_bitplane_pixels() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - 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)); + // 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)); } #[test] @@ -4419,9 +4437,11 @@ fn beam_timed_bplcon0_lowres_widens_later_bitplane_pixels() { let mut fb = vec![0; FB_PIXELS]; bitplane::render(&mut bus, &mut fb); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 30], rgb12_to_rgba8(0x0000)); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 32], rgb12_to_rgba8(0x0F00)); - assert_eq!(fb[STANDARD_VISIBLE_X0 + 33], rgb12_to_rgba8(0x0F00)); + // 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 + 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(0x0F00)); } #[test] @@ -6348,7 +6368,10 @@ fn attached_manual_sprite_data_writes_accumulate_live_sprite_sprite_clxdat() { #[test] fn attached_manual_sprite_odd_data_writes_accumulate_later_live_sprite_sprite_clxdat() { let mut bus = empty_bus(); - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0083); + // hstart +1 vs the pre-fix value: sprite comparator positions share + // Denise's counter and moved with the corrected window-edge anchor + // (2H-196); the beam-anchored playfield sample did not. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0084); bus.agnus.vpos = 0x2C; bus.agnus.hpos = 0x38; bus.denise.diwstrt = 0x2C83; @@ -6474,7 +6497,10 @@ fn manual_sprite_position_write_on_compare_boundary_preserves_live_source() { #[test] fn manual_sprite_data_writes_accumulate_live_sprite_playfield_clxdat() { let mut bus = empty_bus(); - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0081); + // hstart +1 vs the pre-fix value: sprite comparator positions share + // Denise's counter and moved with the corrected window-edge anchor + // (2H-196); the beam-anchored playfield sample did not. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0082); bus.agnus.vpos = 0x2C; bus.agnus.hpos = 0x38; bus.denise.diwstrt = 0x2C81; @@ -6810,11 +6836,13 @@ fn shifted_horizontal_diw_offsets_live_playfield_clxdat_fetch_origin() { #[test] fn denise_horizontal_delay_aligns_sprite_playfield_collision_domain() { let display_x = live_display_window_x(0x2C81, 0x2DC1, DiwHigh::ocs_implicit()).0; - let copper_hpos = RENDER_COPPER_WAIT_HPOS_FB0 + (display_x as u32 / 4); + // The hardware window edge (62) is off the 4-px copper/register grid; + // the nearest register-domain position maps one lo-res pixel later. + let copper_hpos = RENDER_COPPER_WAIT_HPOS_FB0 + ((display_x as u32 + 2) / 4); assert_eq!(display_x, STANDARD_VISIBLE_X0 as i32); assert_eq!( framebuffer_x_for_live_collision_hpos(copper_hpos), - display_x + display_x + 2 ); let row = CapturedBitplaneRow { @@ -7286,7 +7314,10 @@ fn same_line_bplcon0_dual_playfield_enable_does_not_retime_earlier_live_clxdat() fn captured_sprite_and_bitplane_rows_accumulate_live_sprite_playfield_clxdat() { let mut bus = empty_bus(); let sprite_ptr = 0x0300usize; - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0081); + // hstart +1 vs the pre-fix value: sprite comparator positions share + // Denise's counter and moved with the corrected window-edge anchor + // (2H-196); the beam-anchored playfield sample did not. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0082); write_chip_word(&mut bus, sprite_ptr, pos); write_chip_word(&mut bus, sprite_ptr + 2, ctl); write_chip_word(&mut bus, sprite_ptr + 4, 0x8000); @@ -7377,7 +7408,10 @@ fn manual_sprite_and_bpl1dat_writes_accumulate_live_sprite_playfield_clxdat() { fn same_line_bplcon1_scroll_increase_latches_later_live_sprite_playfield_clxdat() { let mut bus = empty_bus(); let sprite_ptr = 0x0300usize; - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0093); + // hstart +1 vs the pre-fix value: sprite comparator positions share + // Denise's counter and moved with the corrected window-edge anchor + // (2H-196); the beam-anchored playfield sample did not. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0094); write_chip_word(&mut bus, sprite_ptr, pos); write_chip_word(&mut bus, sprite_ptr + 2, ctl); write_chip_word(&mut bus, sprite_ptr + 4, 0x8000); @@ -7467,7 +7501,8 @@ fn bplcon3_spres_hires_narrows_live_sprite_playfield_clxdat() { let clxdat_after_bitplane_row_capture = |bplcon3| { let mut bus = empty_bus(); let sprite_ptr = 0x0300usize; - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0081); + // hstart +1 vs the pre-fix value: see the sibling clxdat tests. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0082); write_chip_word(&mut bus, sprite_ptr, pos); write_chip_word(&mut bus, sprite_ptr + 2, ctl); write_chip_word(&mut bus, sprite_ptr + 4, 0x8000); @@ -7512,7 +7547,8 @@ fn same_line_bplcon3_spres_write_does_not_retime_earlier_live_sprite_playfield_c let clxdat_after_bitplane_row_capture = |spres_hpos: Option| { let mut bus = empty_bus(); let sprite_ptr = 0x0300usize; - let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0081); + // hstart +1 vs the pre-fix value: see the sibling clxdat tests. + let (pos, ctl) = sprite_control_words(0x2C, 0x2D, 0x0082); write_chip_word(&mut bus, sprite_ptr, pos); write_chip_word(&mut bus, sprite_ptr + 2, ctl); write_chip_word(&mut bus, sprite_ptr + 4, 0x8000); diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index 06b8ef7..73bcbca 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -35,12 +35,14 @@ use std::time::Instant; // Denise/Agnus scheduler. #[cfg_attr(not(test), allow(dead_code))] const PAL_VISIBLE_LINE0: i32 = 0x2C; -// Framebuffer x=0 anchor. Held 8 colour clocks (16 lo-res pixels) left of the -// standard display start so the framebuffer captures the deep-left overscan a -// real Denise can display, matching vAmiga's 716-wide regression cutout. The -// matching shift of COPPER_WAIT_HPOS_FB0 (below) keeps the bitplane/register -// pipeline delta (BITPLANE_CONTROL_PIPELINE_FB) invariant. -const DIW_HSTART_FB0: i32 = 0x61; +// Framebuffer x=0 anchor. Held deep left of the standard display start so the +// framebuffer captures the deep-left overscan a real Denise can display, +// matching vAmiga's 716-wide regression cutout. The value pins the DIW +// comparator mapping: a DIWSTRT hstart H opens the window at framebuffer +// x = (H - this) * 2, and real hardware puts the standard $81 edge at x = 62 +// (2H - 196; measured on the sblit0_A500_ECS.jpeg partial swatch columns, +// 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 @@ -49,12 +51,15 @@ const STANDARD_DIW_HSTART: i32 = 0x81; // 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. Lo-res references $80; -// hi-res references $81 so a standard $81/$3C display starts its 640 fetched -// pixels at the display-window edge instead of clipping the right edge. +// 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. // See `fetch_reference` below. -const DIW_HSTART_FETCH_REFERENCE_LORES: i32 = 0x80; -const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x81; +const DIW_HSTART_FETCH_REFERENCE_LORES: i32 = 0x81; +const DIW_HSTART_FETCH_REFERENCE_HIRES: i32 = 0x82; // 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. diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index f71f2df..5c64378 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -42,13 +42,13 @@ fn h_window_flop_carries_open_past_line_with_unreachable_hstart() { control: degenerate, }); let rows = compute_h_window_rows(&base_controls, &control_segments, PAL_VISIBLE_LINE0); - // Row 0: standard window. - assert_eq!(rows[0].open_runs(), &[(64, 704)]); + // Row 0: standard window (edge at 2H-196, hardware-verified). + assert_eq!(rows[0].open_runs(), &[(62, 702)]); // Row 1: opens at the standard hstart; the rewritten stop never // matches before the line ends, so the run reaches the edge. - assert_eq!(rows[1].open_runs(), &[(64, FB_WIDTH)]); + assert_eq!(rows[1].open_runs(), &[(62, FB_WIDTH)]); // Row 2: carried open, closes at hstop $100. - assert_eq!(rows[2].open_runs(), &[(0, 318)]); + assert_eq!(rows[2].open_runs(), &[(0, 316)]); } #[test] @@ -204,9 +204,9 @@ fn sprite_color_entry_follows_bplcon4_on_aga() { #[test] fn present_h_shift_centres_standard_window() { - // Stock PAL DIW ($2C81/$2CC1 -> H 0x81/0x1C1). 64px left border vs - // 12px right border; recentre by half the difference = 26px. - assert_eq!(present_h_shift(0x81, 0x1C1), 26); + // Stock PAL DIW ($2C81/$2CC1 -> H 0x81/0x1C1). 62px left border vs + // 14px right border; recentre by half the difference = 24px. + assert_eq!(present_h_shift(0x81, 0x1C1), 24); } #[test] @@ -279,9 +279,9 @@ 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 starts 24 pixels into - // its narrower DIW after the standard hi-res $81/$3C phase is aligned - // to the display-window edge. + // 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). let ks_boot = ControlState { diwstrt: 0x2C95, diwstop: 0x2CAD, @@ -289,7 +289,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), 24); + assert_eq!(ks_boot.native_x_offset(ks_boot.diw_h_start(), repeat), 22); } fn ocs_snapshot(diwstrt: u16, diwstop: u16, ddfstrt: u16, ddfstop: u16) -> RenderRegisterSnapshot { @@ -311,15 +311,15 @@ fn present_h_shift_for_centres_wide_diw_around_standard_fetch() { // (DIWSTRT $5702 -> H $02, DIWSTOP $FFFF -> H $1FF) around a standard // 320-px lo-res picture (DDF $38..$D0). The open window only reveals // COLOR0 border the TV crops, so the picture must still recentre by the - // stock 26px instead of sitting right-of-centre. + // stock 24px instead of sitting right-of-centre. assert_eq!( present_h_shift_for(&ocs_snapshot(0x5702, 0xFFFF, 0x0038, 0x00D0)), - 26 + 24 ); // A genuinely centred stock display is unchanged. assert_eq!( present_h_shift_for(&ocs_snapshot(0x2C81, 0x2CC1, 0x0038, 0x00D0)), - 26 + 24 ); } @@ -644,7 +644,7 @@ fn display_window_uses_stop_as_exclusive_right_edge() { ..blank_state() }; - assert_eq!(state.display_window_x(), (104, 664)); + assert_eq!(state.display_window_x(), (102, 662)); } #[test] @@ -796,14 +796,18 @@ fn denise_horizontal_delay_aligns_copper_beam_and_display_fetch_domains() { ..blank_state() }; let display_left = state.display_window_x().0; - let copper_hpos = COPPER_WAIT_HPOS_FB0 + (display_left / 4) as i32; + // The standard window edge sits at 62 (2H-196, hardware-verified) with + // the first fetched lo-res sample flush at the edge; the copper/register + // domain position of the same beam time maps one lo-res pixel later + // (the Agnus-fetch -> Denise-display pipeline). + let copper_hpos = COPPER_WAIT_HPOS_FB0 + ((display_left + 2) / 4) as i32; - assert_eq!(display_left, 64); + assert_eq!(display_left, 62); assert_eq!( beam_to_framebuffer_pos(PAL_VISIBLE_LINE0 as u32, copper_hpos as u32), - (0, display_left) + (0, display_left + 2) ); - assert_eq!(state.native_x_offset(false, 2), 1); + assert_eq!(state.native_x_offset(false, 2), 0); assert_eq!(state.fetch_start_native_x(false, 2), 0); } @@ -1029,7 +1033,7 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { ddfstrt: 0x0040, ..blank_state() }; - assert_eq!(kickstart_hires.native_x_offset(true, 1), 24); + assert_eq!(kickstart_hires.native_x_offset(true, 1), 22); // Wide FMODE fetches quantize the displayed shifter origin to the gulp // grid: AGA system screens program DDFSTRT $38 or $3C interchangeably @@ -1062,7 +1066,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); - assert_eq!(wb_hires_overscan_fetch.fetch_start_native_x(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 placement gulp grid runs on absolute colour-clock multiples of // the fetch period. Lores FMODE=1 has a 16-cck gulp: DDFSTRT $30 is @@ -1099,10 +1106,12 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { ddfstrt: 0x003C, ..blank_state() }; - assert_eq!(diagrom_hires.display_window_x().0, 64); + 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); - assert_eq!(diagrom_hires.fetch_start_native_x(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); let lores_extra_fetch_word = RenderState { bplcon0: 0, @@ -1134,12 +1143,13 @@ fn native_x_offset_accounts_for_diw_and_ddf_alignment() { 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). + // pixels) earlier than the standard $38. With the hardware window edge + // (2H-196) the standard $81 window is flush with the $38 bitmap, so + // the $30 offset is exactly the 16 extra samples. 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), - 17 + 16 ); } @@ -1176,8 +1186,10 @@ fn ddfstrt_positions_first_lowres_bitplane_word_relative_to_diwstrt() { ddfstrt: 0x0038, ..blank_state() }; + // The standard $81/$38 lo-res picture is flush with the hardware + // window edge (both at framebuffer x = 62): no hidden sample, no inset. assert_eq!(standard_lowres.fetch_start_native_x(false, 2), 0); - assert_eq!(standard_lowres.native_x_offset(false, 2), 1); + assert_eq!(standard_lowres.native_x_offset(false, 2), 0); let late_window_aligned_fetch = RenderState { bplcon0: 0, @@ -1186,7 +1198,7 @@ fn ddfstrt_positions_first_lowres_bitplane_word_relative_to_diwstrt() { ..blank_state() }; assert_eq!(late_window_aligned_fetch.fetch_start_native_x(false, 2), 0); - assert_eq!(late_window_aligned_fetch.native_x_offset(false, 2), 1); + assert_eq!(late_window_aligned_fetch.native_x_offset(false, 2), 0); let inset_fetch = RenderState { bplcon0: 0, @@ -1194,7 +1206,7 @@ fn ddfstrt_positions_first_lowres_bitplane_word_relative_to_diwstrt() { ddfstrt: 0x0050, ..blank_state() }; - assert_eq!(inset_fetch.fetch_start_native_x(false, 2), 14); + assert_eq!(inset_fetch.fetch_start_native_x(false, 2), 15); assert_eq!(inset_fetch.native_x_offset(false, 2), 0); let late_fetch_standard_window = RenderState { @@ -1205,7 +1217,7 @@ fn ddfstrt_positions_first_lowres_bitplane_word_relative_to_diwstrt() { }; assert_eq!( late_fetch_standard_window.fetch_start_native_x(false, 2), - 31 + 32 ); assert_eq!(late_fetch_standard_window.native_x_offset(false, 2), 0); } @@ -1227,9 +1239,9 @@ fn late_lowres_ddf_reaches_diwstop_with_undelayed_planes_active() { ddfstop: 0x00D0, ..ControlState::default() }; - assert_eq!(control.display_window_x(), (96, 704)); + assert_eq!(control.display_window_x(), (94, 702)); assert_eq!(control.words_per_row(0), 17); - assert_eq!(control.fetch_start_native_x(control.diw_h_start(), 2), 31); + assert_eq!(control.fetch_start_native_x(control.diw_h_start(), 2), 32); assert_eq!(control.native_x_offset(control.diw_h_start(), 2), 0); assert!(control.holds_final_lowres_fetch_sample_at_diwstop()); @@ -1237,13 +1249,16 @@ fn late_lowres_ddf_reaches_diwstop_with_undelayed_planes_active() { let output_native_x = (last_visible_x - control.display_window_x().0) / control.framebuffer_pixel_repeat(); let native_x = output_native_x - control.fetch_start_native_x(control.diw_h_start(), 2); - assert_eq!(native_x, 272); + // With the hardware window edge (2H-196) the final visible sample is + // the last fetched one (271), not one past the row; the DDFSTOP hold + // keeps it available either way. + assert_eq!(native_x, 271); let plane1 = vec![0; 17]; let mut plane2 = vec![0; 17]; plane2[16] = 0x0001; let planes = vec![plane1, plane2]; - let plan = DenisePlannedPlayfieldLine::new(0, 96, 704, &planes, 17 * 16); + let plan = DenisePlannedPlayfieldLine::new(0, 94, 702, &planes, 17 * 16); let delays = std::array::from_fn(|plane| control.scroll_for_plane(plane)); let sample = plan.sample_prepared_with_final_fetch_hold( control.nplanes(), @@ -1272,14 +1287,14 @@ fn late_lowres_ddf_stop_hold_keeps_left_origin_unadvanced() { ddfstop: 0x00D0, ..ControlState::default() }; - assert_eq!(control.display_window_x(), (96, 704)); - assert_eq!(control.fetch_start_native_x(control.diw_h_start(), 2), 31); + assert_eq!(control.display_window_x(), (94, 702)); + assert_eq!(control.fetch_start_native_x(control.diw_h_start(), 2), 32); assert!(control.holds_final_lowres_fetch_sample_at_diwstop()); let mut plane = vec![0; 17]; plane[0] = 0x8000; let planes = vec![plane]; - let plan = DenisePlannedPlayfieldLine::new(0, 96, 704, &planes, 17 * 16); + let plan = DenisePlannedPlayfieldLine::new(0, 94, 702, &planes, 17 * 16); let mut palette = Palette::new(); palette.write_ocs(1, 0x00F0); let mut fb = vec![0; FB_PIXELS]; @@ -5028,10 +5043,13 @@ fn planned_ham_dma_advances_hold_through_edge_fetch_phase() { row_words[0][0] |= 0x8000; // native x 0: direct palette entry 1 row_words[1][0] |= 0x4000; // native x 1: HAM blue := 2 row_words[4][0] |= 0x4000; + // DIWSTRT one lores px right of standard ($82): the window opens one + // sample into the fetched stream, so sample 0 is the hidden edge sample + // whose HAM hold advance this test pins. let line_plan = DenisePlannedPlayfieldLine::new(0, 64, 66, &row_words, 2); let mut control = visible_lowres_control(0x6800); - control.diwstrt = ((PAL_VISIBLE_LINE0 as u16) << 8) | STANDARD_DIW_HSTART as u16; - control.diwstop = (((PAL_VISIBLE_LINE0 + 1) as u16) << 8) | (STANDARD_DIW_HSTART as u16 + 1); + control.diwstrt = ((PAL_VISIBLE_LINE0 as u16) << 8) | (STANDARD_DIW_HSTART as u16 + 1); + control.diwstop = (((PAL_VISIBLE_LINE0 + 1) as u16) << 8) | (STANDARD_DIW_HSTART as u16 + 2); let mut palette = Palette::new(); palette.write_ocs(1, 0x0123); let mut fb = vec![0; FB_PIXELS]; @@ -5070,9 +5088,9 @@ 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][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); + row_words[4][0] |= 0x0001; // native x 15: HAM blue := 0 (hidden, pre-DIW) + row_words[4][1] |= 0x8000; // native x 16: HAM blue := 0 (first visible) + let line_plan = DenisePlannedPlayfieldLine::new(0, 62, 64, &row_words, 32); let mut control = visible_lowres_control(0x6800); control.diwstrt = ((PAL_VISIBLE_LINE0 as u16) << 8) | STANDARD_DIW_HSTART as u16; control.diwstop = (((PAL_VISIBLE_LINE0 + 1) as u16) << 8) | (STANDARD_DIW_HSTART as u16 + 1); @@ -5106,20 +5124,20 @@ fn planned_ham_dma_ignores_extra_early_ddf_history_before_diw() { 0, ); - // 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]); + // 16 = one 8-cck fetch period before the standard $38 origin: DDFSTRT + // placement is linear and the hardware window edge (2H-196) is flush + // with the standard-DDF picture (vAmigaTS Agnus/DIW/OLDDIW/diw1 photos). + assert_eq!(control.native_x_offset(control.diw_h_start(), 2), 16); + assert_eq!(fb[62], rgb12_to_rgba8(0x0000)); + assert_eq!(fb[63], rgb12_to_rgba8(0x0000)); + assert_eq!(&playfield_mask[62..64], &[0x02, 0x02]); } #[test] fn bplcon1_write_at_diw_right_edge_does_not_retap_current_ham_line() { let mut row_words = vec![vec![0; 1]; 6]; row_words[0][0] |= 0x4000; // native x 1: direct palette entry 1 - let line_plan = DenisePlannedPlayfieldLine::new(0, 64, 96, &row_words, 16); + let line_plan = DenisePlannedPlayfieldLine::new(0, 62, 94, &row_words, 16); let mut control = visible_lowres_control(0x6800); control.diwstrt = ((PAL_VISIBLE_LINE0 as u16) << 8) | STANDARD_DIW_HSTART as u16; control.diwstop = (((PAL_VISIBLE_LINE0 + 1) as u16) << 8) | (STANDARD_DIW_HSTART as u16 + 16); @@ -5158,7 +5176,7 @@ fn bplcon1_write_at_diw_right_edge_does_not_retap_current_ham_line() { 0, ); - assert_eq!(control.display_window_x(), (64, 96)); + assert_eq!(control.display_window_x(), (62, 94)); assert_eq!(fb[64], rgb12_to_rgba8(0x0123)); assert_eq!(fb[65], rgb12_to_rgba8(0x0123)); } diff --git a/src/video/window/tests.rs b/src/video/window/tests.rs index db618ee..6085c08 100644 --- a/src/video/window/tests.rs +++ b/src/video/window/tests.rs @@ -1513,14 +1513,16 @@ fn tv_overscan_mask_tracks_the_centering_shift() { let marker = rgba(0x12, 0x34, 0x56); let mut fb = vec![marker; FB_PIXELS]; - // A standard display shifted left 16px for centring: the bezel - // moves with it, so the window's left edge is not clipped. + // A standard display shifted left 8px for centring: the bezel + // moves with it, so the window's left edge is not clipped. (The shift + // must stay below the source-left bound, 14px with the hardware + // window edge at 62, for the unmasked strip to remain in-frame.) let std_top = standard_window_top_row(STANDARD_PAL_VISIBLE_START_VPOS); - mask_present_frame_to_tv(&mut fb, 16, std_top); + mask_present_frame_to_tv(&mut fb, 8, std_top); let (source_left, source_right) = tv_source_h_bounds(); - let left = source_left - 16; - let right = source_right - 16; + let left = source_left - 8; + let right = source_right - 8; let mid_row = std_top + 100; assert_eq!(fb[mid_row * FB_WIDTH + left - 1], rgba(0, 0, 0)); assert_eq!(fb[mid_row * FB_WIDTH + left], marker);