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
12 changes: 7 additions & 5 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
48 changes: 30 additions & 18 deletions src/video/bitplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
88 changes: 74 additions & 14 deletions src/video/bitplane/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,19 @@ 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,
ddfstrt: 0x0040,
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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading