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: 12 additions & 0 deletions docs/internals/chipset.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ Alice adds the FMODE wide-fetch latch, which scales the bitplane and
sprite fetch quanta (FMODE=0 stays byte-identical to the OCS/ECS slot
timing).

The CPU sees the reach too: the motherboard decode (Gary and equivalents)
routes the whole $000000-$1FFFFF window to Agnus, which decodes only as
many address bits as its DRAM reach, so the fitted chip RAM image repeats
across the window. A 512K OCS machine mirrors chip RAM at
$080000/$100000/$180000, and the 8372A (A20 unused) repeats its 1M image
at $100000, while addresses past the fitted RAM inside one image select
no DRAM bank and stay open bus ($080000-$0FFFFF on an 8372A with 512K
fitted). Kickstart's chip sizing detects the wrap and still reports the
fitted size. Action Replay freeze-disk loaders are a regression example:
they park the supervisor stack at $100000 on a 512K OCS machine and rely
on the pushes landing at the top of chip RAM.

Sprite DMA is modelled at the register level, the way the chips work:
there is no separate "descriptor" concept. Each channel keeps Agnus-side
copies of its SPRxPOS/SPRxCTL words and the vertical comparator values
Expand Down
95 changes: 92 additions & 3 deletions src/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::bus::{Bus, CpuBusAccessKind};
use crate::chipset::paula::{pending_ipl, INT_MASTER};
use crate::config::CpuModel;
use crate::memory::{
AUTOCONFIG_BASE, AUTOCONFIG_SIZE, CHIP_RAM_BASE, ROM_BASE, SLOW_RAM_BASE, WCS_BASE,
AUTOCONFIG_BASE, AUTOCONFIG_SIZE, CHIP_RAM_BASE, CHIP_WINDOW_SIZE, ROM_BASE, SLOW_RAM_BASE,
WCS_BASE,
};
use anyhow::{anyhow, Result};
use log::{debug, trace};
Expand Down Expand Up @@ -2223,7 +2224,7 @@ impl CpuBus {
if let Some(off) = self.overlay_rom_offset(addr, size) {
return Some(PlainMemRegion::OverlayRom(off));
}
if let Some(off) = region_offset(self.bus.mem.chip_ram.len(), CHIP_RAM_BASE, addr, size) {
if let Some(off) = self.chip_window_offset(addr, size) {
return Some(PlainMemRegion::ChipRam(off));
}
if let Some((board, off)) = self.bus.mem.zorro.region_at(addr, size) {
Expand All @@ -2249,6 +2250,26 @@ impl CpuBus {
None
}

/// CPU-side chip RAM decode. The motherboard address decode (Gary and
/// equivalents) routes the whole $000000-$1FFFFF window to Agnus, and
/// Agnus only decodes as many address bits as its DRAM reach
/// (dma_addr_capability_mask): an OCS 8370/8371 ignores A19/A20, so its
/// 512 KiB image repeats at $080000/$100000/$180000; the 1 MiB 8372A
/// ignores A20, repeating its 1 MiB image at $100000; the 2 MiB
/// 8375/Alice decode the full window with no repeat. Within one image,
/// addresses past the fitted RAM select no DRAM bank and stay open bus
/// (e.g. $080000-$0FFFFF on an 8372A with 512 KiB fitted). Kickstart's
/// chip sizing copes: it detects the wrap by checking whether a probe
/// write aliased the bottom of RAM.
fn chip_window_offset(&self, addr: u32, size: usize) -> Option<usize> {
let len = self.bus.mem.chip_ram.len();
if len == 0 || u64::from(addr) >= CHIP_WINDOW_SIZE {
return None;
}
let off = (addr & self.bus.agnus.revision().dma_addr_capability_mask()) as usize;
(off.checked_add(size)? <= len).then_some(off)
}

/// Canonical custom-register page offset for a CPU access that the
/// motherboard address decode routes to the custom chips: the canonical
/// $DFF000 page on every machine, plus the mirrors Gary's coarse decode
Expand Down Expand Up @@ -2464,7 +2485,7 @@ impl CpuBus {
if self.overlay_rom_offset(addr, 1).is_some() {
return false;
}
region_offset(self.bus.mem.chip_ram.len(), CHIP_RAM_BASE, addr, 1).is_some()
self.chip_window_offset(addr, 1).is_some()
|| self.bus.mem.zorro.region_at(addr, 1).is_some()
|| region_offset(self.bus.mem.slow_ram.len(), SLOW_RAM_BASE, addr, 1).is_some()
|| region_offset(self.bus.mem.rom.len(), ROM_BASE, addr, 1).is_some()
Expand Down Expand Up @@ -2722,6 +2743,14 @@ impl CpuBus {
self.bus.ui_note_cpu_ram_write(addr, size);
write_be(&mut self.bus.mem.chip_ram, off, size, value);
self.dbg_note_memw(addr, size);
if off as u32 != addr {
// The write landed through an Agnus image repeat: note
// the canonical chip address as well as the CPU's bus
// address, so watchpoints and UI highlights fire
// whichever alias of the RAM cell they were set on.
self.bus.ui_note_cpu_ram_write(off as u32, size);
self.dbg_note_memw(off as u32, size);
}
return;
}
Some(PlainMemRegion::ZorroRam(board, off)) => {
Expand Down Expand Up @@ -3444,6 +3473,66 @@ mod tests {
Ok(())
}

// The motherboard decode routes the whole $000000-$1FFFFF window to
// Agnus, which decodes only as many address bits as its DRAM reach, so
// the fitted chip RAM image repeats across the window (every 512 KiB on
// OCS). Regression example: Action Replay freeze-disk loaders point the
// supervisor stack at $100000 on a 512 KiB machine and rely on the
// pushes landing at the top of chip RAM.
#[test]
fn cpu_chip_window_image_aliases_fitted_ram() -> Result<()> {
// move.l #$DEADBEEF,$180010.l ; move.w $100012.l,d1
let mut machine = machine_with_program(
0x0500,
&[
0x23FC, 0xDEAD, 0xBEEF, 0x0018, 0x0010, // move.l #imm,$180010.l
0x3239, 0x0010, 0x0012, // move.w $100012.l,d1
],
)?;
// Watch both aliases of the target cell: the mirror write must fire
// the watch whichever alias it was set on.
machine.bus_mut().set_ui_mem_watches(&[0x10, 0x180010]);
machine.step_slice(2)?;
// On the fixture's OCS Agnus both $180010 and $100012 are images of
// chip $10/$12.
assert_eq!(read_chip_long(machine.bus(), 0x10), 0xDEADBEEF);
assert_eq!(machine.d(1) & 0xFFFF, 0xBEEF);
assert!(machine.bus_mut().ui_take_mem_writer(0x10).is_some());
assert!(machine.bus_mut().ui_take_mem_writer(0x180010).is_some());
Ok(())
}

#[test]
fn chip_window_images_follow_agnus_reach() -> Result<()> {
use crate::chipset::agnus::AgnusRevision;
let mut machine = machine_with_program(0x0500, &[0x4E71])?;
machine.bus_mut().mem.chip_ram[0x60000] = 0xA5;

// OCS (512 KiB reach, A19/A20 unused): the image repeats every
// 512 KiB, and a write through an image lands in the RAM cell.
assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xA5);
assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xA5);
assert_eq!(machine.debug_write_memory(0x1E0004, &[0x77]), 1);
assert_eq!(machine.bus().mem.chip_ram[0x60004], 0x77);

// 8372A (1 MiB reach, A20 unused): $0E0000 selects the unfitted
// second DRAM bank (open bus) while $160000 is the A20 image of
// $060000.
machine
.bus_mut()
.set_agnus_revision(AgnusRevision::Ecs8372Rev4);
assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xFF);
assert_eq!(machine.debug_write_memory(0x0E0000, &[0x11]), 0);
assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xA5);

// 8375/Alice (2 MiB reach): the full window is decoded, so nothing
// past the fitted RAM answers.
machine.bus_mut().set_agnus_revision(AgnusRevision::Ecs8375);
assert_eq!(machine.debug_read_memory(0x0E0000, 1)[0], 0xFF);
assert_eq!(machine.debug_read_memory(0x160000, 1)[0], 0xFF);
Ok(())
}

fn assert_single_instruction_timing(
label: &str,
slice: CpuStepSlice,
Expand Down
5 changes: 5 additions & 0 deletions src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pub const ROM_SIZE: usize = 512 * 1024;
pub const ROM_SIZE_256K: usize = 256 * 1024;
pub const ROM_BASE: u64 = 0x00F8_0000;
pub const CHIP_RAM_BASE: u64 = 0x0000_0000;
/// Size of the chip-RAM select window the motherboard address decode (Gary
/// and equivalents) routes to Agnus: $000000-$1FFFFF. Agnus decodes fewer
/// address bits than the window on the smaller parts, so the fitted RAM
/// image repeats inside it (see CpuBus::chip_window_offset).
pub const CHIP_WINDOW_SIZE: u64 = 0x0020_0000;
/// Conventional base of the first Zorro II RAM board (the start of the
/// Zorro II expansion space, where the ROM assigns it). Test fixtures
/// pre-configure their fast RAM boards here.
Expand Down
18 changes: 11 additions & 7 deletions src/video/window/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3192,13 +3192,17 @@ fn memory_tab_find_scroll_and_bitmap_toggle() {
assert_eq!(panel.mem_last_find, Some(0x60000));
assert_eq!(panel.mem_addr, 0x60000);
}
// Find again continues past the hit; with a single match the page
// wraps back around to the same place.
app.activate_ui_control(UiControl::DebugMemFind);
assert_eq!(
app.debugger_panel.as_ref().unwrap().mem_last_find,
Some(0x60000)
);
// Find again continues past the hit. The pattern is CPU-visible again
// at each Agnus image repeat of the 512 KiB chip RAM (OCS Agnus decodes
// only A1-A18, so the image recurs every $80000 across the $000000-
// $1FFFFF chip window), then the search wraps back to the original.
for expected in [0xE0000, 0x160000, 0x1E0000, 0x60000] {
app.activate_ui_control(UiControl::DebugMemFind);
assert_eq!(
app.debugger_panel.as_ref().unwrap().mem_last_find,
Some(expected)
);
}

// Scrolling moves by 16-byte hex rows.
app.debugger_mem_scroll(2);
Expand Down
Loading