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);
|
||||
|
|
|
|||
|
|
@ -52,9 +52,9 @@ Byte writes are diagnostic because the consulted bus-width description is uncert
|
|||
Byte, halfword, and word reads use ordinary little-endian lanes, including register/ROM boundaries.
|
||||
|
||||
Only the documented first-window addresses are implemented.
|
||||
Other ROM windows still expose supplied ROM bytes; their writes remain read-only diagnostics.
|
||||
Other ROM windows still expose supplied ROM bytes; CPU and DMA writes there are ignored like other ROM writes.
|
||||
GPIO mirroring and writes at alias addresses have not been independently validated.
|
||||
Without explicit hardware selection, all these addresses remain ordinary read-only cartridge memory.
|
||||
Without explicit hardware selection, all these addresses remain ordinary read-only cartridge memory, so CPU and DMA writes are ignored.
|
||||
|
||||
## RTC command and control transfers
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ It does not decode archives or ELF executables. Supply a raw binary with an ARM
|
|||
Headerless original programs are accepted.
|
||||
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values.
|
||||
All three Game Pak ROM windows use the same supplied bytes and existing wait-state rules.
|
||||
Ordinary cartridge bytes remain read-only.
|
||||
Ordinary cartridge bytes remain read-only. CPU and DMA writes to the `0x08`–`0x0c` ROM windows pay their bus cost and are ignored, as on hardware.
|
||||
Writes to the `0x0d` window remain `ReadOnly` diagnostics until EEPROM is modeled there. Host `Memory::write*` calls stay strict everywhere.
|
||||
Optional `--save-type flash64` or `--save-type flash128` enables [nominal Flash programming, erase, and reads](saves.md).
|
||||
Without `--save-file PATH`, changes remain volatile. With it, the frontend loads an exact-size image and persists changes on clean exit.
|
||||
Without `--save-file PATH`, changes remain volatile. With it, the frontend loads an exact-size image and persists changes on clean exit or when a ROM window ends on a diagnostic.
|
||||
Missing images start erased. Existing images receive unique backups before replacement; incomplete Flash operations refuse persistence.
|
||||
Read the [storage safeguards and limits](saves.md#desktop-persistence) before using a valuable save.
|
||||
Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC calendar interface](cartridge-gpio.md).
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Writes into the mapped 16 KiB BIOS [complete without changing its image](bios.md
|
|||
Source reads, channel data, address progression, timing, and completion IRQs still apply to these ignored writes.
|
||||
DMA0–2 mask destination bit 27, so a programmed cartridge address can instead select the BIOS region.
|
||||
Other accesses use the existing memory map, alignment checks, mirrors, and I/O write rules.
|
||||
Other unmapped reads and cartridge writes remain diagnostics, not general DMA open-bus behavior.
|
||||
Other unmapped reads remain diagnostics, not general DMA open-bus behavior. Writes to the `0x08`–`0x0c` ROM windows are ignored; the `0x0d` window remains a diagnostic.
|
||||
A failed unit changes no memory, device state, or clock. Earlier units remain committed; the failed unit remains pending.
|
||||
Successful units update local IWRAM lanes for their actual IWRAM reads and writes, in that order.
|
||||
No CPU continuation is required; DMA can establish lanes before the first CPU instruction.
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ folding plus write-ignore semantics need independent hardware evidence and
|
|||
a deliberate policy decision against the explicit-error convention. See
|
||||
[the usability roadmap](../usability-roadmap.md) for sequencing.
|
||||
|
||||
Update: emulated writes to the `0x08`–`0x0c` ROM windows are now ignored, which removes the
|
||||
`0x09fe2ffe` store diagnostic. Reads past the end of the image remain explicit errors.
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK memory map](https://problemkaputt.de/gbatek-gba-memory-map.htm):
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- Stack save/restore, PC loads, empty register lists, and ARM7 base-register overlap rules.
|
||||
- `SWP`/`SWPB`, including unaligned word rotation and register aliases.
|
||||
- 256 KiB external work RAM and 32 KiB internal work RAM, including mirrors.
|
||||
- Read-only cartridge bytes in the three GBA cartridge windows.
|
||||
- Read-only cartridge bytes in the three GBA cartridge windows; emulated writes below the `0x0d` window are ignored.
|
||||
- Explicit cartridge RTC selection with first-window GPIO read enable, direction/latch behavior, and serial calendar/control transactions.
|
||||
- Validated RTC date/time, BCD transfers, read snapshots, complete writes/reset, and caller-supplied battery-clock advancement.
|
||||
- Desktop `--rtc` seeds UTC once and supplies host elapsed seconds independently of CPU cycles and STOP.
|
||||
|
|
@ -230,7 +230,7 @@ Host inspection and instruction fetches remain strict. Other unmapped reads retu
|
|||
Reads beyond the supplied cartridge bytes also return errors.
|
||||
Direct memory-bus halfword and word accesses require alignment.
|
||||
CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus.
|
||||
Ordinary ROM writes remain read-only. Explicitly selected cartridge GPIO and supported Flash command writes are separate from ROM storage.
|
||||
CPU and DMA writes to the `0x08`–`0x0c` ROM windows are ignored; the `0x0d` window stays diagnostic until EEPROM exists. Explicitly selected cartridge GPIO and supported Flash command writes are separate from ROM storage.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ A reproducible source build or an accessible upstream ROM is needed before recor
|
|||
Next implementation steps:
|
||||
|
||||
1. Visible startup/menu progress is established: attract mode reaches the start menu and Birch dialogue, and sparse taps reach the in-game start menu by 12,000 frames ([probe](research/emerald-visible-progress.md)).
|
||||
The first demonstrated second-game failure is a boot-init halfword store to ROM mirror `0x09fe2ffe` (recorded, not fixed).
|
||||
The first demonstrated second-game failure is a boot-init halfword store to ROM mirror `0x09fe2ffe`. ROM-window writes are now ignored; reads past the image end remain diagnostics.
|
||||
Use original regressions for any hardware correction; do not invent connected serial devices.
|
||||
2. Validate an external game's completed save/restart/load cycle using a disposable `--save-file` path.
|
||||
Refine nominal Flash timing and polling only with independent evidence and original regressions.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue