diff --git a/src/cpu/instructions/control.rs b/src/cpu/instructions/control.rs index a78d1af..8f7d8dd 100644 --- a/src/cpu/instructions/control.rs +++ b/src/cpu/instructions/control.rs @@ -1,6 +1,17 @@ +use crate::cpu::register; + #[allow(clippy::module_name_repetitions)] #[derive(Debug, Clone, Copy)] pub enum ControlFlowInstruction { - JumpToImmediate { addr: u16 }, - Call { addr: u16 }, + JumpToImmediate { + addr: u16, + }, + Call { + addr: u16, + }, + CallIfFlagMatches { + flag: register::Flag, + value: u8, + addr: u16, + }, } diff --git a/src/cpu/parse/control.rs b/src/cpu/parse/control.rs index 3de2927..e236f7d 100644 --- a/src/cpu/parse/control.rs +++ b/src/cpu/parse/control.rs @@ -1,7 +1,7 @@ use crate::{ cpu::{ instructions::{control::ControlFlowInstruction, Instruction}, - parse, + parse, register, }, memory::{GetViewTuple, View}, }; @@ -22,6 +22,38 @@ impl OpcodeParser for Parser { data, |addr| ControlFlowInstruction::Call { addr }, )), + 0xC4 => Ok(build_immediate_parameterized_control_flow_data( + data, + |addr| ControlFlowInstruction::CallIfFlagMatches { + flag: register::Flag::Zero, + value: 0, + addr, + }, + )), + 0xD4 => Ok(build_immediate_parameterized_control_flow_data( + data, + |addr| ControlFlowInstruction::CallIfFlagMatches { + flag: register::Flag::Carry, + value: 0, + addr, + }, + )), + 0xCC => Ok(build_immediate_parameterized_control_flow_data( + data, + |addr| ControlFlowInstruction::CallIfFlagMatches { + flag: register::Flag::Zero, + value: 1, + addr, + }, + )), + 0xDC => Ok(build_immediate_parameterized_control_flow_data( + data, + |addr| ControlFlowInstruction::CallIfFlagMatches { + flag: register::Flag::Carry, + value: 1, + addr, + }, + )), _ => Err(parse::Error::UnknownOpcode(opcode)), } } diff --git a/src/cpu/run/control.rs b/src/cpu/run/control.rs index 9fb2690..fc91c49 100644 --- a/src/cpu/run/control.rs +++ b/src/cpu/run/control.rs @@ -13,21 +13,32 @@ impl Run for ControlFlowInstruction { Ok(Cycles(16)) } Self::Call { addr } => { - let current_pc = processor - .registers - .get_single_16bit_register(register::SingleSixteenBit::ProgramCounter); - - // We know the call instruction to be 3 bytes. A bit of a hack, but eh. - processor.push_16bit_value_to_stack(current_pc + 3)?; - dbg!(current_pc); - dbg!(current_pc + 3); - - processor - .registers - .set_single_16bit_register(register::SingleSixteenBit::ProgramCounter, addr); - + do_call(processor, addr)?; Ok(Cycles(24)) } + Self::CallIfFlagMatches { flag, value, addr } => { + if processor.registers.get_flag_bit(flag) == value { + do_call(processor, addr)?; + Ok(Cycles(24)) + } else { + Ok(Cycles(12)) + } + } } } } + +fn do_call(processor: &mut Processor, to_addr: u16) -> Result<(), super::Error> { + let current_pc = processor + .registers + .get_single_16bit_register(register::SingleSixteenBit::ProgramCounter); + + // We know the call instruction to be 3 bytes. A bit of a hack, but eh. + processor.push_16bit_value_to_stack(current_pc + 3)?; + + processor + .registers + .set_single_16bit_register(register::SingleSixteenBit::ProgramCounter, to_addr); + + Ok(()) +} diff --git a/tests/cpu/control.rs b/tests/cpu/control.rs index e22ba09..1ca8f17 100644 --- a/tests/cpu/control.rs +++ b/tests/cpu/control.rs @@ -1,4 +1,8 @@ -use ferris_boi::cpu::{instructions::Instruction, Processor}; +use ferris_boi::{ + cpu::{instructions::Instruction, Processor}, + register::Flag, +}; +use test_case::test_case; #[test] fn test_can_jump_to_immediate() { @@ -47,3 +51,64 @@ fn test_call_pushes_pc_onto_stack() { assert_eq!(0xF2, processor.memory.get((old_sp - 2).into()).unwrap()); assert_eq!(old_sp - 2, processor.registers.stack_pointer); } + +#[test_case(0xCC, Flag::Zero, 1; "CALL Z")] +#[test_case(0xDC, Flag::Carry, 1; "CALL C")] +#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")] +#[test_case(0xD4, Flag::Carry, 0; "CALL NC")] +fn test_call_jumps_if_condition_matches(opcode: u8, flag: Flag, expected_value: u8) { + let mut processor = Processor::default(); + processor.registers.set_flag_bit(flag, expected_value); + + let data = [opcode, 0x37, 0x13, 0x06]; + let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction"); + + assert_eq!(extra_data, &[0x06]); + + processor.run_instruction(ins); + + assert_eq!(0x1337, processor.registers.program_counter); +} + +#[test_case(0xCC, Flag::Zero, 1; "CALL Z")] +#[test_case(0xDC, Flag::Carry, 1; "CALL C")] +#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")] +#[test_case(0xD4, Flag::Carry, 0; "CALL NC")] +fn test_call_grows_stack_if_condition_matches(opcode: u8, flag: Flag, expected_value: u8) { + let mut processor = Processor::default(); + processor.registers.set_flag_bit(flag, expected_value); + + let data = [opcode, 0x37, 0x13, 0x06]; + let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction"); + + assert_eq!(extra_data, &[0x06]); + + let old_sp = processor.registers.stack_pointer; + processor.run_instruction(ins); + + assert_eq!(old_sp - 2, processor.registers.stack_pointer); +} + +#[test_case(0xCC, Flag::Zero, 1; "CALL Z")] +#[test_case(0xDC, Flag::Carry, 1; "CALL C")] +#[test_case(0xC4, Flag::Zero, 0; "CALL NZ")] +#[test_case(0xD4, Flag::Carry, 0; "CALL NC")] +fn test_call_does_nothing_if_condition_fails(opcode: u8, flag: Flag, expected_value: u8) { + let mut processor = Processor::default(); + processor + .registers + .set_flag_bit(flag, if expected_value == 0 { 1 } else { 0 }); + + let data = [opcode, 0x37, 0x13, 0x06]; + let (ins, extra_data) = Instruction::from_data(&data).expect("could not parse instruction"); + + assert_eq!(extra_data, &[0x06]); + + let old_sp = processor.registers.stack_pointer; + let old_pc = processor.registers.program_counter; + processor.run_instruction(ins); + + assert_eq!(old_sp, processor.registers.stack_pointer); + // run_instruction does not advance the program counter, so this will be the same. Normally it would be old_pc plus 3 + assert_eq!(old_pc, processor.registers.program_counter); +} diff --git a/tests/cpu/jsmoo/testdata/disabled/c4.json b/tests/cpu/jsmoo/testdata/c4.json similarity index 100% rename from tests/cpu/jsmoo/testdata/disabled/c4.json rename to tests/cpu/jsmoo/testdata/c4.json diff --git a/tests/cpu/jsmoo/testdata/disabled/cc.json b/tests/cpu/jsmoo/testdata/cc.json similarity index 100% rename from tests/cpu/jsmoo/testdata/disabled/cc.json rename to tests/cpu/jsmoo/testdata/cc.json diff --git a/tests/cpu/jsmoo/testdata/disabled/d4.json b/tests/cpu/jsmoo/testdata/d4.json similarity index 100% rename from tests/cpu/jsmoo/testdata/disabled/d4.json rename to tests/cpu/jsmoo/testdata/d4.json diff --git a/tests/cpu/jsmoo/testdata/disabled/dc.json b/tests/cpu/jsmoo/testdata/dc.json similarity index 100% rename from tests/cpu/jsmoo/testdata/disabled/dc.json rename to tests/cpu/jsmoo/testdata/dc.json