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
42 changes: 26 additions & 16 deletions docs/internals/timing.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@ save state).
### CPU vs blitter

DMACON's BLTPRI ("blitter nasty") is modelled as on hardware: with BLTPRI
set, BBUSY holds the BLS line asserted for the whole blit, so the CPU is
denied every chip-bus cycle until BLTDONE -- including the pipeline's
bus-free micro-cycles (the Copper and fixed DMA still use them). Without
it, the blitter yields a slot after the CPU has missed three consecutive
slots, matching the Minimig RTL. The counter and the back-pressure rule
are detailed under [](#cpu-contention) below.
set the blitter wins every slot it requests, and the request line stays
asserted through the blit's warm-up -- the startup ladder and, for
D-writing blits, the first word's cycles including the empty first-D
bubble -- so the CPU is fenced out of those holes too (the Copper and
fixed DMA still use them). Once the pipeline is primed, genuinely bus-free
micro-cycles (line-mode Bresenham cycles, fill's idle cycle,
disabled-channel gaps) release the request and stay CPU-available even
under BLTPRI, matching FS-UAE/vAmiga per-slot arbitration. Without
BLTPRI, the blitter yields a slot after the CPU has missed three
consecutive slots, matching the Minimig RTL. The counter and the
back-pressure rule are detailed under [](#cpu-contention) below.

### Sprite DMA control rewrites

Expand Down Expand Up @@ -445,16 +450,21 @@ blitter yield earlier. After `BLITTER_SLOWDOWN_CPU_MISS_LIMIT` (3)
consecutive pressure misses the blitter yields one slot, matching the HRM
"one bus cycle in four" rule while also matching UAE's `blitter_nasty`
distinction between normal idle cycles and fill/line BUSIDLE. Idle blit
pipeline cycles never claim the bus and stay CPU-available while BLTPRI is
clear; with BLTPRI set the asserted BLS line fences the CPU out of them
too, for the entire blit. Software depends on that fence: MFM-decode
trackloaders (e.g. Jim Power's) save the word below a decode blit's
destination, write BLTSIZE, and restore it two instructions later, relying
on the restore write being held past the blit's first D write -- granting
the CPU the first-D bubble let the blitter overwrite the restored word and
corrupted the last data word of every decoded sector. The counter resets
when the CPU gets the bus, when BLTPRI is set, or when blitter DMA cannot
run.
pipeline cycles never claim the bus and stay CPU-available -- with one
exception: with BLTPRI set, the blit's warm-up holes (the startup ladder
and, for D-writing blits, the first word's cycles including the empty
first-D bubble) fence the CPU, because the sequencer's bus request is held
asserted by its queued back-to-back first fetches. Software depends on the
warm-up fence: MFM-decode trackloaders (e.g. Jim Power's) save the word
below a decode blit's destination, write BLTSIZE, and restore it two
instructions later; granting the CPU the startup and first-D holes let the
restore (and its prefetches) land mid-blit and the blitter overwrite the
restored word, corrupting the last data word of every decoded sector. Once
the pipeline is primed, bus-free micro-cycles are CPU-available even under
BLTPRI -- line-heavy demo main loops (Rampage's vector parts) depend on
that CPU time, and a whole-blit fence overshoots the FS-UAE/vAmiga
references on timing-test row 26 by ~+70 cck. The counter resets when the
CPU gets the bus, when BLTPRI is set, or when blitter DMA cannot run.

A 68000 `TAS` read-modify-write is unsafe on chip RAM (the HRM warns
against it); the `m68k` backend exposes it as a byte read then a byte
Expand Down
2 changes: 2 additions & 0 deletions src/bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ const DMACON_SPREN: u16 = 1 << 5;
const DMACON_BLTEN: u16 = 1 << 6;
const DMACON_BPLEN: u16 = 1 << 8;
const DMACON_BLTPRI: u16 = 1 << 10;
#[cfg(test)]
const BLTCON0_USE_A: u16 = 1 << 11;
const BLTCON0_USE_C: u16 = 1 << 9;
const BLTCON0_USE_D: u16 = 1 << 8;
const BLTCON1_LINE: u16 = 1 << 0;
Expand Down
26 changes: 18 additions & 8 deletions src/bus/dma_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,16 +425,26 @@ impl Bus {
return ChipBusOwner::Copper;
}
if self.blitter.busy && self.blitter_dma_enabled() {
// With BLTPRI set, BBUSY holds the BLS line asserted for the whole
// blit, so the CPU is denied every chip-bus cycle until BLTDONE --
// including the pipeline's bus-free micro-cycles below. Regression
// With BLTPRI set, BLS fences the CPU (not the Copper or fixed
// DMA) during the blit's warm-up: the startup ladder and, for
// D-writing blits, the first word's cycles including the empty
// first-D bubble, while the sequencer's bus request is held
// asserted by the queued back-to-back first fetches. Regression
// example: the Jim Power trackloader saves the word below a
// descending MFM-decode blit's destination, writes BLTSIZE, and
// restores the word two instructions later, relying on the nasty
// lockout to hold that CPU write until after the blit's final
// D write; letting it into the first-D bubble corrupts the last
// word of every decoded sector.
if for_cpu && self.agnus.dmacon & DMACON_BLTPRI != 0 {
// restores the word two instructions later; the fence keeps the
// restore's prefetches out of the startup holes so the CPU write
// lands only after the blit completes. Once the pipeline is
// primed the request line drops on genuine bus-free micro-cycles
// (line-mode Bresenham, fill idle, disabled-channel gaps) and
// the CPU uses them even under BLTPRI -- line-heavy demo loops
// (Rampage's vector parts) rely on that CPU time, and FS-UAE and
// vAmiga agree (timing-test row 26: 25095/25097; a whole-blit
// fence overshoots to 25161).
if for_cpu
&& self.agnus.dmacon & DMACON_BLTPRI != 0
&& self.blitter.bltpri_warmup_fences_cpu()
{
return ChipBusOwner::Blitter;
}
// Idle blit pipeline cycles (the "-" slots in the HRM cycle diagrams,
Expand Down
96 changes: 78 additions & 18 deletions src/bus/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use super::{
BeamRegisterWrite, BeamWriteSource, Bus, CapturedBitplaneRow, CapturedSpriteLine, ChipBusOwner,
CpuBusAccessKind, DeviceClock, DisplaySpriteDmaState, DisplaySpriteLineData, FrameBusTrace,
LiveCollisionControl, LiveCollisionLineReplay, LiveSpriteCollisionSource,
RenderRegisterSnapshot, BLITTER_SLOWDOWN_CPU_MISS_LIMIT, BLTCON0_USE_C, BLTCON0_USE_D,
BLTCON1_DOFF, BLTCON1_LINE, BPLCON0_ECSENA, BPLCON3_BRDRBLNK, BPLCON3_BRDSPRT,
RenderRegisterSnapshot, BLITTER_SLOWDOWN_CPU_MISS_LIMIT, BLTCON0_USE_A, BLTCON0_USE_C,
BLTCON0_USE_D, BLTCON1_DOFF, BLTCON1_LINE, BPLCON0_ECSENA, BPLCON3_BRDRBLNK, BPLCON3_BRDSPRT,
BPLCON3_SPRES_HIRES, DENISE_HPOS_LAG_CCK, DMACON_BLTEN, DMACON_BLTPRI, DMACON_BPLEN,
DMACON_SPREN, PAL_SPRITE_DMA_FIRST_ACTIVE_VPOS, RENDER_COPPER_WAIT_HPOS_FB0,
RENDER_DIW_HSTART_FB0, RENDER_MIN_OVERSCAN_START_VPOS, RENDER_VISIBLE_LINES,
Expand Down Expand Up @@ -8900,17 +8900,19 @@ fn bltpri_stalls_cpu_chip_access_through_blitter_access_cycles() {

bus.grant_cpu_bus_access(2, CpuBusAccessKind::Read);

// With BLTPRI set BBUSY holds the BLS line asserted for the entire
// blit, so the CPU is denied even the idle D-bubble cycle after the A
// fetch (the bubble is bus-free, but BLS fences the CPU, not the bus).
// The CPU waits through the A fetch, the D bubble, the D write, and
// the terminal cycles, then spends its granted slot plus the bus-free
// tail: 6 color clocks. Software depends on this fence: MFM-decode
// trackloaders (e.g. Jim Power's) restore the word below a decode
// blit's destination right after BLTSIZE, relying on that CPU write
// landing only after the blit's first D write.
// With BLTPRI set, BLS fences the CPU through the D pipeline's warm-up:
// the idle first-D bubble after the A fetch is bus-free, but the
// sequencer's bus request is still asserted (its first fetches are
// queued back-to-back), so the CPU is denied it. The first cycle the
// CPU wins is the internal E flush, where BBUSY has already dropped;
// its granted slot and bus-free tail ride E/F while the blitter's
// terminal F cycle writes the queued word: 4 color clocks in all.
// Software depends on the closed bubble: MFM-decode trackloaders
// (e.g. Jim Power's) restore the word below a decode blit's destination
// right after BLTSIZE, relying on the startup and first-D holes staying
// shut so the restore's prefetches queue behind the blit instead.
let (cck, _) = bus.take_slice_bus_advance();
assert_eq!(cck, 6);
assert_eq!(cck, 4);
assert!(!bus.blitter.busy);
assert_eq!(&bus.mem.chip_ram[0x20..0x22], &[0x12, 0x34]);
assert_ne!(bus.paula.intreq & INT_BLIT, 0);
Expand Down Expand Up @@ -8950,17 +8952,75 @@ fn blithog_set_blocks_cpu_slowdown_back_pressure_until_blitter_finishes() {

bus.grant_cpu_bus_access(2, CpuBusAccessKind::Read);

// With BLTPRI set the CPU gets no starvation yield and BLS stays
// asserted until BBUSY drops: it waits through all twelve A/B/C
// accesses of the four words and the terminal E/F cycles, then spends
// its granted slot plus the bus-free tail cck, costing 16 color
// clocks.
// With BLTPRI set the CPU gets no starvation yield: it waits through
// all twelve A/B/C accesses of the four words (a source-only blit has
// no bus-free micro-cycles to release the request line), then spends
// its granted slot plus the bus-free tail cck, costing 14 color
// clocks. The terminal (internal) E/F cycles ride the CPU's
// granted/tail clocks -- BBUSY has already dropped -- so the engine
// finishes within the same span and the interrupt is asserted off the
// terminal cycle.
let (cck, _) = bus.take_slice_bus_advance();
assert_eq!(cck, 16);
assert_eq!(cck, 14);
assert!(!bus.blitter.busy);
assert_ne!(bus.paula.intreq & INT_BLIT, 0);
}

#[test]
fn bltpri_line_blit_bresenham_cycles_stay_cpu_available_after_warmup() {
let mut bus = empty_bus();
bus.agnus.dmacon = DMACON_DMAEN | DMACON_BLTEN | DMACON_BLTPRI;
bus.agnus.hpos = 0x20;
// Canonical 4-pixel line draw: USEA|USEC|USED, octant 0. The per-pixel
// cadence is [L1 -, L2 C-fetch, L3 -, L4 D-write]: two bus accesses and
// two bus-free Bresenham cycles.
bus.blitter.bltcon0 = BLTCON0_USE_A | BLTCON0_USE_C | BLTCON0_USE_D | 0x00CA;
bus.blitter.bltcon1 = BLTCON1_LINE;
bus.blitter.bltafwm = 0xFFFF;
bus.blitter.bltalwm = 0xFFFF;
bus.blitter.bltapt = 0;
bus.blitter.bltcpt = 0x1000;
bus.blitter.bltdpt = 0x1000;
bus.blitter.start_scheduled((4 << 6) | 2, &bus.mem.chip_ram);

// The startup ladder holds the BLS fence: with BLTPRI set the CPU is
// denied the register-commit/BLT_STRT/Init cycles even though they do
// not access the bus.
let mut warmup_cycles = 0;
while bus.blitter.bltpri_warmup_fences_cpu() {
assert_eq!(
bus.scheduled_dma_owner(true),
ChipBusOwner::Blitter,
"warm-up cycle must fence the CPU under BLTPRI"
);
bus.advance_chipset(1);
warmup_cycles += 1;
assert!(warmup_cycles < 8, "line warm-up should end with the ladder");
}

// Body: line mode's internal Bresenham cycles (L1/L3) release the bus
// request, so the CPU may use them even under BLTPRI -- line-heavy demo
// main loops (Rampage's vector parts) rely on that CPU time. Bus cycles
// (C fetch, D write) still deny the CPU.
let mut cpu_available = 0;
let mut guard = 0;
while bus.blitter.busy {
let owner = bus.scheduled_dma_owner(true);
if bus.blitter.current_slot_needs_bus() {
assert_eq!(owner, ChipBusOwner::Blitter);
} else if owner == ChipBusOwner::Idle {
cpu_available += 1;
}
bus.advance_chipset(1);
guard += 1;
assert!(guard < 64, "4-pixel line must finish within 64 cck");
}
assert!(
cpu_available >= 8,
"expected at least 2 CPU-available cycles per pixel, got {cpu_available}"
);
}

#[test]
fn front_panel_power_led_follows_cia_a_led_bit() {
let mut bus = empty_bus();
Expand Down
54 changes: 54 additions & 0 deletions src/chipset/blitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,32 @@ impl Blitter {
}
}

/// Whether the pending pipeline cycle falls inside the blit's warm-up
/// window, during which BLTPRI's BLS assertion fences the CPU off the
/// chip bus even though the cycle itself is bus-free. From the BLTSIZE
/// poke until the D pipeline is primed (the first D slot has passed) the
/// sequencer's bus request stays asserted: its first fetches are queued
/// back-to-back, so the startup ladder and the first-word bubble never
/// release the request line. MFM-decode trackloaders (e.g. Jim Power's)
/// depend on this: they restore a saved word below a decode blit's
/// destination right after writing BLTSIZE, relying on the nasty lockout
/// to keep that CPU write (and the instruction prefetches before it) out
/// of the startup and first-D holes. Once the pipeline is primed the
/// request drops on genuine bus-free micro-cycles -- disabled-channel
/// gaps, fill's idle cycle, line-mode Bresenham cycles -- and the CPU
/// may use them even under BLTPRI, which line-drawing main loops rely
/// on for CPU time (2 of the 4 line cycles per pixel are bus-free).
pub fn bltpri_warmup_fences_cpu(&self) -> bool {
if !self.busy {
return false;
}
match self.pending.as_ref() {
Some(PendingBlit::Normal(state)) => state.bltpri_warmup_fences_cpu(),
Some(PendingBlit::Line(state)) => state.bltpri_warmup_fences_cpu(),
None => false,
}
}

/// Arbitration class of the pipeline cycle the next `tick_scheduled_slot`
/// will process (see BlitSlotClass). `Internal` when no blit is pending
/// so callers need no extra guard.
Expand Down Expand Up @@ -1277,6 +1303,15 @@ impl LineBlitState {
}
}

/// Line-mode warm-up window for the BLTPRI CPU fence (see
/// `Blitter::bltpri_warmup_fences_cpu`): only the startup ladder. Line
/// mode has no D pipeline bubble -- the first D write is a real access --
/// so once the per-pixel cadence starts, the bus-free Bresenham cycles
/// (L1/L3) release the request line and stay CPU-available.
fn bltpri_warmup_fences_cpu(&self) -> bool {
matches!(self.phase, LineBlitPhase::StartDelay | LineBlitPhase::Init)
}

/// Access pattern of the next `limit` scheduled slots (bit k = slot k
/// consumes a blitter-eligible colour clock: a bus access or a bus-free
/// micro-cycle; clear = internal): the line startup ladder (register
Expand Down Expand Up @@ -1645,6 +1680,25 @@ impl NormalBlitState {
}
}

/// Warm-up window for the BLTPRI CPU fence (see
/// `Blitter::bltpri_warmup_fences_cpu`): the startup ladder plus, for
/// D-writing blits, the body cycles until the first D slot has primed
/// the hold register. That covers the first-word pipeline bubble; from
/// the second word on, bus-free micro-cycles release the request line.
/// The terminal E/F cycles are past the fence: BBUSY has already
/// dropped at the last body cycle.
fn bltpri_warmup_fences_cpu(&self) -> bool {
match self.phase {
NormalBlitPhase::StartDelay | NormalBlitPhase::Init => true,
NormalBlitPhase::A
| NormalBlitPhase::B
| NormalBlitPhase::C
| NormalBlitPhase::D
| NormalBlitPhase::FillIdle => self.use_d && !self.pipeline_full,
NormalBlitPhase::E | NormalBlitPhase::F | NormalBlitPhase::Done => false,
}
}

/// A D slot claims the bus only when a destination word is queued in
/// the hold register. The first word's D slot is always the HRM "-"
/// pipeline bubble -- including in D-only blits: real hardware writes
Expand Down
Loading
Loading