diff --git a/README.md b/README.md index b301606..1364c53 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 705 tests pass in debug and release builds. +- All 720 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, SoftReset, 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, selective RegisterRamReset, 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`. @@ -508,6 +508,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 | +| `0x01` | RegisterRamReset | r0 selects RAM and supported I/O resets; force blank; reject serial/sound flags | | `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 | @@ -535,7 +536,7 @@ Only User/System callers are supported. Calls from exception modes return a diag 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. +Supervisor stack use is 28 bytes normally, including ArcTan and RegisterRamReset. Division and ArcTan2 use 40 bytes. CpuFastSet, affine-matrix services, BitUnPack, all three decompression formats, and differential filters use 60 bytes. They do not reproduce the original firmware's internal stack layout or cycle counts. @@ -597,10 +598,64 @@ The clearing loop performs ordinary word stores; stopping execution partway leav 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. +Use RegisterRamReset or separate initialization code for those regions. Calls from exception modes retain the existing unsupported-service diagnostic. Exact firmware timing, bus-protection behavior, and undocumented side effects remain incomplete. +#### RegisterRamReset + +`RegisterRamReset` (`SWI 0x01`) selects RAM and supported I/O resets using the low byte of r0. +Bits 8–31 are ignored. The service preserves all caller registers and status. +Supported flags can be combined: + +| Bit | Action | +| --- | --- | +| 0 | Clear external RAM: `0x02000000..0x0203ffff` | +| 1 | Clear lower internal RAM: `0x03000000..0x03007dff`; exclude the top 512 bytes | +| 2 | Clear palette RAM: `0x05000000..0x050003ff` | +| 3 | Clear all 96 KiB of video RAM: `0x06000000..0x06017fff` | +| 4 | Clear sprite attribute memory (OAM): `0x07000000..0x070003ff` | +| 5 | Serial reset: unsupported | +| 6 | Sound reset: unsupported | +| 7 | Reset supported display, DMA, timer, and interrupt registers as described below | + +**Unsupported flags:** any request containing bit 5 or 6 reaches `bios::INVALID_ARGUMENT_TRAP`. +Validation occurs before forced blank or RAM/I/O reset writes, but after the common SWI stack save. +This includes the common all-devices request `r0=0xff`; it is not silently treated as a successful full reset. +Serial and audio devices remain unimplemented. The firmware's unconditional serial-data side effect is not reproduced. + +Every supported request first writes `DISPCNT=0x0080`, including requests with no selected flags. +This forces a white screen and clears other display-control bits. +RAM clears then run in bit order, using ordinary word stores. +Zeroed OAM contains regular sprites at the origin, not disabled sprites; software must configure them before enabling objects. + +Bit 7 runs after the selected RAM clears: + +- Disable IME. +- Disable all four DMA channels, then zero their source, destination, count, and control registers. +- Stop all four timers, then zero their reload and control registers. +- Zero display registers at `0x04000004..0x04000057`, subject to normal read-only register rules. +- Set BG2/BG3 affine PA and PD to 256; PB, PC, and programmed origins remain zero. +- Clear IE and WAITCNT, then acknowledge IF with a halfword write of `0xffff`. + +Timer counter reads retain the stopped count until a later enable loads the cleared reload value. +This is normal timer-register behavior, not a host-side replacement of device state. +Read-only display status and VCOUNT continue to reflect the advancing display clock. +GREENSWAP, KEYINPUT/button state, POSTFLG, and BIOS IRQ communication words are not reset. +Keypad interrupt control is not implemented. Bit 7 covers only the listed supported registers. + +Without bit 7, device configuration remains unchanged except for DISPCNT. +IRQ delivery stays CPU-masked during the call. The service restores the caller's mask on return. +Timers and DMA continue until their reset writes, if selected; this service does not stop clocks or replace frame-capture history. +Pending IRQs remain pending without bit 7 and can be delivered after return. + +The service uses only the common 28-byte Supervisor frame. +Default BIOS stacks lie in the excluded top 512 bytes, but the entry save still writes its usual frame there. +Do not put the Supervisor stack or required return code/data in selected RAM. +The service can erase a RAM caller's instructions and still return to that now-cleared address. +Stop conflicting DMA transfers before a clear, even when bit 7 is selected: RAM clears occur before device-register resets. +Stopping emulation midway leaves completed stores committed. Exact firmware write ordering and cycle counts are not reproduced. + #### Integer arithmetic Div truncates the signed quotient toward zero. The remainder has the numerator's sign, or is zero. @@ -849,7 +904,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. -RegisterRamReset, HardReset, and STOP remain unimplemented. +RegisterRamReset serial/sound flags, HardReset, and STOP remain unimplemented. This subset is not sufficient for Pokémon Emerald compatibility. ### Timers, interrupt registers, and the machine clock @@ -1600,7 +1655,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/reset.rs` | Emitted ARM SoftReset and selective RegisterRamReset; CPU banks, RAM clearing, and supported device-register resets | | `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 | @@ -1661,6 +1716,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/ram_reset.rs` | Selective RAM boundaries, flag combinations, I/O reset, display rendering, DMA/timer latches, and diagnostics | | `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 | @@ -1737,12 +1793,12 @@ To test the core without building the window dependency: direnv exec . cargo test --locked --no-default-features ``` -This runs 694 core and integration tests; the eleven desktop and command-line tests are excluded. +This runs 709 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 705 tests. +The default suite has 720 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. @@ -1817,6 +1873,11 @@ 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. +RegisterRamReset tests check full memory regions and all memory-flag combinations from ARM and Thumb. +They verify forced blank, reserved flag bits, caller preservation, protected BIOS RAM, and serial/sound diagnostics. +Rendering tests check write-only display state, both identity matrices, and the effect of zeroed sprite attributes. +Device tests check DMA address/count latches, stopped timer counters, cleared reloads, and IRQ acknowledgement. +Other checks cover partial clears, preserved input/device state, and callers whose own RAM code is erased. 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. @@ -1882,7 +1943,8 @@ 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. Add RegisterRamReset with explicit limits for unimplemented sound/serial registers; add STOP and remaining DMA device modes. +3. Add keypad interrupt control and remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. + Add STOP and remaining DMA device modes with verified timing and wake-up behavior. 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. @@ -1893,7 +1955,7 @@ 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 reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm), for SoftReset state and restart selection, plus RegisterRamReset flags, RAM bounds, and forced blank. - [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). @@ -1901,7 +1963,7 @@ Hardware references used for the core: - [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. +- [mGBA BIOS service implementation](https://github.com/mgba-emu/mgba/blob/master/src/gba/bios.c), reviewed for RegisterRamReset register defaults, angle-polynomial coefficients and quadrant conventions, BitUnPack ordering/offsets, and Huffman tree layout and packing. - [HALTCNT hardware-access tests](https://github.com/mgba-emu/mgba/issues/2309), for BIOS-only writes and halfword access. - [GBATEK display status and IRQs](https://problemkaputt.de/gbatek-lcd-i-o-interrupts-and-status.htm). - [GBATEK display dimensions and timings](https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm). diff --git a/src/bios.rs b/src/bios.rs index 9863f5e..2e6940c 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,5 +1,5 @@ //! Original, optional ARM BIOS replacement. No Nintendo firmware is included. -//! Supports SoftReset, interrupt waits, integer/fixed-point arithmetic, affine matrices, +//! Supports SoftReset, selective RegisterRamReset, interrupt waits, 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. @@ -30,7 +30,7 @@ pub const IRQ_FLAGS: u32 = 0x0300_7ff8; pub const IRQ_HANDLER: u32 = 0x0300_7ffc; /// An intentional undefined instruction used for unsupported SWIs/vectors and invalid IRQ pointers. pub const UNSUPPORTED_TRAP: u32 = 0xe7f0_00f0; -/// A diagnostic trap for invalid arithmetic or asset-conversion arguments. +/// A diagnostic trap for invalid service arguments, including unsupported reset flags. pub const INVALID_ARGUMENT_TRAP: u32 = 0xe7f0_00f1; /// Start at reset with our optional firmware. Call Machine::step to execute boot. @@ -51,6 +51,8 @@ pub fn boot(rom: Vec) -> Result { /// 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. +/// RegisterRamReset clears selected RAM and supported I/O registers. Serial/sound +/// reset flags reach a diagnostic before display or reset writes. /// 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 @@ -122,6 +124,7 @@ pub fn image() -> Vec { a.emit(0xe20c_c0ff); // AND r12,r12,#0xff for (number, target) in [ (0, "soft_reset"), + (1, "register_ram_reset"), (2, "halt"), (4, "intr_wait"), (5, "vblank_wait"), @@ -238,6 +241,8 @@ pub fn image() -> Vec { // pool here so reset/dispatcher loads stay within ARM's 4 KiB reach. a.flush_literals(); reset::emit(&mut a); + // Both reset routines end without fallthrough; keep their literals local. + a.flush_literals(); arithmetic::emit(&mut a); affine::emit(&mut a); angles::emit(&mut a); diff --git a/src/bios/reset.rs b/src/bios/reset.rs index 4022fd8..fba3428 100644 --- a/src/bios/reset.rs +++ b/src/bios/reset.rs @@ -1,6 +1,11 @@ -//! Original ARM SoftReset. This is a restart service, not a hardware reset. +//! Original ARM SoftReset and selective RegisterRamReset services. +//! Serial/sound reset requests are diagnostics until those devices are modeled. use super::{ArmImage, IRQ_STACK, ROM_START, SVC_STACK, SYSTEM_STACK}; +use crate::{ + dma::DMA_BASE, + io::{DISPCNT, IE, IME, TIMER_BASE}, +}; pub(super) fn emit(a: &mut ArmImage) { a.label("soft_reset"); @@ -35,4 +40,80 @@ pub(super) fn emit(a: &mut ArmImage) { // 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) + + emit_ram_reset(a); +} + +fn emit_ram_reset(a: &mut ArmImage) { + a.label("register_ram_reset"); + // Validate unsupported devices before forced blank or any reset writes. + // Higher flag bits are ignored, as in the documented low-byte flag format. + a.emit(0xe310_0060); // TST r0,#serial|sound + a.branch(1, "invalid_argument"); + a.literal(1, DISPCNT); + a.emit(0xe3a0_2080); // MOV r2,#forced blank (also clear other DISPCNT bits) + a.emit(0xe1c1_20b0); // STRH r2,[r1] + a.emit(0xe3a0_3000); // MOV r3,#0 (shared fill value) + for (flag, start, bytes, next) in [ + (1, 0x0200_0000, 0x40000, "ram_reset_iwram"), + (2, 0x0300_0000, 0x7e00, "ram_reset_palette"), + (4, 0x0500_0000, 0x400, "ram_reset_vram"), + (8, 0x0600_0000, 0x18000, "ram_reset_oam"), + (16, 0x0700_0000, 0x400, "ram_reset_io"), + ] { + a.emit(0xe310_0000 | flag); // TST r0,#flag + a.branch(0, next); + clear_words(a, start, bytes); + a.label(next); + } + a.emit(0xe310_0080); // TST r0,#other registers + a.branch(0, "return"); + + // IRQ remains CPU-masked throughout. Disable DMA/timers before clearing + // their address/count/reload registers. These are bus writes, not host resets. + a.literal(1, IME); + store_halfword(a, 0); // IME = 0 + a.literal(1, DMA_BASE); + for offset in [10, 22, 34, 46] { + store_halfword(a, offset); // Disable each channel before clearing its registers. + } + clear_words(a, DMA_BASE, 48); + a.literal(1, TIMER_BASE); + for offset in [2, 6, 10, 14] { + store_halfword(a, offset); // Stop each timer; the stopped counter remains latched. + } + clear_words(a, TIMER_BASE, 16); // Clear reload/control, not hidden counter/clock state. + clear_words(a, DISPCNT + 4, 0x54); // Display controls, offsets, origins, windows/effects. + a.literal(1, DISPCNT); + a.emit(0xe3a0_3c01); // MOV r3,#256 (identity affine PA/PD for BG2 and BG3) + for offset in [0x20, 0x26, 0x30, 0x36] { + store_halfword(a, offset); + } + a.emit(0xe3a0_3000); // MOV r3,#0 + a.literal(1, IE); + store_halfword(a, 0); // IE = 0 + store_halfword(a, 4); // WAITCNT = 0 + a.literal(3, 0xffff); + store_halfword(a, 2); // Acknowledge IF after stopping timer/display IRQ sources. + a.branch(14, "return"); + + // Leaf routine; only r1/r2 change. The common 28-byte SWI frame saves lr. + a.label("ram_reset_words"); + a.emit(0xe481_3004); // STR r3,[r1],#4 + a.emit(0xe252_2004); // SUBS r2,r2,#4 + a.branch(1, "ram_reset_words"); + a.emit(0xe12f_ff1e); // BX lr +} + +fn clear_words(a: &mut ArmImage, address: u32, bytes: u32) { + debug_assert!(bytes != 0 && bytes % 4 == 0); + a.literal(1, address); + a.literal(2, bytes); + a.call("ram_reset_words"); +} + +// Store r3 through r1 with an ARM immediate halfword offset. +fn store_halfword(a: &mut ArmImage, offset: u32) { + debug_assert!(offset <= 255 && offset % 2 == 0); + a.emit(0xe1c1_30b0 | (offset & 0xf0) << 4 | (offset & 15)); } diff --git a/tests/bios.rs b/tests/bios.rs index 7753d85..d6a1c04 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/ram_reset.rs"] +mod ram_reset; #[path = "bios/reset.rs"] mod reset; #[path = "bios/run_length.rs"] @@ -229,7 +231,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 [1, 3, 0x0d, 0x19, 0xff] { + for service in [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/ram_reset.rs b/tests/bios/ram_reset.rs new file mode 100644 index 0000000..59fc1f5 --- /dev/null +++ b/tests/bios/ram_reset.rs @@ -0,0 +1,492 @@ +use super::*; +use gba_rust::{ + bios::INVALID_ARGUMENT_TRAP, + dma::{DmaError, DMA_BASE, DMA_STRIDE}, + input::Buttons, + io::{ + BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYINPUT, + WAITCNT, WININ, WINOUT, + }, + memory::{OAM_START, PALETTE_START}, + video::Framebuffer, +}; + +const REGIONS: [(u32, u32, u32); 5] = [ + (1, 0x0200_0000, 0x40000), + (2, 0x0300_0000, 0x7e00), + (4, PALETTE_START, 0x400), + (8, VRAM_START, 0x18000), + (16, OAM_START, 0x400), +]; +const MARKER: u32 = 0xa55a_c33c; + +fn literal(code: &mut Vec, register: u32, value: u32) { + code.extend([0xe59f_0000 | register << 12, 0xea00_0000, value]); +} + +fn prepare(flags: u32, thumb: bool, status: u32) -> (Machine, u32) { + prepare_at(flags, thumb, status, None) +} + +fn prepare_at(flags: u32, thumb: bool, status: u32, location: Option) -> (Machine, u32) { + let mut code = Vec::new(); + literal(&mut code, 0, flags); + for r in 1..=12 { + code.push(0xe3a0_0080 | r << 12 | r); + } + code.extend([0xe328_f20f, 0xe321_f000 | status]); + 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_df01 } else { 0xef01_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_df01 } else { 0xef01_0000 }) + .unwrap(); + m.memory_mut().write32(address + 4, 0xeaff_fffe).unwrap(); + } + reach(&mut m, swi_pc, 200); + (m, swi_pc + if thumb { 2 } else { 4 }) +} + +fn complete(m: &mut Machine, pc: u32) { + let before = m.cpu().clone(); + m.step().unwrap(); + reach(m, pc, 400_000); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80); +} + +fn samples(start: u32, bytes: u32) -> [u32; 4] { + [start, start + 4, start + bytes / 2, start + bytes - 4] +} + +fn seed_samples(m: &mut Machine) { + for (_, start, bytes) in REGIONS { + for address in samples(start, bytes) { + m.memory_mut().write32(address, MARKER).unwrap(); + } + } +} + +fn check_samples(m: &Machine, flags: u32) { + for (bit, start, bytes) in REGIONS { + for address in samples(start, bytes) { + assert_eq!( + m.memory().read32(address).unwrap(), + if flags & bit == 0 { MARKER } else { 0 }, + "flags={flags:#x}, address={address:#x}" + ); + } + } +} + +#[test] +fn each_memory_flag_clears_its_entire_region_and_no_other_region() { + for thumb in [false, true] { + for (flag, _, _) in REGIONS { + let (mut m, pc) = prepare(flag, thumb, 0x1f); + for (_, start, bytes) in REGIONS { + for address in (start..start + bytes).step_by(4) { + m.memory_mut().write32(address, MARKER ^ address).unwrap(); + } + } + complete(&mut m, pc); + for (bit, start, bytes) in REGIONS { + for address in (start..start + bytes).step_by(4) { + assert_eq!( + m.memory().read32(address).unwrap(), + if bit == flag { 0 } else { MARKER ^ address }, + "flag={flag}, address={address:#x}" + ); + } + } + } + } +} + +#[test] +fn all_memory_flag_combinations_are_independent_from_arm_and_thumb() { + for thumb in [false, true] { + for flags in 0..32 { + let (mut m, pc) = prepare(flags, thumb, 0x1f); + seed_samples(&mut m); + complete(&mut m, pc); + check_samples(&m, flags); + } + } +} + +#[test] +fn zero_flags_still_force_blank_and_clear_other_display_control_bits() { + for thumb in [false, true] { + let (mut m, pc) = prepare(0, thumb, 0x1f); + seed_samples(&mut m); + m.memory_mut().write16(DISPCNT, 0x1403).unwrap(); + m.memory_mut().write16(BG0CNT, 0x1234).unwrap(); + complete(&mut m, pc); + check_samples(&m, 0); + assert_eq!(m.memory().read16(BG0CNT).unwrap(), 0x1204); + let mut frame = Framebuffer::default(); + m.memory().render_frame(&mut frame).unwrap(); + assert!(frame.pixels().iter().all(|p| *p == 0xffffff)); + } +} + +#[test] +fn reserved_high_bits_are_ignored_and_original_argument_is_restored() { + for flags in [ + 0x8000_0000, + 0xffff_ff00, + 0x1234_5604, + 0xffff_ff80, + 0xffff_ff9f, + ] { + for thumb in [false, true] { + let (mut m, pc) = prepare(flags, thumb, 0x1f); + seed_samples(&mut m); + complete(&mut m, pc); + check_samples(&m, flags); + } + } +} + +#[test] +fn internal_ram_clear_preserves_bios_work_area_except_common_swi_frame() { + for thumb in [false, true] { + let (mut m, pc) = prepare(0x9f, thumb, 0x1f); + for address in (0x0300_7e00..0x0300_8000).step_by(4) { + m.memory_mut().write32(address, MARKER ^ address).unwrap(); + } + complete(&mut m, pc); + for address in (0x0300_7e00..0x0300_8000).step_by(4) { + if (bios::SVC_STACK - 28..bios::SVC_STACK).contains(&address) { + continue; + } + assert_eq!( + m.memory().read32(address).unwrap(), + MARKER ^ address, + "{address:#x}" + ); + } + assert_eq!(m.memory().read32(0x0300_7dfc).unwrap(), 0); + } +} + +#[test] +fn all_serial_sound_requests_fail_before_forced_blank_or_memory_clear() { + for thumb in [false, true] { + for flags in (0..256).filter(|flags| flags & 0x60 != 0) { + let (mut m, _) = prepare(flags, thumb, 0x1f); + seed_samples(&mut m); + m.memory_mut().write16(DISPCNT, 0x0403).unwrap(); + for _ in 0..1000 { + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + if let Err(error) = m.step() { + assert!(matches!( + error, + MachineError::Cpu(CpuError::UnsupportedInstruction { + instruction: INVALID_ARGUMENT_TRAP, + .. + }) + )); + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.step(), Err(error)); + break; + } + } + assert!(m.step().is_err()); + assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x0403); + check_samples(&m, 0); + } + } +} + +#[test] +fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() { + for thumb in [false, true] { + for status in [0x10, 0x50, 0x90, 0xd0, 0x1f, 0x5f, 0x9f, 0xdf] { + for flags in [0, 4, 0x80] { + let (mut m, pc) = prepare(flags, thumb, status); + assert!(m.cpu().registers()[1..13].iter().all(|r| *r != 0)); + complete(&mut m, pc); + } + } + } +} + +#[test] +fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() { + for flags in [0x80, 0x9f] { + let (mut m, pc) = prepare(flags, false, 0x9f); + m.memory_mut().set_buttons(Buttons::from_bits(0x251)); + let keys = m.memory().read16(KEYINPUT).unwrap(); + m.memory_mut().write16(GREENSWAP, 1).unwrap(); + for address in [ + BG0CNT, + BG0CNT + 2, + BG0CNT + 4, + BG0CNT + 6, + WININ, + WINOUT, + BLDCNT, + BLDALPHA, + WAITCNT, + IE, + ] { + m.memory_mut().write16(address, 0xffff).unwrap(); + } + m.memory_mut().write16(DISPSTAT, 0x7b38).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + // Overflow queues IF, independently of CPU execution. + m.memory_mut().write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + m.memory_mut().advance_cycles(1); + assert_ne!(m.memory().read16(IF).unwrap(), 0); + let cycles = m.cycles(); + complete(&mut m, pc); + for address in [ + BG0CNT, + BG0CNT + 2, + BG0CNT + 4, + BG0CNT + 6, + WININ, + WINOUT, + BLDCNT, + BLDALPHA, + WAITCNT, + IE, + IF, + IME, + ] { + assert_eq!(m.memory().read16(address).unwrap(), 0, "{address:#x}"); + } + assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0); + assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys); + assert_eq!(m.memory().read16(POSTFLG).unwrap(), 1); + assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1); + assert!(m.cycles() > cycles); + assert!(!m.halted()); + } +} + +#[test] +fn io_reset_clears_write_only_display_state_and_restores_both_identity_matrices() { + let (mut m, pc) = prepare(0x80, true, 0x1f); + let mut reference = Memory::new(vec![]).unwrap(); + for (_, start, bytes) in [REGIONS[2], REGIONS[3]] { + for offset in (0..bytes).step_by(2) { + let value = ((offset * 37 + 1) ^ (offset >> 4)) as u16; + m.memory_mut().write16(start + offset, value).unwrap(); + reference.write16(start + offset, value).unwrap(); + } + } + for address in (BG0CNT..0x0400_0058).step_by(2) { + m.memory_mut().write16(address, 0x5353).unwrap(); + } + complete(&mut m, pc); + for address in [BG2PA, BG2PD, BG3PA, BG3PD] { + reference.write16(address, 256).unwrap(); + } + for control in [ + 0x0100, 0x0200, 0x0400, 0x0800, 0x0402, 0x0802, 0x0403, 0x2403, 0x4403, 0x6403, + ] { + for bus in [m.memory_mut(), &mut reference] { + bus.write16(DISPCNT, control).unwrap(); + // Enable mosaic in each background without altering its offset/matrix. + for address in (BG0CNT..=BG0CNT + 6).step_by(2) { + bus.write16(address, 0x40).unwrap(); + } + } + let mut actual = Framebuffer::default(); + let mut expected = Framebuffer::default(); + m.memory().render_frame(&mut actual).unwrap(); + reference.render_frame(&mut expected).unwrap(); + assert_eq!(actual.pixels(), expected.pixels(), "DISPCNT={control:#x}"); + } +} + +#[test] +fn timer_reset_stops_counters_clears_reload_and_does_not_replace_counter_state() { + let (mut m, pc) = prepare(0x80, false, 0x1f); + for timer in 0..4 { + m.memory_mut() + .write32(TIMER_BASE + timer * 4, 0x0080_4321) + .unwrap(); + } + complete(&mut m, pc); + for timer in 0..4 { + let address = TIMER_BASE + timer * 4; + assert_eq!(m.memory().read16(address + 2).unwrap(), 0); + let counter = m.memory().read16(address).unwrap(); + assert!(counter >= 0x4321); + m.memory_mut().advance_cycles(1000); + assert_eq!(m.memory().read16(address).unwrap(), counter); + m.memory_mut().write16(address + 2, 0x80).unwrap(); + assert_eq!(m.memory().read16(address).unwrap(), 0); // Reload latch was cleared. + m.memory_mut().write16(address + 2, 0).unwrap(); + } +} + +#[test] +fn dma_reset_disables_channels_and_clears_write_only_address_and_count_latches() { + for channel in 0..4 { + let (mut m, pc) = prepare(0x80, false, 0x1f); + let base = DMA_BASE + channel as u32 * DMA_STRIDE; + m.memory_mut().write32(base, SOURCE).unwrap(); + m.memory_mut().write32(base + 4, DEST).unwrap(); + m.memory_mut().write32(base + 8, 0x9200_0001).unwrap(); // Repeating VBlank, count one. + complete(&mut m, pc); + assert_eq!(m.memory().read16(base + 10).unwrap(), 0); + // With only control re-enabled, the source must now be zero. + m.memory_mut().write16(base + 10, 0x8000).unwrap(); + assert_eq!( + m.step(), + Err(MachineError::Dma(DmaError::UnsupportedSource { + channel, + address: 0 + })) + ); + m.memory_mut().write16(base + 10, 0).unwrap(); + m.memory_mut().write32(base, SOURCE).unwrap(); + m.memory_mut().write16(base + 10, 0x8000).unwrap(); + assert_eq!( + m.step(), + Err(MachineError::Dma(DmaError::Memory { + channel, + error: MemoryError::ReadOnly(0) + })) + ); + m.memory_mut().write16(base + 10, 0).unwrap(); + m.memory_mut().write32(base + 4, DEST).unwrap(); + m.memory_mut().write32(SOURCE, MARKER).unwrap(); + m.memory_mut().write16(base + 10, 0x8500).unwrap(); // Fixed source, words; count remains zero. + assert_eq!(m.step().unwrap(), StepKind::Dma { channel }); + assert_eq!(m.step().unwrap(), StepKind::Dma { channel }); + assert_eq!(m.memory().read32(DEST).unwrap(), MARKER); + assert_eq!(m.memory().read32(DEST + 4).unwrap(), MARKER); + // Count zero expands to the channel maximum, not the stale count of one. + assert_ne!(m.memory().read16(base + 10).unwrap() & 0x8000, 0); + m.memory_mut().write16(base + 10, 0).unwrap(); + } +} + +#[test] +fn memory_only_reset_keeps_io_and_allows_dma_timers_and_deferred_irq() { + for thumb in [false, true] { + let (mut m, pc) = prepare(4, thumb, 0x1f); + let before = m.cpu().clone(); + m.step().unwrap(); + m.memory_mut().write16(BG0CNT, 0x41).unwrap(); + m.memory_mut().write16(WAITCNT, 0x4317).unwrap(); + m.memory_mut().write16(IE, 8).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + m.memory_mut().write32(TIMER_BASE, 0x00c0_fff0).unwrap(); + m.memory_mut().write32(SOURCE, MARKER).unwrap(); + m.memory_mut().write32(DMA_BASE, SOURCE).unwrap(); + m.memory_mut().write32(DMA_BASE + 4, DEST).unwrap(); + m.memory_mut().write32(DMA_BASE + 8, 0x8400_0001).unwrap(); + let mut dma = 0; + for _ in 0..4000 { + if m.cpu().pc() == pc { + break; + } + let step = m.step().unwrap(); + assert_ne!(step, StepKind::IrqEntry); + if matches!(step, StepKind::Dma { channel: 0 }) { + dma += 1; + } + } + assert_eq!(m.cpu().pc(), pc); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + assert_eq!(dma, 1); + assert_eq!(m.memory().read32(DEST).unwrap(), MARKER); + for (address, value) in [ + (BG0CNT, 0x41), + (WAITCNT, 0x4317), + (IE, 8), + (IME, 1), + (TIMER_BASE + 2, 0xc0), + ] { + assert_eq!(m.memory().read16(address).unwrap(), value); + } + assert_ne!(m.memory().read16(IF).unwrap() & 8, 0); + assert_eq!(m.step().unwrap(), StepKind::IrqEntry); + } +} + +#[test] +fn clear_is_visible_one_word_at_a_time_and_uses_normal_video_bus_stores() { + for flag in [1, 2, 4, 8, 16] { + let (mut m, pc) = prepare(flag, false, 0x1f); + let (_, start, _) = REGIONS.into_iter().find(|r| r.0 == flag).unwrap(); + m.memory_mut().write32(start, MARKER).unwrap(); + m.memory_mut().write32(start + 4, MARKER).unwrap(); + m.step().unwrap(); + for _ in 0..1000 { + if m.memory().read32(start).unwrap() == 0 { + break; + } + m.step().unwrap(); + } + assert_eq!(m.memory().read32(start).unwrap(), 0); + assert_eq!(m.memory().read32(start + 4).unwrap(), MARKER); + assert_eq!(m.memory().read16(DISPCNT).unwrap(), 0x80); + assert!(m.cpu().pc() < BIOS_SIZE as u32); + reach(&mut m, pc, 400_000); + assert_eq!(m.memory().read32(start + 4).unwrap(), 0); + } +} + +#[test] +fn ram_callers_can_return_but_selected_ram_code_is_really_erased() { + for thumb in [false, true] { + for (location, clear_flag) in [(SOURCE + 0x100, 1), (0x0300_1000, 2)] { + for flags in [4, clear_flag] { + let (mut m, pc) = prepare_at(flags, thumb, 0x1f, Some(location)); + complete(&mut m, pc); + assert_eq!( + m.memory().read32(location).unwrap(), + if flags == clear_flag { + 0 + } else if thumb { + 0xe7fe_df01 + } else { + 0xef01_0000 + } + ); + } + } + } +} + +#[test] +fn cleared_oam_is_zero_not_the_disabled_sprite_encoding() { + let (mut m, pc) = prepare(16, false, 0x1f); + m.memory_mut().write16(OAM_START, 0x200).unwrap(); + m.memory_mut().write16(PALETTE_START + 0x202, 31).unwrap(); + m.memory_mut() + .write16(VRAM_START + 0x10000, 0x1111) + .unwrap(); + complete(&mut m, pc); + assert_eq!(m.memory().read16(OAM_START).unwrap(), 0); + m.memory_mut().write16(DISPCNT, 0x1000).unwrap(); + let mut frame = Framebuffer::default(); + m.memory().render_frame(&mut frame).unwrap(); + assert_eq!(frame.pixels()[0], 0xff0000); +}