fix(memory): ignore CPU and DMA writes to the Game Pak ROM windows
Stores to 0x08-0x0c returned ReadOnly and stopped the machine. Hardware ignores them, and a second game's boot code stores to a ROM mirror. Emulated writes now pay their bus cost and change nothing. The 0x0d window stays a diagnostic until EEPROM is modeled there, and host writes stay strict. Tests that used a ROM store as their failing write now cross an ignored ROM word into the 0x0d window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU
This commit is contained in:
parent
66817c919d
commit
258fbff2fa
25 changed files with 86 additions and 74 deletions
|
|
@ -403,9 +403,11 @@ fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
|
|||
let fixture = Fixture::new();
|
||||
let path = fixture.rom(&rtc_setup());
|
||||
let original = fs::read(&path).unwrap();
|
||||
// Without the device, the GPIO store is an ordinary ignored ROM write. The read-back
|
||||
// then reaches past this short image, which remains a diagnostic.
|
||||
let absent = run(&path, "100");
|
||||
assert_eq!(absent.status.code(), Some(1));
|
||||
assert!(stderr(&absent).contains("read-only memory at 0x080000c8"));
|
||||
assert!(stderr(&absent).contains("unmapped memory at 0x080000c8"));
|
||||
let selected = command()
|
||||
.arg("--rtc")
|
||||
.arg("--rom")
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ fn unmapped_bios_and_cartridge_destinations_remain_retryable_diagnostics() {
|
|||
(0, false),
|
||||
(0x4000, true),
|
||||
(0x0100_0000, true),
|
||||
(ROM_START, true),
|
||||
(0x0d00_0000, true),
|
||||
] {
|
||||
let (mut cpu, mut memory, _) = setup(ROM_START, 0xe480_1004, false);
|
||||
if !mapped_bios {
|
||||
|
|
@ -201,7 +201,7 @@ fn unmapped_bios_and_cartridge_destinations_remain_retryable_diagnostics() {
|
|||
let error = machine.step().unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
MachineError::Cpu(CpuError::Memory(if address == ROM_START {
|
||||
MachineError::Cpu(CpuError::Memory(if address == 0x0d00_0000 {
|
||||
MemoryError::ReadOnly(address)
|
||||
} else {
|
||||
MemoryError::Unmapped(address)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
|
|||
machine.memory_mut(),
|
||||
3,
|
||||
if fail_destination { IWRAM } else { 0x0e00_0000 },
|
||||
if fail_destination { 0x0800_0000 } else { IWRAM },
|
||||
if fail_destination { 0x0d00_0000 } else { IWRAM },
|
||||
1,
|
||||
true,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ fn empty_stores_advance_devices_only_after_a_successful_single_word_write() {
|
|||
|
||||
for instruction in [0xc200, 0xb400] {
|
||||
for (address, error) in [
|
||||
(ROM_START, MemoryError::ReadOnly(ROM_START)),
|
||||
(0x0d00_0000, MemoryError::ReadOnly(0x0d00_0000)),
|
||||
(0x0e00_0000, MemoryError::Unmapped(0x0e00_0000)),
|
||||
] {
|
||||
let (mut cpu, mut memory) = setup(instruction, CODE);
|
||||
|
|
|
|||
|
|
@ -727,17 +727,28 @@ fn thumb_memory_errors_leave_state_unchanged() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn failed_thumb_store_to_rom_does_not_modify_rom_or_cpu() {
|
||||
fn thumb_store_to_rom_is_ignored_and_the_cartridge_window_stays_diagnostic() {
|
||||
let mut memory = program(&[0x6001]);
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = ROM_START;
|
||||
cpu.registers[1] = 0x1234_5678;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 2);
|
||||
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
|
||||
// Mirrors past the supplied image ignore stores as well.
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0x09fe_2ffc;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.pc(), ROM_START + 2);
|
||||
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[0] = 0x0d00_0000;
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START)))
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(0x0d00_0000)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -748,9 +759,14 @@ fn late_thumb_stack_and_block_errors_do_not_partially_commit() {
|
|||
memory.write32(0x07ff_fffc, 0x1234_5678).unwrap();
|
||||
let mut cpu = thumb_cpu();
|
||||
cpu.registers[15] = 0x0300_0000;
|
||||
cpu.registers[0] = 0x07ff_fffc;
|
||||
// Stores cross an ignored ROM word into the diagnostic cartridge window.
|
||||
cpu.registers[0] = if instruction == 0xc003 {
|
||||
0x0cff_fffc
|
||||
} else {
|
||||
0x07ff_fffc
|
||||
};
|
||||
cpu.registers[13] = if instruction == 0xb403 {
|
||||
0x0800_0004
|
||||
0x0d00_0004
|
||||
} else {
|
||||
0x07ff_fffc
|
||||
};
|
||||
|
|
|
|||
|
|
@ -302,7 +302,9 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
|
|||
memory.write32(0x07ff_fffc, 0xdead_beef).unwrap();
|
||||
let mut cpu = Cpu::new(0x0300_0000);
|
||||
cpu.registers[0] = 42;
|
||||
cpu.registers[2] = 0x07ff_fffc; // Second transfer reaches absent/read-only ROM.
|
||||
// The second load reaches absent ROM. The second store crosses an ignored
|
||||
// ROM word into the diagnostic cartridge window.
|
||||
cpu.registers[2] = if load { 0x07ff_fffc } else { 0x0cff_fffc };
|
||||
cpu.flags = flags();
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
|
|
@ -310,7 +312,7 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
|
|||
Err(CpuError::Memory(if load {
|
||||
MemoryError::Unmapped(ROM_START)
|
||||
} else {
|
||||
MemoryError::ReadOnly(ROM_START)
|
||||
MemoryError::ReadOnly(0x0d00_0000)
|
||||
}))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
|
|
@ -320,7 +322,7 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn block_load_can_read_rom_and_store_reports_read_only_memory() {
|
||||
fn block_load_can_read_rom_and_store_to_rom_is_ignored() {
|
||||
for load in [false, true] {
|
||||
let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
|
|
@ -330,13 +332,11 @@ fn block_load_can_read_rom_and_store_reports_read_only_memory() {
|
|||
assert_eq!(&cpu.registers[1..3], &[11, 22]);
|
||||
assert_eq!(cpu.registers[0], ROM_START + 12);
|
||||
} else {
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
cpu.registers[1] = 33;
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[0], ROM_START + 12);
|
||||
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 11);
|
||||
assert_eq!(memory.read32(ROM_START + 8).unwrap(), 22);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -414,19 +414,16 @@ fn swap_snapshots_aliased_source_destination_and_base_registers() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn swap_write_failure_preserves_destination_and_memory() {
|
||||
fn swap_with_rom_loads_the_old_value_and_ignores_the_store() {
|
||||
for byte in [false, true] {
|
||||
let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = ROM_START + 4;
|
||||
cpu.registers[1] = 123;
|
||||
cpu.registers[2] = 456;
|
||||
let before = cpu.registers;
|
||||
assert_eq!(
|
||||
cpu.step(&mut memory),
|
||||
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
|
||||
);
|
||||
assert_eq!(cpu.registers, before);
|
||||
cpu.step(&mut memory).unwrap();
|
||||
assert_eq!(cpu.registers[1], if byte { 0x78 } else { 0x1234_5678 });
|
||||
assert_eq!(cpu.registers[2], 456);
|
||||
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -987,7 +987,7 @@ impl Memory {
|
|||
}
|
||||
return Ok(());
|
||||
}
|
||||
if address < BIOS_SIZE as u32 {
|
||||
if address < BIOS_SIZE as u32 || matches!(address >> 24, 0x08..=0x0d) {
|
||||
// Only validated CPU/DMA writes reach here. Pay the bus cost but
|
||||
// never modify the mapped ROM image or drive BIOS read history.
|
||||
return Ok(());
|
||||
|
|
@ -1123,8 +1123,11 @@ impl Memory {
|
|||
Ok(0) // I/O does not use a RAM index.
|
||||
}
|
||||
0x08..=0x0d => {
|
||||
// Game Pak ROM ignores emulated writes; host setup stays strict.
|
||||
// The 0x0d window stays diagnostic until EEPROM is modeled there.
|
||||
let ignored = access == WriteAccess::Emulated && address >> 24 != 0x0d;
|
||||
for offset in 0..N as u32 {
|
||||
if !self.cartridge_state().mapped(address + offset) {
|
||||
if !ignored && !self.cartridge_state().mapped(address + offset) {
|
||||
return Err(MemoryError::ReadOnly(address + offset));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ fn failed_instruction_discards_its_iwram_fetch_and_failed_dma_preserves_committe
|
|||
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
|
||||
let dma = DMA_BASE + 3 * DMA_STRIDE;
|
||||
memory.write32(dma, IWRAM + 8).unwrap();
|
||||
memory.write32(dma + 4, ROM_START).unwrap(); // Read-only destination.
|
||||
memory.write32(dma + 4, 0x0d00_0000).unwrap(); // Diagnostic cartridge window.
|
||||
memory.write32(dma + 8, 0x8400_0001).unwrap();
|
||||
assert!(memory.step_dma().is_err());
|
||||
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ fn ram_dma_advances_the_queue_and_rom_dma_cancels_it_before_cpu_resume() {
|
|||
|
||||
#[test]
|
||||
fn failed_dma_preserves_queue_and_clocks() {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (RAM, ROM_START)] {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (RAM, 0x0d00_0000)] {
|
||||
let (mut cpu, mut memory) = prepared(false, &[NOP], &[]);
|
||||
enable(&mut memory);
|
||||
cpu.step_timed(&mut memory).unwrap();
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ fn dma_samples_timers_after_startup_and_source_and_writes_after_destination() {
|
|||
|
||||
#[test]
|
||||
fn dma_failures_preserve_timers_and_if_and_clear_the_transaction() {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (TIMER_BASE, ROM_START)] {
|
||||
for (source, destination) in [(0x0e00_0000, RAM), (TIMER_BASE, 0x0d00_0000)] {
|
||||
let mut memory = Memory::new(Vec::new()).unwrap();
|
||||
start(&mut memory, 0, 0xffff, 0xc0);
|
||||
let before = memory.io.timer_step();
|
||||
|
|
|
|||
|
|
@ -437,8 +437,8 @@ fn protected_or_wrapping_source_ranges_return_without_copying() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() {
|
||||
let (mut machine, _) = call(0x0b, false, [SOURCE, 0x07ff_fffc, 2 | (1 << 26)]);
|
||||
fn copy_failure_follows_an_ignored_rom_word_without_partial_instruction_changes() {
|
||||
let (mut machine, _) = call(0x0b, false, [SOURCE, 0x0cff_fffc, 2 | (1 << 26)]);
|
||||
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
|
||||
let mut failed = false;
|
||||
for _ in 0..100 {
|
||||
|
|
@ -448,12 +448,11 @@ fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() {
|
|||
if let Err(error) = machine.step() {
|
||||
assert_eq!(
|
||||
error,
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), cycles);
|
||||
assert_eq!(machine.last_timing(), timing);
|
||||
assert_eq!(machine.memory().read32(0x07ff_fffc).unwrap(), 0xaabb_ccdd);
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -498,12 +498,11 @@ fn destination_failure_keeps_preceding_halfword_stores() {
|
|||
angle: 0,
|
||||
}])
|
||||
};
|
||||
let (mut m, _) = prepare(service, false, [DATA, 0x07ff_fffc, 1, 2], 0x1f, &data);
|
||||
let (mut m, _) = prepare(service, false, [DATA, 0x0cff_fffc, 1, 2], 0x1f, &data);
|
||||
assert_eq!(
|
||||
fail(&mut m),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 256);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -361,10 +361,10 @@ fn truncated_descriptors_and_sources_return_memory_diagnostics() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn destination_failure_preserves_completed_word() {
|
||||
fn destination_failure_follows_an_ignored_rom_word() {
|
||||
let (mut m, _) = prepare(
|
||||
false,
|
||||
0x07ff_fffc,
|
||||
0x0cff_fffc,
|
||||
&[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4],
|
||||
8,
|
||||
8,
|
||||
|
|
@ -372,9 +372,8 @@ fn destination_failure_preserves_completed_word() {
|
|||
);
|
||||
assert_eq!(
|
||||
fail(&mut m),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x7856_3412);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -321,17 +321,13 @@ fn failed_source_reads_keep_completed_units_but_not_pending_halfwords() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn destination_failure_preserves_preceding_completed_halfword() {
|
||||
fn destination_failure_follows_an_ignored_rom_halfword() {
|
||||
for service in [0x17, 0x18] {
|
||||
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]);
|
||||
let (mut m, _) = prepare(service, false, 0x07ff_fffe, &data);
|
||||
let (mut m, _) = prepare(service, false, 0x0cff_fffe, &data);
|
||||
assert_eq!(
|
||||
fail(&mut m),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
|
||||
);
|
||||
assert_eq!(
|
||||
m.memory().read16(0x07ff_fffe).unwrap(),
|
||||
if service == 0x17 { 0x3412 } else { 0x12 }
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -412,14 +412,13 @@ fn input_word_refill_failure_does_not_commit_incomplete_output() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn destination_failure_preserves_preceding_completed_word() {
|
||||
fn destination_failure_follows_an_ignored_rom_word() {
|
||||
let (data, _) = encode(8, &pair(), &[0x12; 8]);
|
||||
let (mut m, _) = prepare(false, 0x07ff_fffc, &data);
|
||||
let (mut m, _) = prepare(false, 0x0cff_fffc, &data);
|
||||
assert_eq!(
|
||||
fail(&mut m),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(m.memory().read32(0x07ff_fffc).unwrap(), 0x1212_1212);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -431,10 +431,9 @@ fn destination_failure_preserves_preceding_completed_halfword() {
|
|||
Token::Literal(0x56),
|
||||
Token::Literal(0x78),
|
||||
]);
|
||||
let (mut machine, _) = prepare(0x12, false, 0x07ff_fffe, &compressed);
|
||||
let (mut machine, _) = prepare(0x12, false, 0x0cff_fffe, &compressed);
|
||||
assert_eq!(
|
||||
fail(&mut machine),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(machine.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,14 +317,13 @@ fn incomplete_halfwords_are_not_committed_after_source_failure() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn destination_failure_preserves_earlier_completed_writes() {
|
||||
fn destination_failure_follows_an_ignored_rom_halfword() {
|
||||
let (data, _) = stream(&[Block::Literal(vec![0x12, 0x34, 0x56, 0x78])]);
|
||||
let (mut m, _) = prepare(0x15, false, 0x07ff_fffe, &data);
|
||||
let (mut m, _) = prepare(0x15, false, 0x0cff_fffe, &data);
|
||||
assert_eq!(
|
||||
fail(&mut m),
|
||||
MachineError::Cpu(MemoryError::ReadOnly(ROM_START).into())
|
||||
MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
|
||||
);
|
||||
assert_eq!(m.memory().read16(0x07ff_fffe).unwrap(), 0x3412);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -686,10 +686,10 @@ fn invalid_memory_and_dma_register_destinations_are_diagnostics() {
|
|||
),
|
||||
(
|
||||
SOURCE,
|
||||
ROM_START,
|
||||
0x0d00_0000,
|
||||
DmaError::Memory {
|
||||
channel: 3,
|
||||
error: MemoryError::ReadOnly(ROM_START),
|
||||
error: MemoryError::ReadOnly(0x0d00_0000),
|
||||
},
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -145,12 +145,12 @@ fn zero_is_known_data_not_an_unknown_latch() {
|
|||
fn failed_units_preserve_the_old_latch_without_new_data_or_irq() {
|
||||
for (source, destination, control) in [
|
||||
(0x0e00_0000, DEST, 0x4400), // Unmapped reads do not use a known latch.
|
||||
(SOURCE, ROM_START, 0x4400),
|
||||
(SOURCE, 0x0d00_0000, 0x4400),
|
||||
(SOURCE, 0x0400_0058, 0x4400),
|
||||
(SOURCE, DMA_BASE, 0x4400),
|
||||
(SOURCE, 0x0400_0300, 0x4400),
|
||||
(SOURCE, DEST, 0x4580), // Unsupported source mode.
|
||||
(0, ROM_START, 0x4400), // A blocked source cannot bypass destination checks.
|
||||
(SOURCE, DEST, 0x4580), // Unsupported source mode.
|
||||
(0, 0x0d00_0000, 0x4400), // A blocked source cannot bypass destination checks.
|
||||
] {
|
||||
let mut machine = machine();
|
||||
seed(&mut machine, 3, DATA);
|
||||
|
|
@ -175,7 +175,7 @@ fn failed_units_preserve_the_old_latch_without_new_data_or_irq() {
|
|||
fn failed_destination_does_not_seed_a_cold_channel() {
|
||||
let mut machine = machine();
|
||||
machine.memory_mut().write32(SOURCE, DATA).unwrap();
|
||||
configure(machine.memory_mut(), 3, SOURCE, ROM_START, 1, 0x400);
|
||||
configure(machine.memory_mut(), 3, SOURCE, 0x0d00_0000, 1, 0x400);
|
||||
assert!(machine.step().is_err());
|
||||
machine.memory_mut().write16(base(3) + 10, 0).unwrap();
|
||||
configure(machine.memory_mut(), 3, 0, DEST, 1, 0x400);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue