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:
0xrsydn 2026-10-14 08:26:03 +07:00
commit 258fbff2fa
25 changed files with 86 additions and 74 deletions

View file

@ -403,9 +403,11 @@ fn rtc_selection_is_explicit_and_does_not_modify_the_rom_file() {
let fixture = Fixture::new(); let fixture = Fixture::new();
let path = fixture.rom(&rtc_setup()); let path = fixture.rom(&rtc_setup());
let original = fs::read(&path).unwrap(); 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"); let absent = run(&path, "100");
assert_eq!(absent.status.code(), Some(1)); 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() let selected = command()
.arg("--rtc") .arg("--rtc")
.arg("--rom") .arg("--rom")

View file

@ -183,7 +183,7 @@ fn unmapped_bios_and_cartridge_destinations_remain_retryable_diagnostics() {
(0, false), (0, false),
(0x4000, true), (0x4000, true),
(0x0100_0000, true), (0x0100_0000, true),
(ROM_START, true), (0x0d00_0000, true),
] { ] {
let (mut cpu, mut memory, _) = setup(ROM_START, 0xe480_1004, false); let (mut cpu, mut memory, _) = setup(ROM_START, 0xe480_1004, false);
if !mapped_bios { if !mapped_bios {
@ -201,7 +201,7 @@ fn unmapped_bios_and_cartridge_destinations_remain_retryable_diagnostics() {
let error = machine.step().unwrap_err(); let error = machine.step().unwrap_err();
assert_eq!( assert_eq!(
error, error,
MachineError::Cpu(CpuError::Memory(if address == ROM_START { MachineError::Cpu(CpuError::Memory(if address == 0x0d00_0000 {
MemoryError::ReadOnly(address) MemoryError::ReadOnly(address)
} else { } else {
MemoryError::Unmapped(address) MemoryError::Unmapped(address)

View file

@ -175,7 +175,7 @@ fn failed_dma_and_failed_resumed_instruction_preserve_last_completed_dma_history
machine.memory_mut(), machine.memory_mut(),
3, 3,
if fail_destination { IWRAM } else { 0x0e00_0000 }, if fail_destination { IWRAM } else { 0x0e00_0000 },
if fail_destination { 0x0800_0000 } else { IWRAM }, if fail_destination { 0x0d00_0000 } else { IWRAM },
1, 1,
true, true,
); );

View file

@ -141,7 +141,7 @@ fn empty_stores_advance_devices_only_after_a_successful_single_word_write() {
for instruction in [0xc200, 0xb400] { for instruction in [0xc200, 0xb400] {
for (address, error) in [ for (address, error) in [
(ROM_START, MemoryError::ReadOnly(ROM_START)), (0x0d00_0000, MemoryError::ReadOnly(0x0d00_0000)),
(0x0e00_0000, MemoryError::Unmapped(0x0e00_0000)), (0x0e00_0000, MemoryError::Unmapped(0x0e00_0000)),
] { ] {
let (mut cpu, mut memory) = setup(instruction, CODE); let (mut cpu, mut memory) = setup(instruction, CODE);

View file

@ -727,17 +727,28 @@ fn thumb_memory_errors_leave_state_unchanged() {
} }
#[test] #[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 memory = program(&[0x6001]);
let mut cpu = thumb_cpu(); let mut cpu = thumb_cpu();
cpu.registers[0] = ROM_START; 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; let before = cpu.registers;
assert_eq!( assert_eq!(
cpu.step(&mut memory), 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!(cpu.registers, before);
assert_eq!(memory.read16(ROM_START).unwrap(), 0x6001);
} }
#[test] #[test]
@ -748,9 +759,14 @@ fn late_thumb_stack_and_block_errors_do_not_partially_commit() {
memory.write32(0x07ff_fffc, 0x1234_5678).unwrap(); memory.write32(0x07ff_fffc, 0x1234_5678).unwrap();
let mut cpu = thumb_cpu(); let mut cpu = thumb_cpu();
cpu.registers[15] = 0x0300_0000; 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 { cpu.registers[13] = if instruction == 0xb403 {
0x0800_0004 0x0d00_0004
} else { } else {
0x07ff_fffc 0x07ff_fffc
}; };

View file

@ -302,7 +302,9 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
memory.write32(0x07ff_fffc, 0xdead_beef).unwrap(); memory.write32(0x07ff_fffc, 0xdead_beef).unwrap();
let mut cpu = Cpu::new(0x0300_0000); let mut cpu = Cpu::new(0x0300_0000);
cpu.registers[0] = 42; 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(); cpu.flags = flags();
let before = cpu.registers; let before = cpu.registers;
assert_eq!( assert_eq!(
@ -310,7 +312,7 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
Err(CpuError::Memory(if load { Err(CpuError::Memory(if load {
MemoryError::Unmapped(ROM_START) MemoryError::Unmapped(ROM_START)
} else { } else {
MemoryError::ReadOnly(ROM_START) MemoryError::ReadOnly(0x0d00_0000)
})) }))
); );
assert_eq!(cpu.registers, before); assert_eq!(cpu.registers, before);
@ -320,7 +322,7 @@ fn block_diagnostics_do_not_commit_partial_register_or_ram_changes() {
} }
#[test] #[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] { for load in [false, true] {
let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]); let mut memory = program(&[block(0x0080_0000, load, true, 0, 0x6), 11, 22]);
let mut cpu = Cpu::new(ROM_START); 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[1..3], &[11, 22]);
assert_eq!(cpu.registers[0], ROM_START + 12); assert_eq!(cpu.registers[0], ROM_START + 12);
} else { } else {
let before = cpu.registers; cpu.registers[1] = 33;
assert_eq!( cpu.step(&mut memory).unwrap();
cpu.step(&mut memory), assert_eq!(cpu.registers[0], ROM_START + 12);
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 11); 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] #[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] { for byte in [false, true] {
let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]); let mut memory = program(&[0xe100_1092 | (u32::from(byte) << 22), 0x1234_5678]);
let mut cpu = Cpu::new(ROM_START); let mut cpu = Cpu::new(ROM_START);
cpu.registers[0] = ROM_START + 4; cpu.registers[0] = ROM_START + 4;
cpu.registers[1] = 123; cpu.registers[1] = 123;
cpu.registers[2] = 456; cpu.registers[2] = 456;
let before = cpu.registers; cpu.step(&mut memory).unwrap();
assert_eq!( assert_eq!(cpu.registers[1], if byte { 0x78 } else { 0x1234_5678 });
cpu.step(&mut memory), assert_eq!(cpu.registers[2], 456);
Err(CpuError::Memory(MemoryError::ReadOnly(ROM_START + 4)))
);
assert_eq!(cpu.registers, before);
assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678); assert_eq!(memory.read32(ROM_START + 4).unwrap(), 0x1234_5678);
} }
} }

View file

@ -987,7 +987,7 @@ impl Memory {
} }
return Ok(()); 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 // Only validated CPU/DMA writes reach here. Pay the bus cost but
// never modify the mapped ROM image or drive BIOS read history. // never modify the mapped ROM image or drive BIOS read history.
return Ok(()); return Ok(());
@ -1123,8 +1123,11 @@ impl Memory {
Ok(0) // I/O does not use a RAM index. Ok(0) // I/O does not use a RAM index.
} }
0x08..=0x0d => { 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 { 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)); return Err(MemoryError::ReadOnly(address + offset));
} }
} }

