diff --git a/crates/gba-core/src/cpu/arm.rs b/crates/gba-core/src/cpu/arm.rs index 8a46cb6..c816f29 100644 --- a/crates/gba-core/src/cpu/arm.rs +++ b/crates/gba-core/src/cpu/arm.rs @@ -256,9 +256,10 @@ impl Cpu { let rs = ((instruction >> 8) & 15) as usize; let rm = (instruction & 15) as usize; let accumulate = instruction & (1 << 21) != 0; - if [rd, rn, rs, rm].contains(&15) || rd == rm || (!accumulate && rn != 0) { + if [rd, rn, rs, rm].contains(&15) || (!accumulate && rn != 0) { return Err(self.unsupported(instruction)); } + // Rd may equal Rm: every operand is read before the destination is written. let mut result = self.registers[rm].wrapping_mul(self.registers[rs]); if accumulate { result = result.wrapping_add(self.registers[rn]); @@ -278,7 +279,7 @@ impl Cpu { let lo = ((instruction >> 12) & 15) as usize; let rs = ((instruction >> 8) & 15) as usize; let rm = (instruction & 15) as usize; - if [hi, lo, rs, rm].contains(&15) || hi == lo || hi == rm || lo == rm { + if [hi, lo, rs, rm].contains(&15) || hi == lo { return Err(self.unsupported(instruction)); } let mut result = if instruction & (1 << 22) != 0 { diff --git a/crates/gba-core/src/cpu/instruction_tests.rs b/crates/gba-core/src/cpu/instruction_tests.rs index c44575b..20a1ab2 100644 --- a/crates/gba-core/src/cpu/instruction_tests.rs +++ b/crates/gba-core/src/cpu/instruction_tests.rs @@ -487,6 +487,23 @@ fn long_multiply_supports_signed_unsigned_and_accumulation() { } } +#[test] +fn multiplies_read_every_operand_before_writing_an_aliased_destination() { + for (instruction, expected) in [ + (0xe000_0190, [u32::MAX - 1, 2, 9, 5]), // MUL r0,r0,r1 + (0xe020_3190, [3, 2, 9, 5]), // MLA r0,r0,r1,r3 + (0xe083_0190, [u32::MAX - 1, 2, 9, 1]), // UMULL r0,r3,r0,r1 (RdLo=Rm) + (0xe080_2190, [1, 2, u32::MAX - 1, 5]), // UMULL r2,r0,r0,r1 (RdHi=Rm) + (0xe0a3_0190, [u32::MAX - 2, 2, 9, 7]), // UMLAL r0,r3,r0,r1 (+5:0xffffffff) + ] { + let mut memory = program(&[instruction]); + let mut cpu = Cpu::new(ROM_START); + cpu.registers[..4].copy_from_slice(&[u32::MAX, 2, 9, 5]); + cpu.step(&mut memory).unwrap(); + assert_eq!(cpu.registers[..4], expected, "{instruction:08x}"); + } +} + #[test] fn long_multiply_zero_flag_uses_both_halves() { let mut memory = program(&[0xe093_2190]); @@ -510,7 +527,6 @@ fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() { 0xe790_1012, // illegal register-specified memory shift 0xe790_100f, // memory offset register PC 0xe1c0_10d0, // ARMv5 doubleword load, not an ARM7 signed store - 0xe000_0190, // MUL Rd=Rm 0xe082_2190, // UMULL RdHi=RdLo 0xe8f0_0001, // LDM user-bank transfer with writeback 0xe100_f090, // SWP with PC destination diff --git a/docs/hardware/cpu.md b/docs/hardware/cpu.md index ac6b1bc..9cbcbca 100644 --- a/docs/hardware/cpu.md +++ b/docs/hardware/cpu.md @@ -299,6 +299,9 @@ Other S-bit block transfers access User registers while using the current mode's User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics. These restrictions avoid unpredictable or unverified forms. +ARM multiplies accept a destination equal to the first operand: `MUL`/`MLA` with Rd=Rm and long multiplies with RdHi or RdLo equal to Rm. +Every operand is read before the result is written. R15 operands and RdHi=RdLo remain diagnostics. + `Cpu::enter_exception` explicitly supports software interrupt, undefined instruction, prefetch abort, data abort, IRQ, and FIQ entry. For synchronous exceptions, call it with PC at the faulting instruction. For IRQ/FIQ, call it between instructions, with PC at the next instruction.