Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/internals/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 34 additions & 4 deletions src/video/bitplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
44 changes: 44 additions & 0 deletions src/video/bitplane/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 24 additions & 3 deletions tests/probe_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))
}
Expand Down Expand Up @@ -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 = \"<bundled-aros>\"\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\
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 8 additions & 1 deletion timing-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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),
Expand Down
115 changes: 115 additions & 0 deletions timing-test/ddfprobe-agafold.asm
Original file line number Diff line number Diff line change
@@ -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
Binary file added timing-test/ddfprobe-agafold.bin
Binary file not shown.
Binary file added timing-test/golden/ddfprobe-agafold.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading