From b535c0831306e91cd97973baf9dff9ade91b003b Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 14 Oct 2026 08:38:10 +0700 Subject: [PATCH] fix(cpu): accept ARM multiplies whose destination equals Rm MUL/MLA with Rd=Rm and long multiplies with RdHi or RdLo equal to Rm returned UnsupportedInstruction and stopped the machine. The ARM7TDMI reads every operand before writing the result, so these forms execute normally. R15 operands and RdHi=RdLo remain diagnostics. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HhfHfHEuzv2AUTd6erW1gU --- crates/gba-core/src/cpu/arm.rs | 5 +++-- crates/gba-core/src/cpu/instruction_tests.rs | 18 +++++++++++++++++- docs/hardware/cpu.md | 3 +++ 3 files changed, 23 insertions(+), 3 deletions(-) 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.