diff --git a/docs/internals/video.md b/docs/internals/video.md index 2a7bbd5..3a53e21 100644 --- a/docs/internals/video.md +++ b/docs/internals/video.md @@ -73,7 +73,26 @@ super-hi-res playfield four, and the comparison narrows with the word cadence, so hi-res ignores nibble bit 3 and super-hi-res bits 2-3 (pinned by the `ddfprobe-hscroll` golden probe on the Kickstart 2.05 boot-screen constellation, vAmiga-verified). AGA's extended BPLCON1 fields feed the -same per-plane delays through `aga_bplcon1_scroll_samples`. +same per-plane delays through `aga_bplcon1_scroll_samples`, masked to one +fetch-unit width (32-bit fetches scroll within 32 lo-res px, 64-bit within +64). +An off-grid DDFSTRT interacts with the scroll in both fetch regimes. An +FMODE=0 fetch placed off the shifter reload grid rounds UP (the data is +late for its own slot), and a scroll that covers the lateness catches the +floor slot one gulp earlier (vAmiga-verified, `ddfprobe-phase`). A wide +FMODE fetch has the opposite sense: Agnus masks DDFSTRT DOWN to the +fetch-unit grid, the data arrives early, and Denise's reload comparator +window is anchored at that early fetch start, so scroll taps folding into +the last `earliness` px of the gulp window already see the next gulp's +data and the playfield sits one full gulp left -- the display delay is +`((tap + earliness) mod gulp) - earliness`. On-grid starts never fold. +Pinned by the `ddfprobe-agafold` golden probe on the Alien Breed II AGA +playfield constellation (lo-res BPL32, DDFSTRT $24 -> earliness 8 px), +whose scroller pairs the folded taps with a one-gulp pointer step and +jumps 32 px for 4 of every 16 pan frames without the fold. FS-UAE +(WinUAE core) renders the probe's 16-band map identically, band by band +(vAmiga is OCS/ECS-only and cannot arbitrate AGA); the hi-res/SHRES +scaling of the fold is not yet externally verified. BPLCON1-delayed samples at the left edge of a scanline do not reuse the previous line's final bitplane word. Before the current line's shifter has a sample for a delayed tap, replay marks playfield output active but returns diff --git a/src/video/bitplane.rs b/src/video/bitplane.rs index e4b5761..40e0079 100644 --- a/src/video/bitplane.rs +++ b/src/video/bitplane.rs @@ -1056,7 +1056,12 @@ impl ControlState { } /// One-gulp reload advance (in native px) for a playfield whose BPLCON1 - /// scroll covers the fetch's off-grid phase. + /// scroll interacts with the fetch's off-grid phase. Two cases, by + /// FMODE: an FMODE=0 fetch placement rounds UP (data late; scroll + /// covering the lateness catches the floor slot), a wide-FMODE + /// placement rounds DOWN via the Agnus DDFSTRT unit mask (data early; + /// scroll taps folding into the last `earliness` px of the gulp window + /// see the next gulp). Both cases sit one gulp left of the base origin. /// /// The row's placement (`fetch_origin_native_shift`) quantizes an /// FMODE=0 fetch that starts off the shifter reload grid UP to the next @@ -1074,9 +1079,6 @@ impl ControlState { /// times a second. Scroll 0 (every previously calibrated case) is /// unchanged: the advance only triggers when scroll covers the phase. fn reload_advance_for_scroll(&self, scroll: usize) -> i32 { - if self.fetch_quantum() != 1 { - return 0; - } let gulp = self.fetch_period() as i32; let start = effective_ddf_start_hpos( self.agnus_revision, @@ -1094,6 +1096,34 @@ impl ControlState { } else { 2 }; + if self.fetch_quantum() != 1 { + // Wide-FMODE counterpart of the FMODE=0 rule below, with the + // opposite sense: Agnus masks an off-grid DDFSTRT DOWN to the + // fetch-unit grid (see `align`), so the data arrives + // phase*native px EARLY relative to the programmed start + // instead of late. Denise's reload comparator window is + // anchored at that early fetch start, so scroll taps in the + // last `earliness` px of the gulp window already see the NEXT + // gulp's data: the playfield sits one full gulp left, i.e. the + // display delay folds as ((scroll + earliness) mod gulp) - + // earliness. On-grid starts (every calibrated KS/CD32/Pinball + // case) keep phase 0 and are unchanged. Calibrated against + // Alien Breed II AGA's playfield (lo-res, FMODE BPL32, + // DDFSTRT $24 -> phase 4 cck, earliness 8 px): its scroller + // pairs AGA BPLCON1 values $CC99..$CCFF (folded taps 25..31) + // with a one-gulp pointer step, which jumps 32 px for 4 of + // every 16 frames without the fold. FS-UAE (WinUAE core) + // renders the ddfprobe-agafold band map identically, band by + // band, corroborating the rule and the fold boundary. TODO: + // verify the hi-res/SHRES scaling on real hardware or FS-UAE; + // only the lo-res BPL32 case is pinned. + let gulp_native = gulp * native_per_cck; + let earliness = phase * native_per_cck; + if scroll as i32 >= gulp_native - earliness { + return gulp_native; + } + return 0; + } let lateness = phase * native_per_cck; if lateness <= scroll as i32 { gulp * native_per_cck diff --git a/src/video/bitplane/tests.rs b/src/video/bitplane/tests.rs index aaf2ed2..07443a6 100644 --- a/src/video/bitplane/tests.rs +++ b/src/video/bitplane/tests.rs @@ -5006,6 +5006,50 @@ fn covered_scroll_catches_floor_reload_slot_for_off_grid_ddfstrt() { assert_eq!(on_grid.sample_delay_for_plane(0), 15); } +#[test] +fn wide_fmode_scroll_folds_into_the_early_masked_fetch_start() { + // Lo-res 32-bit fetch (FMODE BPL32) with DDFSTRT $24: Agnus masks the + // start down to the 16-cck fetch-unit grid ($20), so the data arrives + // 4 cck (8 lo-res px) EARLY relative to the programmed start - the + // opposite sense of the FMODE=0 round-up above. Denise's reload + // comparator window is anchored at that early fetch start, so scroll + // taps in the last 8 px of the 32-px gulp window already see the NEXT + // gulp's data and the playfield sits one full gulp left: the display + // delay folds as ((scroll + earliness) mod gulp) - earliness. + // Calibrated against Alien Breed II AGA's playfield scroller, which + // pairs the folded BPLCON1 taps ($CC99..$CCFF, taps 25..31) with a + // one-gulp bitplane-pointer step; without the fold the pan jumps 32 px + // for 4 of every 16 frames (issue #248). + let control = |bplcon1: u16| ControlState { + agnus_revision: AgnusRevision::AgaAlice, + bplcon0: 0x6200, // 6 planes, lo-res + ddfstrt: 0x0024, + ddfstop: 0x00D4, + fmode: 0x0005, // BPL32 | SPR32 + bplcon1, + ..ControlState::default() + }; + + // $CC77: raw 55 -> masked tap 23, outside the fold window: no advance. + assert_eq!(control(0xCC77).row_reload_advance(), 0); + assert_eq!(control(0xCC77).sample_delay_for_plane(0), 23); + // $CC99: raw 57 -> masked tap 25 >= 32 - 8: one gulp (32 px) left. + assert_eq!(control(0xCC99).row_reload_advance(), 32); + assert_eq!(control(0xCC99).sample_delay_for_plane(0), 25); + assert_eq!(control(0xCC99).sample_delay_for_plane(1), 25); + // $CCFF: raw 63 -> tap 31 folds; past the wrap, raw 1 does not. + assert_eq!(control(0xCCFF).row_reload_advance(), 32); + assert_eq!(control(0x0011).row_reload_advance(), 0); + // On the unit grid the comparator window starts with the programmed + // fetch: no fold at any scroll. + let on_grid = ControlState { + ddfstrt: 0x0020, + ..control(0xCC99) + }; + assert_eq!(on_grid.row_reload_advance(), 0); + assert_eq!(on_grid.sample_delay_for_plane(0), 25); +} + #[test] fn aga_bplcon1_decodes_expanded_scroll_fields() { let control = ControlState { diff --git a/tests/probe_golden.rs b/tests/probe_golden.rs index 2a9d2ba..33e83f9 100644 --- a/tests/probe_golden.rs +++ b/tests/probe_golden.rs @@ -52,6 +52,9 @@ const BOOTBLOCK_SIZE: usize = 1024; enum Machine { Ocs, Ecs, + /// A1200 shape (68EC020, AGA, 2M chip) for probes of AGA-only + /// behaviour (FMODE wide fetches, extended BPLCON1 scroll). + Aga, } /// One golden-render probe: name, main-program binary, emulated seconds @@ -86,6 +89,15 @@ const fn probe_ecs(name: &'static str, program: &'static str, seconds: f64) -> P } } +const fn probe_aga(name: &'static str, program: &'static str, seconds: f64) -> Probe { + Probe { + name, + program, + seconds, + machine: Machine::Aga, + } +} + fn repo_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } @@ -130,16 +142,20 @@ fn probe_config(adf: &Path, machine: Machine) -> String { let chipset = match machine { Machine::Ocs => "revision = \"OCS\"\n", Machine::Ecs => "revision = \"ECS\"\nagnus = \"8372A\"\ndenise = \"OCS\"\n", + Machine::Aga => "revision = \"AGA\"\n", + }; + let (cpu, memory) = match machine { + Machine::Ocs | Machine::Ecs => ("68000", "chip = \"512K\"\nslow = \"512K\"\n"), + Machine::Aga => ("68EC020", "chip = \"2M\"\n"), }; format!( "rom = \"\"\n\ [display]\n\ overscan = \"full\"\n\ [cpu]\n\ - model = \"68000\"\n\ + model = \"{cpu}\"\n\ [memory]\n\ - chip = \"512K\"\n\ - slow = \"512K\"\n\ + {memory}\ [chipset]\n\ {chipset}\ video = \"PAL\"\n\ @@ -356,6 +372,11 @@ probe_tests! { // at the DIW stop (the KS 2.05 first-text-column regression class); // vAmiga-verified band by band. golden_ddfprobe_hscroll => probe_ecs("ddfprobe-hscroll", "ddfprobe-hscroll.bin", 16.0); + // AGA wide-FMODE off-grid DDFSTRT scroll fold: taps in the last + // `earliness` px of the gulp window show the next gulp, one gulp left + // (the Alien Breed II AGA horizontal-scroll regression class, issue + // #248); FS-UAE-verified band by band. + golden_ddfprobe_agafold => probe_aga("ddfprobe-agafold", "ddfprobe-agafold.bin", 16.0); // CPU pacing bars under BLTPRI copy/fill/line blits (the Rampage // "present" flicker / BLS fence regression class). golden_bltprobe_pace => probe("bltprobe-pace", "bltprobe-pace.bin", 16.0); diff --git a/timing-test/README.md b/timing-test/README.md index e1fa3a5..d573687 100644 --- a/timing-test/README.md +++ b/timing-test/README.md @@ -286,7 +286,7 @@ refreshed (or a count sitting on an 8-iteration display-bucket edge flips a whole bar word). Re-bless and review the diff after a ROM refresh. Each probe is its own `#[test]`, so the harness runs the emulator boots in -parallel on the available cores (the full suite of 20 takes ~20 s on an +parallel on the available cores (the full suite of 21 takes ~20 s on an 8-core host vs ~90 s sequentially). Covered: `timing-test` (all 32 timing rows as rendered hex), `ddfprobe` @@ -305,6 +305,13 @@ DIW narrower than the fetch: one lo-res pixel = 2 hi-res px per scroll step, nibble bit 3 ignored, and the row-end overlap words exactly clipped at the DIW stop; the KS 2.05 first-text-column wrap regression class, vAmiga-verified band by band), +`ddfprobe-agafold` (the AGA wide-FMODE scroll fold: an off-grid DDFSTRT +is masked down to the fetch-unit grid, so scroll taps in the last +`earliness` px of the gulp window show the next gulp one full gulp left, +swept against bitplane-pointer byte offsets on the Alien Breed II AGA +playfield constellation -- the issue #248 horizontal scroll-jump +regression class; boots the A1200/AGA machine shape, FS-UAE-verified +band by band since vAmiga cannot arbitrate AGA), `bltprobe-pace` (CPU pacing bars under BLTPRI copy/fill/line blits and a nice-mode line blit -- the BLS-fence and blitter slot-cadence regression class; the whole-blit fence collapsed the fill/line bars), diff --git a/timing-test/ddfprobe-agafold.asm b/timing-test/ddfprobe-agafold.asm new file mode 100644 index 0000000..9542e77 --- /dev/null +++ b/timing-test/ddfprobe-agafold.asm @@ -0,0 +1,115 @@ +; AGA wide-FMODE scroll-fold placement probe (lo-res, 32-bit fetch). +; Agnus masks an off-grid DDFSTRT DOWN to the fetch-unit grid, so the data +; arrives early relative to the programmed start; Denise's reload +; comparator window is anchored at that early fetch start, so BPLCON1 +; scroll taps folding into the last `earliness` px of the gulp window +; already see the NEXT gulp's data and the playfield sits one full gulp +; left: display delay = ((tap + earliness) mod gulp) - earliness. +; +; The constellation clones Alien Breed II AGA's playfield (the issue #248 +; horizontal scroll regression example): lo-res, FMODE=$0005 (BPL32|SPR32, +; 32-px gulps), DDFSTRT $24 (4 cck past the 16-cck unit grid -> masked to +; $20, earliness 8 px), DDFSTOP $D4, DIW $7B..$1C5 (into the overscan both +; sides). Its scroller pairs the folded taps (raw AGA scroll 57..63, tap +; 25..31) with a one-gulp bitplane-pointer step; without the fold the pan +; jumps 32 px for 4 of every 16 frames. +; +; 16 bands of 8 lines sweep (BPL1PT byte offset, extended BPLCON1) pairs; +; every line re-anchors the same marker bitmap, so the mid-row marker x per +; band IS the placement map: x = x0 + tap*2 - off*16 - 64*[tap >= 24], +; with tap = raw scroll & 31 (32-bit fetch masks the range to one gulp) and +; the fold boundary at gulp - earliness = 24. The pointer's byte offset +; must never change the fold (bands 8-15: same map, shifted by the offset). +; +; FS-UAE-verified (WinUAE core, A1200/KS3.1, 2026-07-22): all 16 band +; positions match Copperline's render exactly (relative map 0,+2,+4,+16, +; -44,-40,-32,-28,-108,-104,-96,-92,-64,-60,-172,-156 raw px from band 0; +; vAmiga is OCS/ECS-only and cannot arbitrate AGA). The fold boundary and +; the lo-res BPL32 scaling are pinned; hi-res/SHRES scaling is not. +CUST equ $dff000 +BMP equ $40000 +CLIST equ $60000 +NBAND equ 16 + lea CUST,a6 + move.w #$7fff,$9a(a6) + move.w #$7fff,$9c(a6) + move.w #$7fff,$96(a6) + lea BMP,a0 + move.w #$ffff,(a0)+ ; w0: row-start bar + move.w #$f000,(a0)+ ; w1: tick + move.w #8-1,d0 ; w2..w9 empty +.z: clr.w (a0)+ + dbra d0,.z + move.w #$ff0f,(a0)+ ; w10: mid marker (8px bar + 4px tick) + move.w #10-1,d0 ; w11..w20 empty +.z1: clr.w (a0)+ + dbra d0,.z1 + move.w #0,(a0)+ ; w21 empty + move.w #$ffff,(a0)+ ; w22: row-end bar + move.w #$ffff,(a0)+ ; w23 + move.w #8192-1,d0 +.z2: clr.w (a0)+ + dbra d0,.z2 + lea CLIST,a1 + move.l #$01001200,(a1)+ ; BPLCON0: 1 plane, lo-res + move.l #$01020000,(a1)+ ; BPLCON1 = 0 + move.l #$01040024,(a1)+ ; BPLCON2 (game uses $224; sprites moot) + move.l #$01060c00,(a1)+ ; BPLCON3 + move.l #$010c0011,(a1)+ ; BPLCON4 + move.l #$01080000,(a1)+ ; BPL1MOD = 0 + move.l #$010a0000,(a1)+ ; BPL2MOD = 0 + move.l #$01800008,(a1)+ ; COLOR00 dark blue + move.l #$01820fff,(a1)+ ; COLOR01 white + move.l #$00920024,(a1)+ ; DDFSTRT $24 (game value, off-grid) + move.l #$009400d4,(a1)+ ; DDFSTOP $D4 + move.l #$008e287b,(a1)+ ; DIWSTRT $7B (into left overscan) + move.l #$00902dc5,(a1)+ ; DIWSTOP $1C5 + move.l #$01fc0005,(a1)+ ; FMODE = BPL32|SPR32 + move.l #$00e00004,(a1)+ ; BPL1PTH + move.l #$00e20000,(a1)+ ; BPL1PTL + lea bands(pc),a2 + moveq #NBAND-1,d2 + move.w #$3c00,d3 ; first band line $3C; WAIT hp = $07 +.band: move.w (a2)+,d6 ; BPL1PTL for this band (BMP + offset) + move.w (a2)+,d7 ; BPLCON1 for this band + moveq #8-1,d5 +.line: move.w d3,d0 + or.w #$0007,d0 + move.w d0,(a1)+ ; WAIT (v,$07) + move.w #$fffe,(a1)+ + move.w #$0102,(a1)+ ; BPLCON1 + move.w d7,(a1)+ + move.w #$00e0,(a1)+ ; BPL1PTH + move.w #$0004,(a1)+ + move.w #$00e2,(a1)+ ; BPL1PTL + move.w d6,(a1)+ + add.w #$0100,d3 + dbra d5,.line + dbra d2,.band + move.l #$fffffffe,(a1)+ + move.l #CLIST,$80(a6) + move.w d0,$88(a6) ; COPJMP1 + move.w #$8380,$96(a6) ; DMAEN|BPLEN|COPEN +.l: bra.s .l + cnop 0,2 +; Per band: BPL1PTL offset, BPLCON1. +; AGA lores raw scroll L both playfields: low byte (L&15)|(L&15)<<4, +; H6-H7 = (L>>4) in bits 10-11 (pf1) and 14-15 (pf2). +; Expected mask-32 model: marker x = x0 + (L & 31) * 2 - off_px * 2. +bands: + dc.w $0000,$00FF ; band 0: off 0, raw 15 -> ref x0+30 + dc.w $0000,$4400 ; band 1: off 0, raw 16 -> x0+32 + dc.w $0000,$CC11 ; band 2: off 0, raw 49 -> x0+34 + dc.w $0000,$CC77 ; band 3: off 0, raw 55 -> x0+46 + dc.w $0000,$CC99 ; band 4: off 0, raw 57 -> x0+50 (game: -32?) + dc.w $0000,$CCBB ; band 5: off 0, raw 59 -> x0+54 + dc.w $0000,$CCFF ; band 6: off 0, raw 63 -> x0+62 + dc.w $0000,$0011 ; band 7: off 0, raw 1 -> x0+2 + dc.w $0004,$CC99 ; band 8: off 4, raw 57 (ptr mod8=4) + dc.w $0004,$CCBB ; band 9: off 4, raw 59 + dc.w $0004,$CCFF ; band 10: off 4, raw 63 + dc.w $0004,$0011 ; band 11: off 4, raw 1 + dc.w $0004,$00FF ; band 12: off 4, raw 15 + dc.w $0004,$CC11 ; band 13: off 4, raw 49 + dc.w $0008,$CC99 ; band 14: off 8, raw 57 (ptr mod8=0) + dc.w $0008,$0011 ; band 15: off 8, raw 1 diff --git a/timing-test/ddfprobe-agafold.bin b/timing-test/ddfprobe-agafold.bin new file mode 100644 index 0000000..1a485d7 Binary files /dev/null and b/timing-test/ddfprobe-agafold.bin differ diff --git a/timing-test/golden/ddfprobe-agafold.png b/timing-test/golden/ddfprobe-agafold.png new file mode 100644 index 0000000..dbfd65d Binary files /dev/null and b/timing-test/golden/ddfprobe-agafold.png differ