View file

@ -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)); assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));
let dma = DMA_BASE + 3 * DMA_STRIDE; let dma = DMA_BASE + 3 * DMA_STRIDE;
memory.write32(dma, IWRAM + 8).unwrap(); 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(); memory.write32(dma + 8, 0x8400_0001).unwrap();
assert!(memory.step_dma().is_err()); assert!(memory.step_dma().is_err());
assert_eq!(memory.iwram_bus.committed_word(), Some(WORD)); assert_eq!(memory.iwram_bus.committed_word(), Some(WORD));

View file

@ -232,7 +232,7 @@ fn ram_dma_advances_the_queue_and_rom_dma_cancels_it_before_cpu_resume() {
#[test] #[test]
fn failed_dma_preserves_queue_and_clocks() { 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], &[]); let (mut cpu, mut memory) = prepared(false, &[NOP], &[]);
enable(&mut memory); enable(&mut memory);
cpu.step_timed(&mut memory).unwrap(); cpu.step_timed(&mut memory).unwrap();

View file

@ -249,7 +249,7 @@ fn dma_samples_timers_after_startup_and_source_and_writes_after_destination() {
#[test] #[test]
fn dma_failures_preserve_timers_and_if_and_clear_the_transaction() { 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(); let mut memory = Memory::new(Vec::new()).unwrap();
start(&mut memory, 0, 0xffff, 0xc0); start(&mut memory, 0, 0xffff, 0xc0);
let before = memory.io.timer_step(); let before = memory.io.timer_step();

View file

@ -437,8 +437,8 @@ fn protected_or_wrapping_source_ranges_return_without_copying() {
} }
#[test] #[test]
fn copy_failure_keeps_earlier_writes_but_not_partial_instruction_changes() { fn copy_failure_follows_an_ignored_rom_word_without_partial_instruction_changes() {
let (mut machine, _) = call(0x0b, false, [SOURCE, 0x07ff_fffc, 2 | (1 << 26)]); let (mut machine, _) = call(0x0b, false, [SOURCE, 0x0cff_fffc, 2 | (1 << 26)]);
machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap(); machine.memory_mut().write32(SOURCE, 0xaabb_ccdd).unwrap();
let mut failed = false; let mut failed = false;
for _ in 0..100 { 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() { if let Err(error) = machine.step() {
assert_eq!( assert_eq!(
error, error,
MachineError::Cpu(MemoryError::ReadOnly(0x0800_0000).into()) MachineError::Cpu(MemoryError::ReadOnly(0x0d00_0000).into())
); );
assert_eq!(machine.cpu(), &before); assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), cycles); assert_eq!(machine.cycles(), cycles);
assert_eq!(machine.last_timing(), timing); assert_eq!(machine.last_timing(), timing);
assert_eq!(machine.memory().read32(0x07ff_fffc).unwrap(), 0xaabb_ccdd);
failed = true; failed = true;
break; break;
} }

View file

@ -498,12 +498,11 @@ fn destination_failure_keeps_preceding_halfword_stores() {
angle: 0, 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!( assert_eq!(
fail(&mut m), 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);
} }
} }

View file

@ -361,10 +361,10 @@ fn truncated_descriptors_and_sources_return_memory_diagnostics() {
} }
#[test] #[test]
fn destination_failure_preserves_completed_word() { fn destination_failure_follows_an_ignored_rom_word() {
let (mut m, _) = prepare( let (mut m, _) = prepare(
false, false,
0x07ff_fffc, 0x0cff_fffc,
&[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4], &[0x12, 0x34, 0x56, 0x78, 1, 2, 3, 4],
8, 8,
8, 8,
@ -372,9 +372,8 @@ fn destination_failure_preserves_completed_word() {
); );
assert_eq!( assert_eq!(
fail(&mut m), 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] #[test]

View file

@ -321,17 +321,13 @@ fn failed_source_reads_keep_completed_units_but_not_pending_halfwords() {
} }
#[test] #[test]
fn destination_failure_preserves_preceding_completed_halfword() { fn destination_failure_follows_an_ignored_rom_halfword() {
for service in [0x17, 0x18] { for service in [0x17, 0x18] {
let (data, _) = filtered(service, &[0x12, 0x34, 0x56, 0x78]); 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!( assert_eq!(
fail(&mut m), 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(),
if service == 0x17 { 0x3412 } else { 0x12 }
); );
} }
} }

View file

@ -412,14 +412,13 @@ fn input_word_refill_failure_does_not_commit_incomplete_output() {
} }
#[test] #[test]
fn destination_failure_preserves_preceding_completed_word() { fn destination_failure_follows_an_ignored_rom_word() {
let (data, _) = encode(8, &pair(), &[0x12; 8]); 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!( assert_eq!(
fail(&mut m), 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] #[test]

View file

@ -431,10 +431,9 @@ fn destination_failure_preserves_preceding_completed_halfword() {
Token::Literal(0x56), Token::Literal(0x56),
Token::Literal(0x78), Token::Literal(0x78),
]); ]);
let (mut machine, _) = prepare(0x12, false, 0x07ff_fffe, &compressed); let (mut machine, _) = prepare(0x12, false, 0x0cff_fffe, &compressed);
assert_eq!( assert_eq!(
fail(&mut machine), 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);
} }

View file

@ -317,14 +317,13 @@ fn incomplete_halfwords_are_not_committed_after_source_failure() {
} }
#[test] #[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 (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!( assert_eq!(
fail(&mut m), 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] #[test]

View file

@ -686,10 +686,10 @@ fn invalid_memory_and_dma_register_destinations_are_diagnostics() {
), ),
( (
SOURCE, SOURCE,
ROM_START, 0x0d00_0000,
DmaError::Memory { DmaError::Memory {
channel: 3, channel: 3,
error: MemoryError::ReadOnly(ROM_START), error: MemoryError::ReadOnly(0x0d00_0000),
}, },
), ),
( (

View file

@ -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() { fn failed_units_preserve_the_old_latch_without_new_data_or_irq() {
for (source, destination, control) in [ for (source, destination, control) in [
(0x0e00_0000, DEST, 0x4400), // Unmapped reads do not use a known latch. (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, 0x0400_0058, 0x4400),
(SOURCE, DMA_BASE, 0x4400), (SOURCE, DMA_BASE, 0x4400),
(SOURCE, 0x0400_0300, 0x4400), (SOURCE, 0x0400_0300, 0x4400),
(SOURCE, DEST, 0x4580), // Unsupported source mode. (SOURCE, DEST, 0x4580), // Unsupported source mode.
(0, ROM_START, 0x4400), // A blocked source cannot bypass destination checks. (0, 0x0d00_0000, 0x4400), // A blocked source cannot bypass destination checks.
] { ] {
let mut machine = machine(); let mut machine = machine();
seed(&mut machine, 3, DATA); 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() { fn failed_destination_does_not_seed_a_cold_channel() {
let mut machine = machine(); let mut machine = machine();
machine.memory_mut().write32(SOURCE, DATA).unwrap(); 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()); assert!(machine.step().is_err());
machine.memory_mut().write16(base(3) + 10, 0).unwrap(); machine.memory_mut().write16(base(3) + 10, 0).unwrap();
configure(machine.memory_mut(), 3, 0, DEST, 1, 0x400); configure(machine.memory_mut(), 3, 0, DEST, 1, 0x400);

View file

@ -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. Byte, halfword, and word reads use ordinary little-endian lanes, including register/ROM boundaries.
Only the documented first-window addresses are implemented. 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. 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 ## RTC command and control transfers

View file

@ -33,9 +33,10 @@ It does not decode archives or ELF executables. Supply a raw binary with an ARM
Headerless original programs are accepted. Headerless original programs are accepted.
Reads past supplied cartridge bytes remain explicit memory errors, not open-bus values. 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. 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). 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. 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. 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). Optional `--rtc` selection enables the bounded [Game Pak GPIO and RTC calendar interface](cartridge-gpio.md).

View file

@ -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. 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. 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 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. 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. 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. No CPU continuation is required; DMA can establish lanes before the first CPU instruction.

View file

@ -91,6 +91,9 @@ folding plus write-ignore semantics need independent hardware evidence and
a deliberate policy decision against the explicit-error convention. See a deliberate policy decision against the explicit-error convention. See
[the usability roadmap](../usability-roadmap.md) for sequencing. [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 ## References
- [GBATEK memory map](https://problemkaputt.de/gbatek-gba-memory-map.htm): - [GBATEK memory map](https://problemkaputt.de/gbatek-gba-memory-map.htm):

View file

@ -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. - Stack save/restore, PC loads, empty register lists, and ARM7 base-register overlap rules.
- `SWP`/`SWPB`, including unaligned word rotation and register aliases. - `SWP`/`SWPB`, including unaligned word rotation and register aliases.
- 256 KiB external work RAM and 32 KiB internal work RAM, including mirrors. - 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. - 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. - 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. - 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. Reads beyond the supplied cartridge bytes also return errors.
Direct memory-bus halfword and word accesses require alignment. Direct memory-bus halfword and word accesses require alignment.
CPU load/store instructions apply ARM7TDMI alignment and rotation rules before accessing the bus. 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 ## Next steps

View file

@ -65,7 +65,7 @@ A reproducible source build or an accessible upstream ROM is needed before recor
Next implementation steps: 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)). 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. 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. 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. Refine nominal Flash timing and polling only with independent evidence and original regressions.