From 258fbff2fa2da175cff4a52b17955769545f6a8e Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 14 Oct 2026 08:26:03 +0700 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU --- crates/desktop/tests/rom_cli.rs | 4 ++- crates/gba-core/src/cpu/bios_write_tests.rs | 4 +-- crates/gba-core/src/cpu/iwram_dma_tests.rs | 2 +- crates/gba-core/src/cpu/thumb_empty_tests.rs | 2 +- crates/gba-core/src/cpu/thumb_tests.rs | 26 +++++++++++++---- crates/gba-core/src/cpu/transfer_tests.rs | 29 +++++++++---------- crates/gba-core/src/memory.rs | 7 +++-- .../src/memory/iwram_history_tests.rs | 2 +- crates/gba-core/src/memory/prefetch_tests.rs | 2 +- .../gba-core/src/memory/timer_step_tests.rs | 2 +- crates/gba-core/tests/bios.rs | 7 ++--- crates/gba-core/tests/bios/affine.rs | 5 ++-- crates/gba-core/tests/bios/bit_unpack.rs | 7 ++--- crates/gba-core/tests/bios/differential.rs | 10 ++----- crates/gba-core/tests/bios/huffman.rs | 7 ++--- crates/gba-core/tests/bios/lz77.rs | 5 ++-- crates/gba-core/tests/bios/run_length.rs | 7 ++--- crates/gba-core/tests/dma.rs | 4 +-- crates/gba-core/tests/dma/latch.rs | 8 ++--- docs/hardware/cartridge-gpio.md | 4 +-- docs/hardware/cartridge.md | 5 ++-- docs/hardware/dma.md | 2 +- docs/research/emerald-visible-progress.md | 3 ++ docs/status.md | 4 +-- docs/usability-roadmap.md | 2 +- 25 files changed, 86 insertions(+), 74 deletions(-) diff --git a/crates/desktop/tests/rom_cli.rs b/crates/desktop/tests/rom_cli.rs index 7f090a4..b065d79 100644 --- a/crates/desktop/tests/rom_cli.rs +++ b/crates/desktop/tests/rom_cli.rs @@ -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") diff --git a/crates/gba-core/src/cpu/bios_write_tests.rs b/crates/gba-core/src/cpu/bios_write_tests.rs index c4ef0b9..ecab531 100644 --- a/crates/gba-core/src/cpu/bios_write_tests.rs +++ b/crates/gba-core/src/cpu/bios_write_tests.rs @@ -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) diff --git a/crates/gba-core/src/cpu/iwram_dma_tests.rs b/crates/gba-core/src/cpu/iwram_dma_tests.rs index 55c02a9..7660235 100644 --- a/crates/gba-core/src/cpu/iwram_dma_tests.rs +++ b/crates/gba-core/src/cpu/iwram_dma_tests.rs @@ -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, ); diff --git a/crates/gba-core/src/cpu/thumb_empty_tests.rs b/crates/gba-core/src/cpu/thumb_empty_tests.rs index 0d11706..11960d6 100644 --- a/crates/gba-core/src/cpu/thumb_empty_tests.rs +++ b/crates/gba-core/src/cpu/thumb_empty_tests.rs @@ -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); diff --git a/crates/gba-core/src/cpu/thumb_tests.rs b/crates/gba-core/src/cpu/thumb_tests.rs index 3bf52ba..33e5169 100644 --- a/crates/gba-core/src/cpu/thumb_tests.rs +++ b/crates/gba-core/src/cpu/thumb_tests.rs @@ -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 }; diff --git a/crates/gba-core/src/cpu/transfer_tests.rs b/crates/gba-core/src/cpu/transfer_tests.rs index 2753b2c..32bc31a 100644 --- a/crates/gba-core/src/cpu/transfer_tests.rs +++ b/crates/gba-core/src/cpu/transfer_tests.rs @@ -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); } } diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index 2437932..249c35f 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -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)); } } diff --git a/crates/gba-core/src/memory/iwram_history_tests.rs b/crates/gba-core/src/memory/iwram_history_tests.rs index 19684ff..90c1a7f 100644 --- a/crates/gba-core/src/memory/iwram_history_tests.rs +++ b/crates/gba-core/src/memory/iwram_history_tests.rs @@ -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)); diff --git a/crates/gba-core/src/memory/prefetch_tests.rs b/crates/gba-core/src/memory/prefetch_tests.rs index 41349bf..0401595 100644 --- a/crates/gba-core/src/memory/prefetch_tests.rs +++ b/crates/gba-core/src/memory/prefetch_tests.rs @@ -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(); diff --git a/crates/gba-core/src/memory/timer_step_tests.rs b/crates/gba-core/src/memory/timer_step_tests.rs index c2b5862..15edee0 100644 --- a/crates/gba-core/src/memory/timer_step_tests.rs +++ b/crates/gba-core/src/memory/timer_step_tests.rs @@ -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(); diff --git a/crates/gba-core/tests/bios.rs b/crates/gba-core/tests/bios.rs index 43b5f9c..d00adf5 100644 --- a/crates/gba-core/tests/bios.rs +++ b/crates/gba-core/tests/bios.rs @@ -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; } diff --git a/crates/gba-core/tests/bios/affine.rs b/crates/gba-core/tests/bios/affine.rs index 5cd87ff..c7f3c1b 100644 --- a/crates/gba-core/tests/bios/affine.rs +++ b/crates/gba-core/tests/bios/affine.rs @@ -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); } } diff --git a/crates/gba-core/tests/bios/bit_unpack.rs b/crates/gba-core/tests/bios/bit_unpack.rs index 0b62bb8..10760f5 100644 --- a/crates/gba-core/tests/bios/bit_unpack.rs +++ b/crates/gba-core/tests/bios/bit_unpack.rs @@ -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] diff --git a/crates/gba-core/tests/bios/differential.rs b/crates/gba-core/tests/bios/differential.rs index 964e24a..06b3c53 100644 --- a/crates/gba-core/tests/bios/differential.rs +++ b/crates/gba-core/tests/bios/differential.rs @@ -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()) ); } } diff --git a/crates/gba-core/tests/bios/huffman.rs b/crates/gba-core/tests/bios/huffman.rs index bf59c79..d17b5e5 100644 --- a/crates/gba-core/tests/bios/huffman.rs +++ b/crates/gba-core/tests/bios/huffman.rs @@ -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] diff --git a/crates/gba-core/tests/bios/lz77.rs b/crates/gba-core/tests/bios/lz77.rs index 94e2663..4be8e4b 100644 --- a/crates/gba-core/tests/bios/lz77.rs +++ b/crates/gba-core/tests/bios/lz77.rs @@ -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); } diff --git a/crates/gba-core/tests/bios/run_length.rs b/crates/gba-core/tests/bios/run_length.rs index 6f3c5b4..6b58743 100644 --- a/crates/gba-core/tests/bios/run_length.rs +++ b/crates/gba-core/tests/bios/run_length.rs @@ -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] diff --git a/crates/gba-core/tests/dma.rs b/crates/gba-core/tests/dma.rs index cc1e91e..30c2925 100644 --- a/crates/gba-core/tests/dma.rs +++ b/crates/gba-core/tests/dma.rs @@ -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), }, ), ( diff --git a/crates/gba-core/tests/dma/latch.rs b/crates/gba-core/tests/dma/latch.rs index e5cfa6c..b8430be 100644 --- a/crates/gba-core/tests/dma/latch.rs +++ b/crates/gba-core/tests/dma/latch.rs @@ -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); diff --git a/docs/hardware/cartridge-gpio.md b/docs/hardware/cartridge-gpio.md index 99b622a..2fe607d 100644 --- a/docs/hardware/cartridge-gpio.md +++ b/docs/hardware/cartridge-gpio.md @@ -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 diff --git a/docs/hardware/cartridge.md b/docs/hardware/cartridge.md index 9ca6265..f5504cd 100644 --- a/docs/hardware/cartridge.md +++ b/docs/hardware/cartridge.md @@ -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). diff --git a/docs/hardware/dma.md b/docs/hardware/dma.md index 4950501..f9ffc5f 100644 --- a/docs/hardware/dma.md +++ b/docs/hardware/dma.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. diff --git a/docs/research/emerald-visible-progress.md b/docs/research/emerald-visible-progress.md index ce686b4..09af92d 100644 --- a/docs/research/emerald-visible-progress.md +++ b/docs/research/emerald-visible-progress.md @@ -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): diff --git a/docs/status.md b/docs/status.md index a6f7f9b..6805874 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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 diff --git a/docs/usability-roadmap.md b/docs/usability-roadmap.md index e54b409..79449a6 100644 --- a/docs/usability-roadmap.md +++ b/docs/usability-roadmap.md @@ -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.