diff --git a/README.md b/README.md index 191e47d..b301606 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ Verified locally on macOS 15.7.3: - Nix system: `aarch64-darwin`. - Rust host: `aarch64-apple-darwin`. - Release executable: native Mach-O `arm64`, without Rosetta. -- All 694 tests pass in debug and release builds. +- All 705 tests pass in debug and release builds. - All ten native window smoke tests pass. The mosaic test submits 128 frames; the other nine submit 60 frames each. - The eight CPU-driven smoke tests check CPU state, every output pixel, and presentation at VBlank entry. - The terminal-only CPU and timer IRQ demos run successfully. @@ -358,7 +358,7 @@ nix develop -c cargo test - Game Pak wait-state control (`WAITCNT`) for all three ROM windows. - Sequential/non-sequential data accesses, branch refill costs, and variable multiply timing. - Optional caller-supplied 16 KiB BIOS mapping for vector code, without CPU BIOS read protection. -- Optional original BIOS replacement: minimal boot, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters. +- Optional original BIOS replacement: minimal boot, SoftReset, IRQ dispatch, interrupt waits, memory copy/fill, integer/fixed-point arithmetic, affine matrices, bit unpacking, LZ77/run-length/Huffman decompression, and differential filters. - All 16 ARM data-processing operations, with immediate and shifted-register operands. - Logical operations: `AND`, `EOR`, `TST`, `TEQ`, `ORR`, `MOV`, `BIC`, and `MVN`. - Arithmetic operations: `SUB`, `RSB`, `ADD`, `ADC`, `SBC`, `RSC`, `CMP`, and `CMN`. @@ -507,6 +507,7 @@ Supported software interrupt services: | Number | Service | Behavior | | --- | --- | --- | +| `0x00` | SoftReset | Clear BIOS work RAM and reset CPU registers/stacks; restart in ROM or RAM without returning | | `0x02` | Halt | Wait for `IE & IF`; preserve IME and caller registers | | `0x04` | IntrWait | `r0`: discard old selected flags when nonzero; `r1`: flags to wait for | | `0x05` | VBlankIntrWait | IntrWait with discard enabled and the VBlank flag selected | @@ -531,7 +532,8 @@ Supported software interrupt services: ARM code uses `SWI service_number << 16`; Thumb code uses the service number directly. Only User/System callers are supported. Calls from exception modes return a diagnostic rather than risking nested stack corruption. -Services preserve CPSR and every caller register except the documented arithmetic outputs. +Returning services preserve CPSR and every caller register except the documented arithmetic outputs. +SoftReset instead initializes CPU registers and status as described below. This is deterministic behavior, not a claim about undocumented firmware outputs. Supervisor stack use is 28 bytes normally, including ArcTan. Division and ArcTan2 use 40 bytes. CpuFastSet, affine-matrix services, BitUnPack, all three decompression formats, and differential filters use 60 bytes. @@ -560,6 +562,45 @@ Other invalid accesses use normal memory diagnostics. Earlier successful writes These copy services keep CPU IRQ delivery masked until return; device clocks and DMA continue throughout. BIOS bus protection, exact firmware timing, undocumented side effects, and nested service calls remain incomplete. +#### SoftReset + +`SoftReset` (`SWI 0x00`) does not return to its caller. +It reads the byte at `0x03007ffa` before clearing the containing RAM: +zero selects cartridge ROM at `0x08000000`; any nonzero value selects work RAM at `0x02000000`. +Surrounding bytes do not affect this choice. Both destinations start in ARM state, even for Thumb callers. + +The service clears exactly `0x03007e00..0x03007fff`, including BIOS IRQ flags, the callback pointer, and the restart flag. +It resets r0–r12 to zero and initializes these CPU banks: + +| Mode | Stack pointer | Link register | Saved status | +| --- | --- | --- | --- | +| System/User | `0x03007f00` | Selected restart address | Not present | +| Supervisor | `0x03007fe0` | Zero | Zero | +| IRQ | `0x03007fa0` | Zero | Zero | + +Other banked registers and saved status registers remain unchanged. +The service enters System mode with CPSR `0x9f`: IRQ masked, FIQ unmasked, condition flags clear. +This is the subset's deterministic status policy, not independently verified hardware behavior. +It jumps through the System link register and does not execute the cold-boot sequence. + +SoftReset does not reset I/O registers or devices. +IME, IE, pending IF bits, POSTFLG, display configuration, and timer configuration remain in place. +Device clocks and DMA continue, so counters and pending requests can change during the service. +IRQ delivery stays masked throughout the service and after restart. +Restart code must install an IRQ callback and configure interrupts before unmasking IRQ. + +The common SWI entry still requires an initialized, writable Supervisor stack and saves a 28-byte frame. +SoftReset abandons that frame. It uses no further stack storage and never restores the old caller state. +With default stacks, the frame lies inside the cleared RAM. +If the Supervisor stack is outside this region, its entry writes remain there after reset. +The clearing loop performs ordinary word stores; stopping execution partway leaves a partially cleared region. +Active DMA can still modify memory. Software must stop conflicting transfers before requesting a restart. + +Lower internal RAM, external RAM, palette RAM, video RAM, and sprite attribute memory are not cleared by SoftReset. +Use separate initialization code for those regions. `RegisterRamReset` is not implemented yet. +Calls from exception modes retain the existing unsupported-service diagnostic. +Exact firmware timing, bus-protection behavior, and undocumented side effects remain incomplete. + #### Integer arithmetic Div truncates the signed quotient toward zero. The remainder has the numerator's sign, or is zero. @@ -808,7 +849,7 @@ Unsupported SWIs, unsupported exception vectors, and null/misaligned IRQ callbac The CPU reports `CpuError::UnsupportedInstruction` with `bios::UNSUPPORTED_TRAP`, rather than silently treating a service as a no-op. Invalid arithmetic/decompression arguments use the same CPU error type with the distinct `bios::INVALID_ARGUMENT_TRAP` instruction. Prior boot/service steps remain committed on failure. -BIOS reset services and STOP remain unimplemented. +RegisterRamReset, HardReset, and STOP remain unimplemented. This subset is not sufficient for Pokémon Emerald compatibility. ### Timers, interrupt registers, and the machine clock @@ -1559,6 +1600,7 @@ Important timing limits: | `src/timer_demo.rs` | Original timer-configuration program and IRQ handler | | `src/lib.rs` | Core modules and original demo bytes | | `src/bios.rs` | Original ARM BIOS image builder, minimal boot, IRQ dispatch, waits, and memory services | +| `src/bios/reset.rs` | Emitted ARM SoftReset, BIOS work RAM clearing, bank initialization, and ROM/RAM restart | | `src/bios/arithmetic.rs` | Emitted ARM division and integer-square-root routines | | `src/bios/affine.rs` | Emitted ARM background/sprite matrix services, range checks, and generated sine table | | `src/bios/angles.rs` | Emitted ARM ArcTan polynomial, ArcTan2 ratio/quadrant handling, and input validation | @@ -1619,6 +1661,7 @@ Important timing limits: | `tests/dma.rs` | DMA widths, latches, priority, triggers, IRQs, timing, errors, and CPU integration | | `tests/halt.rs` | HALT wake masks, idle timing, BIOS-only writes, DMA progress, and STOP diagnostics | | `tests/bios.rs` | ARM/Thumb service calls, copy/fill boundaries, wait races, callback contracts, and boot | +| `tests/bios/reset.rs` | All restart flags, exact RAM boundaries, CPU banks, restart execution, device continuity, and IRQ masking | | `tests/bios/arithmetic.rs` | Arithmetic boundaries, wide-integer references, status restoration, and zero-division diagnostics | | `tests/bios/affine.rs` | Matrix/origin references, all angle phases, strides, live rendering, register preservation, and diagnostics | | `tests/bios/angles.rs` | Fixed-point references, axes/quadrants, rounding, caller flags, stack bounds, DMA, and diagnostics | @@ -1694,12 +1737,12 @@ To test the core without building the window dependency: direnv exec . cargo test --locked --no-default-features ``` -This runs 683 core and integration tests; the eleven desktop and command-line tests are excluded. +This runs 694 core and integration tests; the eleven desktop and command-line tests are excluded. On Apple Silicon, `file` must report a Mach-O `arm64` executable. Do not set a Linux cross-compilation target for this validation. -The default suite has 694 tests. +The default suite has 705 tests. Angle-service tests compare results with wide-integer polynomial and signed-sector references, plus floating-point accuracy checks in supported ranges. They cover a dense ArcTan unit-interval grid, sampled full-domain and seeded inputs, axes, quadrant boundaries, signed extremes, and scale invariance. Other tests verify all caller flag combinations, User/System masks, non-result registers, stack bounds, repeated calls, diagnostics, and DMA/IRQ progress. @@ -1774,6 +1817,10 @@ The CPU demo test checks sprite controls, OAM attributes, disabled unused object Tile tests cover both color depths, palette banks, flips, all map sizes, scrolling, transparency, priorities, and register masks. They check errors for unsupported features and out-of-range fetches without changing the output image. The CPU tile demo test verifies asset copies, every pixel, wrapping, opposite directions, reset, and VBlank synchronization. +SoftReset tests check every restart flag byte from ARM and Thumb, exact RAM clearing, and preserved memory. +They seed CPU banks and verify stack pointers, link registers, saved status, caller modes, and restart status. +Other tests execute RAM restart code, call BIOS services again, and restart from code inside the erased region. +Device tests check retained I/O state, advancing timers/DMA, pending IRQs, and instruction-by-instruction RAM clearing. BIOS affine tests check all 256 angle phases against independent trigonometric and wide-integer references. They cover signed scales, origin wraparound, rounding, ignored angle bits, strides, alignment, and partial failures. Live rendering tests apply generated matrices to background registers and sprite attribute memory. @@ -1835,7 +1882,7 @@ These tests do not replace validation against GBA hardware or public hardware te 1. Validate CPU behavior with public ARM7TDMI test programs before claiming instruction compatibility. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. -3. Expand BIOS services with reset functions; add STOP and remaining DMA device modes. +3. Add RegisterRamReset with explicit limits for unimplemented sound/serial registers; add STOP and remaining DMA device modes. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility. @@ -1846,11 +1893,12 @@ Hardware references used for the core: - [GBATEK ARM instruction cycle times](https://problemkaputt.de/gbatek-arm-cpu-instruction-cycle-times.htm). - [GBATEK GBA system control and WAITCNT](https://problemkaputt.de/gbatek-gba-system-control.htm). +- [GBATEK BIOS reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm), for SoftReset RAM bounds, CPU banks, stack pointers, and restart selection. - [GBATEK BIOS halt functions](https://problemkaputt.de/gbatek-bios-halt-functions.htm), for wait contracts and STOP differences. - [GBATEK BIOS function calling conventions](https://problemkaputt.de/gbatek-bios-functions.htm). - [GBATEK BIOS memory-copy services](https://problemkaputt.de/gbatek-bios-memory-copy.htm). - [GBATEK BIOS rotation/scaling services](https://problemkaputt.de/gbatek-bios-rotation-scaling-functions.htm), for record layouts, angle units, and output strides. -- [VisualBoyAdvance-M BIOS services](https://github.com/visualboyadvance-m/visualboyadvance-m/blob/master/src/core/gba/internal/gbaBios.cpp), reviewed for integer affine rounding and signed matrix intermediates. Our sine table is generated mathematically. +- [VisualBoyAdvance-M BIOS services](https://github.com/visualboyadvance-m/visualboyadvance-m/blob/master/src/core/gba/internal/gbaBios.cpp), reviewed for integer affine rounding, signed matrix intermediates, and SoftReset status handling. Our sine table is generated mathematically. - [GBATEK BIOS arithmetic services](https://problemkaputt.de/gbatek-bios-arithmetic-functions.htm). - [GBATEK BIOS decompression services](https://problemkaputt.de/gbatek-bios-decompression-functions.htm), including BitUnPack descriptor fields, differential-filter headers, Huffman tree layout, and width constraints. - [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for angle-polynomial coefficients and quadrant conventions, BitUnPack ordering/offsets, and Huffman tree layout and packing. diff --git a/src/bios.rs b/src/bios.rs index 083d386..9863f5e 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,5 +1,5 @@ //! Original, optional ARM BIOS replacement. No Nintendo firmware is included. -//! Supports interrupt waits, integer/fixed-point arithmetic, affine matrices, +//! Supports SoftReset, interrupt waits, integer/fixed-point arithmetic, affine matrices, //! memory copy/fill, bit unpacking, LZ77/run-length/Huffman decoding, and differential filters. //! This is a functional subset, not a complete boot ROM or a timing-compatible BIOS. @@ -13,6 +13,7 @@ mod decompression; mod differential; mod huffman; mod lz77; +mod reset; mod run_length; use crate::{ @@ -46,8 +47,10 @@ pub fn boot(rom: Vec) -> Result { /// Build a deterministic 16 KiB image from original ARM instructions and a /// mathematically generated sine table. No Nintendo firmware bytes are included. /// SWIs use bits 16–23 of the ARM immediate, or the Thumb immediate byte. -/// Services support User/System callers and preserve caller status. Registers -/// other than each arithmetic service's documented outputs are preserved. +/// Services support User/System callers. Returning services preserve caller status +/// and registers other than each arithmetic service's documented outputs. +/// SoftReset instead clears BIOS work RAM, resets specified CPU banks, and enters +/// ROM or RAM in ARM System mode with IRQ masked. It does not reset devices. /// IRQ callbacks must preserve r4–r11, acknowledge IF, update IRQ_FLAGS for wait /// services, and return with BX lr. Nested IRQs and SWIs from callbacks are unsupported. /// Service routines use the Supervisor stack; exact firmware stack layout and @@ -118,6 +121,7 @@ pub fn image() -> Vec { a.emit(0x01a0_c82c); // MOVEQ r12,r12,LSR #16 a.emit(0xe20c_c0ff); // AND r12,r12,#0xff for (number, target) in [ + (0, "soft_reset"), (2, "halt"), (4, "intr_wait"), (5, "vblank_wait"), @@ -233,6 +237,7 @@ pub fn image() -> Vec { // The preceding routine ends with an unconditional branch. Place a literal // pool here so reset/dispatcher loads stay within ARM's 4 KiB reach. a.flush_literals(); + reset::emit(&mut a); arithmetic::emit(&mut a); affine::emit(&mut a); angles::emit(&mut a); diff --git a/src/bios/reset.rs b/src/bios/reset.rs new file mode 100644 index 0000000..4022fd8 --- /dev/null +++ b/src/bios/reset.rs @@ -0,0 +1,38 @@ +//! Original ARM SoftReset. This is a restart service, not a hardware reset. + +use super::{ArmImage, IRQ_STACK, ROM_START, SVC_STACK, SYSTEM_STACK}; + +pub(super) fn emit(a: &mut ArmImage) { + a.label("soft_reset"); + // Read the byte before clearing its containing RAM and the common SWI frame. + // No stack accesses follow: this service never returns to its caller. + a.literal(0, 0x0300_7ffa); + a.emit(0xe5d0_2000); // LDRB r2,[r0] (zero: ROM, nonzero: EWRAM) + a.literal(0, 0x0300_7e00); + a.emit(0xe3a0_1080); // MOV r1,#128 (512 bytes) + a.emit(0xe3a0_3000); // MOV r3,#0 + a.label("soft_reset_clear"); + a.emit(0xe480_3004); // STR r3,[r0],#4 + a.emit(0xe251_1001); // SUBS r1,r1,#1 + a.branch(1, "soft_reset_clear"); + + a.emit(0xe169_f003); // MSR SPSR_fc,r3 (SVC saved status = 0) + a.emit(0xe1a0_e003); // MOV lr,r3 (SVC link = 0) + a.literal(13, SVC_STACK); + a.emit(0xe321_f0d2); // MSR CPSR_c,#IRQ|I|F + a.emit(0xe169_f003); // MSR SPSR_fc,r3 (IRQ saved status = 0) + a.emit(0xe1a0_e003); // MOV lr,r3 (IRQ link = 0) + a.literal(13, IRQ_STACK); + a.emit(0xe321_f0df); // MSR CPSR_c,#System|I|F + a.literal(13, SYSTEM_STACK); + a.emit(0xe352_0000); // CMP r2,#0 + a.literal(14, ROM_START); + a.emit(0x13a0_e402); // MOVNE lr,#0x02000000 + for register in 0..=12 { + a.emit(0xe3a0_0000 | register << 12); // MOV rN,#0 + } + // Deterministic subset policy: clear NZCV and F, keep I set. Do not change + // IME, IE, IF, devices, or unrelated CPU banks. IRQ stays masked throughout. + a.emit(0xe329_f09f); // MSR CPSR_fc,#System|I + a.emit(0xe12f_ff1e); // BX lr (ARM target, no return through erased SWI frame) +} diff --git a/tests/bios.rs b/tests/bios.rs index 160d012..7753d85 100644 --- a/tests/bios.rs +++ b/tests/bios.rs @@ -26,6 +26,8 @@ mod differential; mod huffman; #[path = "bios/lz77.rs"] mod lz77; +#[path = "bios/reset.rs"] +mod reset; #[path = "bios/run_length.rs"] mod run_length; @@ -227,7 +229,7 @@ fn vblank_wait_halts_dispatches_irq_and_returns_through_original_code() { #[test] fn unsupported_services_fail_at_an_explicit_instruction_trap() { - for service in [0, 1, 3, 0x0d, 0x19, 0xff] { + for service in [1, 3, 0x0d, 0x19, 0xff] { let (mut machine, _) = call(service, false, [0, 0, 0]); let error = (0..100) .find_map(|_| machine.step().err()) diff --git a/tests/bios/reset.rs b/tests/bios/reset.rs new file mode 100644 index 0000000..efcaa23 --- /dev/null +++ b/tests/bios/reset.rs @@ -0,0 +1,363 @@ +use super::*; +use gba_rust::{ + dma::DMA_BASE, + io::{BG0CNT, DISPCNT, WAITCNT}, + memory::{OAM_START, PALETTE_START}, +}; + +const FLAG: u32 = 0x0300_7ffa; +const CLEAR_START: u32 = 0x0300_7e00; +const CLEAR_END: u32 = 0x0300_8000; + +fn restart(m: &mut Machine, flag: u8) { + m.memory_mut().write8(FLAG, flag).unwrap(); + m.step().unwrap(); // Enter the real SWI vector. + reach(m, if flag == 0 { ROM_START } else { SOURCE }, 2000); +} + +fn assert_restart_state(m: &Machine, target: u32) { + assert_eq!(m.cpu().pc(), target); + assert_eq!(m.cpu().mode(), Mode::System); + assert_eq!(m.cpu().instruction_set(), InstructionSet::Arm); + assert_eq!(m.cpu().cpsr(), 0x9f); // Deterministic policy: IRQ masked, FIQ clear, NZCV clear. + assert_eq!(&m.cpu().registers()[..13], &[0; 13]); + assert_eq!(m.cpu().registers()[13], SYSTEM_STACK); + assert_eq!(m.cpu().registers()[14], target); +} + +fn assert_cleared(m: &Machine) { + for address in CLEAR_START..CLEAR_END { + assert_eq!(m.memory().read8(address).unwrap(), 0, "{address:#x}"); + } +} + +// Inline literal with a branch over its data. No host register mutation. +fn literal(code: &mut Vec, register: u32, value: u32) { + code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]); +} + +fn dirty_sp(mode: u32) -> u32 { + 0x0300_7000 + mode * 32 +} + +fn dirty_lr(mode: u32) -> u32 { + 0xa100_0000 | mode +} + +fn seeded_call(thumb: bool, status: u32, location: Option) -> Machine { + let mut code = Vec::new(); + for mode in [0x11, 0x12, 0x13, 0x17, 0x1b] { + code.push(0xe321_f0c0 | mode); // Switch bank with IRQ/FIQ masked. + literal(&mut code, 13, dirty_sp(mode)); + literal(&mut code, 14, dirty_lr(mode)); + literal(&mut code, 0, 0xb000_00df); + code.push(0xe169_f000); // MSR SPSR_fc,r0 + if mode == 0x11 { + for r in 8..=12 { + code.push(0xe3a0_00e0 | r << 12 | r); // Distinct FIQ high registers. + } + } + } + code.push(0xe321_f0df); + literal(&mut code, 13, 0x0300_7dc0); + for r in 0..=12 { + code.push(0xe3a0_0080 | r << 12 | r); + } + code.extend([0xe328_f20f, 0xe321_f000 | status]); // NZCV set; caller mode/masks. + let swi_pc = if let Some(address) = location { + literal(&mut code, 14, address | u32::from(thumb)); + code.push(0xe12f_ff1e); + address + } else { + code.extend([ + if thumb { 0xe28f_e001 } else { 0xe1a0_e00f }, + if thumb { 0xe12f_ff1e } else { 0xe1a0_0000 }, + ]); + let address = ROM_START + code.len() as u32 * 4; + code.extend([if thumb { 0xe7fe_df00 } else { 0xef00_0000 }, 0xeaff_fffe]); + address + }; + let mut m = bios::boot(words(&code)).unwrap(); + if let Some(address) = location { + m.memory_mut() + .write32(address, if thumb { 0xe7fe_df00 } else { 0xef00_0000 }) + .unwrap(); + m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap(); + } + reach(&mut m, swi_pc, 500); + m +} + +#[test] +fn every_flag_byte_selects_rom_or_ram_from_arm_and_thumb() { + for thumb in [false, true] { + for flag in 0..=255 { + let (mut m, _) = call(0, thumb, [0x123, 0x456, 0x789]); + // Only the byte at 7ffa selects the target, not surrounding bytes. + m.memory_mut().write32(IRQ_FLAGS, 0xffff_ffff).unwrap(); + restart(&mut m, flag); + assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE }); + assert_cleared(&m); + } + } +} + +#[test] +fn clears_exactly_top_512_bytes_and_keeps_other_memory() { + for thumb in [false, true] { + let (mut m, _) = call(0, thumb, [0, 0, 0]); + for address in (0x0300_0000..CLEAR_END).step_by(4) { + m.memory_mut() + .write32(address, address ^ 0xa55a_c33c) + .unwrap(); + } + let sentinels = [ + SOURCE, + SOURCE + 0x3fffc, + PALETTE_START, + PALETTE_START + 0x3fc, + VRAM_START, + VRAM_START + 0x17ffc, + OAM_START, + OAM_START + 0x3fc, + ]; + for address in sentinels { + m.memory_mut().write32(address, 0x1357_2468).unwrap(); + } + let rom_first = m.memory().read32(ROM_START).unwrap(); + restart(&mut m, 0); + assert_cleared(&m); + for address in (0x0300_0000..CLEAR_START).step_by(4) { + assert_eq!(m.memory().read32(address).unwrap(), address ^ 0xa55a_c33c); + } + for address in sentinels { + assert_eq!(m.memory().read32(address).unwrap(), 0x1357_2468); + } + assert_eq!(m.memory().read32(ROM_START).unwrap(), rom_first); + } +} + +#[test] +fn resets_nonzero_registers_and_status_for_user_system_callers() { + for thumb in [false, true] { + for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] { + for flag in [0, 1] { + let mut m = seeded_call(thumb, status, None); + assert!(m.cpu().registers()[..13].iter().all(|r| *r != 0)); + assert_eq!(m.cpu().cpsr() & 0xf000_0000, 0xf000_0000); + restart(&mut m, flag); + assert_restart_state(&m, if flag == 0 { ROM_START } else { SOURCE }); + assert_cleared(&m); + } + } + } +} + +#[test] +fn reinitializes_supervisor_irq_stacks_links_and_saved_status() { + for thumb in [false, true] { + let mut m = seeded_call(thumb, 0x1f, None); + for (i, mode) in [0xd3, 0xd2, 0xdf].into_iter().enumerate() { + m.memory_mut() + .write32(SOURCE + i as u32 * 4, 0xe321_f000 | mode) + .unwrap(); + } + restart(&mut m, 1); + for (mode, stack) in [ + (Mode::Supervisor, bios::SVC_STACK), + (Mode::Irq, bios::IRQ_STACK), + ] { + m.step().unwrap(); + assert_eq!(m.cpu().mode(), mode); + assert_eq!(m.cpu().registers()[13], stack); + assert_eq!(m.cpu().registers()[14], 0); + assert_eq!(m.cpu().spsr(), Some(0)); + } + m.step().unwrap(); + assert_eq!(m.cpu().mode(), Mode::System); + assert_eq!(m.cpu().registers()[13], SYSTEM_STACK); + assert_eq!(m.cpu().registers()[14], SOURCE); + assert_cleared(&m); + } +} + +#[test] +fn unrelated_banked_registers_and_saved_status_are_preserved() { + let mut m = seeded_call(false, 0x1f, None); + for (i, mode) in [0x11, 0x17, 0x1b].into_iter().enumerate() { + m.memory_mut() + .write32(SOURCE + i as u32 * 4, 0xe321_f0c0 | mode) + .unwrap(); + } + restart(&mut m, 1); + for mode in [Mode::Fiq, Mode::Abort, Mode::Undefined] { + m.step().unwrap(); + assert_eq!(m.cpu().mode(), mode); + assert_eq!(m.cpu().registers()[13], dirty_sp(mode as u32)); + assert_eq!(m.cpu().registers()[14], dirty_lr(mode as u32)); + assert_eq!(m.cpu().spsr(), Some(0xb000_00df)); + if mode == Mode::Fiq { + assert_eq!(&m.cpu().registers()[8..13], &[0xe8, 0xe9, 0xea, 0xeb, 0xec]); + } else { + assert_eq!(&m.cpu().registers()[8..13], &[0; 5]); + } + } +} + +#[test] +fn can_restart_from_work_ram_including_code_in_erased_area() { + for thumb in [false, true] { + for location in [SOURCE + 0x100, 0x0300_1000, CLEAR_START, 0x03ff_fe00] { + let mut m = seeded_call(thumb, 0x1f, Some(location)); + restart(&mut m, 1); + assert_restart_state(&m, SOURCE); + assert_cleared(&m); + if location < CLEAR_START { + assert_eq!( + m.memory().read32(location).unwrap(), + if thumb { 0xe7fe_df00 } else { 0xef00_0000 } + ); + } + } + } +} + +#[test] +fn ram_restart_executes_arm_code_and_can_call_bios_again() { + for thumb in [false, true] { + let mut m = seeded_call(thumb, 0x10, None); + // This program is only valid as ARM: Sqrt(81), then a self-branch. + for (i, instruction) in [0xe3a0_0051, 0xef08_0000, 0xeaff_fffe] + .into_iter() + .enumerate() + { + m.memory_mut() + .write32(SOURCE + i as u32 * 4, instruction) + .unwrap(); + } + restart(&mut m, 1); + reach(&mut m, SOURCE + 8, 1000); + assert_eq!(m.cpu().registers()[0], 9); + assert_eq!(m.cpu().registers()[13], SYSTEM_STACK); + assert_eq!(m.cpu().registers()[14], SOURCE); + assert_eq!(m.cpu().cpsr(), 0x9f); + } +} + +#[test] +fn does_not_reset_io_or_device_clocks() { + for thumb in [false, true] { + let (mut m, _) = call(0, thumb, [0, 0, 0]); + for (address, value) in [ + (DISPCNT, 0x0403), + (BG0CNT, 0x0141), + (WAITCNT, 0x4317), + (DISPSTAT, 0x7b38), + (IE, 0x1234), + (IME, 1), + ] { + m.memory_mut().write16(address, value).unwrap(); + } + m.memory_mut().write32(TIMER_BASE, 0x0080_0000).unwrap(); + let timer_before = m.memory().read16(TIMER_BASE).unwrap(); + let cycles = m.cycles(); + restart(&mut m, 0); + for (address, value) in [ + (DISPCNT, 0x0403), + (BG0CNT, 0x0141), + (WAITCNT, 0x4317), + (IE, 0x1234), + (IME, 1), + (POSTFLG, 1), + (TIMER_BASE + 2, 0x80), + ] { + assert_eq!(m.memory().read16(address).unwrap(), value, "{address:#x}"); + } + assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0x7b38); + let elapsed = m.cycles() - cycles; + assert!(elapsed > 512 && elapsed < 65536); + assert_eq!( + m.memory().read16(TIMER_BASE).unwrap(), + timer_before.wrapping_add(elapsed as u16) + ); + assert!(!m.halted()); + } +} + +#[test] +fn dma_timers_and_pending_irq_continue_but_irq_stays_masked_after_restart() { + for thumb in [false, true] { + for ime in [0, 1] { + let (mut m, _) = call(0, thumb, [0, 0, 0]); + m.memory_mut().write8(FLAG, 1).unwrap(); + m.memory_mut().write32(SOURCE, 0xeaff_fffe).unwrap(); + m.step().unwrap(); + // Queue IRQ and DMA only after SWI entry; otherwise IRQ can win first. + m.memory_mut().write16(IE, 8).unwrap(); + m.memory_mut().write16(IME, ime).unwrap(); + m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap(); + m.memory_mut().write32(DEST, 0x8765_4321).unwrap(); + m.memory_mut().write32(DMA_BASE, DEST).unwrap(); + m.memory_mut().write32(DMA_BASE + 4, DEST + 4).unwrap(); + m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap(); + let mut dma = 0; + for _ in 0..2000 { + if m.cpu().pc() == SOURCE { + break; + } + let step = m.step().unwrap(); + assert_ne!(step, StepKind::IrqEntry); + if matches!(step, StepKind::Dma { channel: 0 }) { + dma += 1; + } + } + assert_restart_state(&m, SOURCE); + assert_eq!(dma, 1); + assert_eq!(m.memory().read32(DEST + 4).unwrap(), 0x8765_4321); + assert_eq!(m.memory().read16(IF).unwrap() & 8, 8); + assert_eq!(m.memory().read16(IME).unwrap(), ime); + assert_cleared(&m); + assert_eq!(m.step().unwrap(), StepKind::Instruction); + assert_eq!(m.cpu().pc(), SOURCE); + } + } +} + +#[test] +fn reset_is_not_an_atomic_host_memory_clear() { + let (mut m, _) = call(0, false, [0, 0, 0]); + for address in (CLEAR_START..CLEAR_END).step_by(4) { + m.memory_mut().write32(address, 0xa55a_c33c).unwrap(); + } + m.memory_mut().write8(FLAG, 0).unwrap(); + m.step().unwrap(); + for _ in 0..1000 { + if m.memory().read32(CLEAR_START).unwrap() == 0 { + break; + } + m.step().unwrap(); + } + assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0); + assert_eq!(m.memory().read32(CLEAR_START + 4).unwrap(), 0xa55a_c33c); + assert_eq!(m.memory().read32(IRQ_HANDLER).unwrap(), 0xa55a_c33c); + assert!(m.cpu().pc() < BIOS_SIZE as u32); + reach(&mut m, ROM_START, 2000); + assert_cleared(&m); +} + +#[test] +fn exception_mode_callers_still_receive_a_diagnostic() { + for status in [0x12, 0x13] { + let (mut m, _) = call_status(0, false, [0, 0, 0], status); + m.memory_mut().write32(CLEAR_START, 0x1357_2468).unwrap(); + let error = (0..100).find_map(|_| m.step().err()).unwrap(); + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: UNSUPPORTED_TRAP, + .. + }) + )); + assert_eq!(m.memory().read32(CLEAR_START).unwrap(), 0x1357_2468); + } +}