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
23 changes: 14 additions & 9 deletions docs/internals/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
80 changes: 58 additions & 22 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<u32>| {
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);
Expand Down
27 changes: 16 additions & 11 deletions src/video/bitplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
Loading
Loading