diff --git a/src/core/cpu_code_cache.cpp b/src/core/cpu_code_cache.cpp index 929292ad1..1276d54d9 100644 --- a/src/core/cpu_code_cache.cpp +++ b/src/core/cpu_code_cache.cpp @@ -83,6 +83,7 @@ static void DeallocateLUTs(); static void ResetCodeLUT(); static void SetCodeLUT(u32 pc, const void* function); static void InvalidateBlock(Block* block, BlockState new_state); +static void ResetBlockCompileCount(Block* block); static void ClearBlocks(); static bool IsInCodeBuffer(const void* pc); @@ -722,6 +723,12 @@ void CPU::CodeCache::InvalidateBlock(Block* block, BlockState new_state) block->state = new_state; } +void CPU::CodeCache::ResetBlockCompileCount(Block* block) +{ + block->compile_frame = System::GetFrameNumber(); + block->compile_count = 1; +} + void CPU::CodeCache::InvalidateAllRAMBlocks() { // TODO: maybe combine the backlink into one big instruction flush cache? @@ -746,6 +753,35 @@ void CPU::CodeCache::InvalidateAllRAMBlocks() Bus::ClearRAMCodePageFlags(); } +void CPU::CodeCache::InvalidateOverlappingBlocks(u32 pc, bool force_recompile) +{ + const u32 table = pc >> LUT_TABLE_SHIFT; + Block** const blocks = s_block_lut[table]; + if (!blocks) + return; + + // optionally force recompilation + const BlockState new_block_state = force_recompile ? BlockState::NeedsRecompile : BlockState::Invalidated; + + // loop through all blocks in the page + for (u32 i = 0; i < LUT_TABLE_SIZE; i++) + { + Block* const block = blocks[i]; + if (!block) + continue; + + if (pc >= block->pc && pc < (block->pc + block->size)) + { + // this is pretty gross, if it's a RAM block we need to unlink it from the page list + RemoveBlockFromPageList(block); + + // don't get trolled into the interpreter if bps are toggled + InvalidateBlock(block, new_block_state); + ResetBlockCompileCount(block); + } + } +} + void CPU::CodeCache::ClearBlocks() { for (u32 i = 0; i < Bus::RAM_8MB_CODE_PAGE_COUNT; i++) @@ -961,6 +997,9 @@ bool CPU::CodeCache::ReadBlockInstructions(u32 start_pc, BlockInstructionList* i const bool use_icache = CPU::IsCachedAddress(start_pc); const bool dynamic_fetch_ticks = (!use_icache && Bus::GetMemoryAccessTimePtr(VirtualAddressToPhysical(start_pc), MemoryAccessSize::Word) != nullptr); + const bool debug_breakpoints_enabled = CPU::HasAnyBreakpoints(BreakpointType::Execute); + const bool cop0_breakpoints_enabled = CPU::g_state.cop0_regs.dcic.ExecutionBreakpointsEnabled(); + u32 pc = start_pc; bool is_branch_delay_slot = false; bool is_load_delay_slot = false; @@ -1028,6 +1067,8 @@ bool CPU::CodeCache::ReadBlockInstructions(u32 start_pc, BlockInstructionList* i info.is_load_instruction = IsMemoryLoadInstruction(instruction); info.is_store_instruction = IsMemoryStoreInstruction(instruction); info.has_load_delay = InstructionHasLoadDelay(instruction); + info.is_cop0_breakpoint = (cop0_breakpoints_enabled && CPU::Cop0BreakpointMatchesPC(pc)); + info.is_debug_breakpoint = (debug_breakpoints_enabled && CPU::HasBreakpointAtAddress(BreakpointType::Execute, pc)); if (use_icache) { @@ -1074,6 +1115,19 @@ bool CPU::CodeCache::ReadBlockInstructions(u32 start_pc, BlockInstructionList* i // instruction is decoded now instructions->emplace_back(instruction, info); + if (info.is_cop0_breakpoint) + { + // end block early at breakpoints, the instruction doesn't actually get executed + WARNING_LOG("Ending block 0x{:08X} at 0x{:08X} due to cop0 breakpoint", start_pc, pc); + break; + } + else if (info.is_debug_breakpoint && is_branch_delay_slot) + { + // can't handle debug breakpoints in branch delay slots currently, since it can't resume + ERROR_LOG("Can't handle debug breakpoint at 0x{:08X} in branch delay slot, skipping block", pc); + return false; + } + // if we're in a branch delay slot, the block is now done // except if this is a branch in a branch delay slot, then we grab the one after that, and so on... if (is_branch_delay_slot && !info.is_branch_instruction) @@ -1668,6 +1722,13 @@ const void* CPU::CodeCache::GetInterpretUncachedBlockFunction() } } +void CPU::CodeCache::ResetAndExitExecution() +{ + DEV_LOG("Resetting code cache and exiting execution"); + Reset(); + CPU::ExitExecution(); +} + void CPU::CodeCache::CompileASMFunctions() { JITWriteProtect(false); @@ -1840,13 +1901,11 @@ PageFaultHandler::HandlerResult CPU::CodeCache::HandleFastmemException(void* exc if (block) { // This is a bit annoying, we have to remove it from the page list if it's a RAM block. + // Need to reset the recompile count, otherwise it'll get trolled into an interpreter fallback. DEV_LOG("Queuing block {:08X} for recompilation due to backpatch", block->pc); RemoveBlockFromPageList(block); InvalidateBlock(block, BlockState::NeedsRecompile); - - // Need to reset the recompile count, otherwise it'll get trolled into an interpreter fallback. - block->compile_frame = System::GetFrameNumber(); - block->compile_count = 1; + ResetBlockCompileCount(block); } JITWriteProtect(true); diff --git a/src/core/cpu_code_cache.h b/src/core/cpu_code_cache.h index 5dc82b736..3f290da7d 100644 --- a/src/core/cpu_code_cache.h +++ b/src/core/cpu_code_cache.h @@ -37,4 +37,7 @@ void InvalidateBlocksWithPageIndex(u32 page_index); /// Invalidates all blocks in the cache. void InvalidateAllRAMBlocks(); +/// Invalidates blocks with the specified PC, forcing recompilation. +void InvalidateOverlappingBlocks(u32 pc, bool force_recompile); + } // namespace CPU::CodeCache diff --git a/src/core/cpu_code_cache_private.h b/src/core/cpu_code_cache_private.h index 312d64425..8212001aa 100644 --- a/src/core/cpu_code_cache_private.h +++ b/src/core/cpu_code_cache_private.h @@ -44,6 +44,8 @@ struct InstructionInfo bool is_load_instruction : 1; bool is_store_instruction : 1; bool is_load_delay_slot : 1; + bool is_cop0_breakpoint : 1; + bool is_debug_breakpoint : 1; bool is_last_instruction : 1; bool has_load_delay : 1; @@ -233,6 +235,7 @@ void CommitFarCode(u32 length); void AlignCode(u32 alignment); const void* GetInterpretUncachedBlockFunction(); +void ResetAndExitExecution(); void CompileOrRevalidateBlock(u32 start_pc); void DiscardAndRecompileBlock(u32 start_pc); diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp index 2e96db45c..1fb98951e 100644 --- a/src/core/cpu_core.cpp +++ b/src/core/cpu_core.cpp @@ -547,18 +547,26 @@ ALWAYS_INLINE_RELEASE bool CPU::Cop0ExecutionBreakpointCheck(u32 pc) if (!g_state.cop0_regs.dcic.ExecutionBreakpointsEnabled()) return false; - const u32 bpc = g_state.cop0_regs.BPC; - const u32 bpcm = g_state.cop0_regs.BPCM; + if (Cop0BreakpointMatchesPC(g_state.current_instruction_pc)) + { + DispatchCop0ExecutionBreakpoint(); + return true; + } - // Break condition is "((PC XOR BPC) AND BPCM)=0". - if (bpcm == 0 || ((pc ^ bpc) & bpcm) != 0u) - return false; + return false; +} + +bool CPU::AreCop0ExecutionBreakpointsActive() +{ + return (g_state.cop0_regs.dcic.ExecutionBreakpointsEnabled() && IsCop0ExecutionBreakpointUnmasked()); +} - DEV_LOG("Cop0 execution breakpoint at {:08X}", pc); +void CPU::DispatchCop0ExecutionBreakpoint() +{ + DEV_LOG("Cop0 execution breakpoint at {:08X}", g_state.current_instruction_pc); g_state.cop0_regs.dcic.status_any_break = true; g_state.cop0_regs.dcic.status_bpc_code_break = true; DispatchCop0Breakpoint(false); - return true; } template @@ -2142,15 +2150,19 @@ CPUExecutionMode CPU::GetCurrentExecutionMode() bool CPU::UpdateDebugDispatcherFlag() { - const bool has_any_breakpoints = (HasAnyBreakpoints() || s_locals.break_type == ExecutionBreakType::SingleStep); - const auto& dcic = g_state.cop0_regs.dcic; const bool has_cop0_breakpoints = dcic.super_master_enable_1 && dcic.super_master_enable_2 && dcic.execution_breakpoint_enable && IsCop0ExecutionBreakpointUnmasked(); + const bool has_execution_breakpoints = HasAnyBreakpoints(BreakpointType::Execute); + const bool requires_interpreter = + (HasAnyBreakpoints(BreakpointType::Read) || HasAnyBreakpoints(BreakpointType::Write) || + s_locals.break_type == ExecutionBreakType::SingleStep); + // TODO: Don't force the int just for cop0 breakpoints, they're cheap. Also need it for cop0 data breakpoints. const bool use_debug_dispatcher = - has_any_breakpoints || has_cop0_breakpoints || s_locals.trace_to_log || - (g_settings.cpu_execution_mode == CPUExecutionMode::Interpreter && g_settings.bios_tty_logging); + s_locals.trace_to_log || requires_interpreter || + (g_settings.cpu_execution_mode == CPUExecutionMode::Interpreter && + (has_cop0_breakpoints || has_execution_breakpoints || g_settings.bios_tty_logging)); if (use_debug_dispatcher == g_state.using_debug_dispatcher) return false; @@ -2235,10 +2247,9 @@ void CPU::CheckForExecutionModeChange() fastjmp_jmp(&s_locals.exit_jmp_buf, 1); } -bool CPU::HasAnyBreakpoints() +bool CPU::HasAnyBreakpoints(BreakpointType type) { - return (GetBreakpointList(BreakpointType::Execute).size() + GetBreakpointList(BreakpointType::Read).size() + - GetBreakpointList(BreakpointType::Write).size()) > 0; + return (GetBreakpointList(type).size() > 0); } ALWAYS_INLINE CPU::BreakpointList& CPU::GetBreakpointList(BreakpointType type) @@ -2283,10 +2294,7 @@ bool CPU::HasBreakpointAtAddress(BreakpointType type, VirtualMemoryAddress addre for (Breakpoint& bp : GetBreakpointList(type)) { if (bp.enabled && (bp.address & 0x0FFFFFFFu) == (address & 0x0FFFFFFFu)) - { - bp.hit_count++; return true; - } } return false; @@ -2328,12 +2336,16 @@ bool CPU::AddBreakpoint(BreakpointType type, VirtualMemoryAddress address, bool Breakpoint bp{address, nullptr, auto_clear ? 0 : s_locals.breakpoint_counter++, 0, type, auto_clear, enabled}; GetBreakpointList(type).push_back(std::move(bp)); - if (UpdateDebugDispatcherFlag()) - System::InterruptExecution(); if (!auto_clear) Host::ReportDebuggerEvent(DebuggerEvent::Message, fmt::format("Added breakpoint at 0x{:08X}.", address)); + if (type == BreakpointType::Execute && s_locals.current_execution_mode != CPUExecutionMode::Interpreter && enabled) + CodeCache::InvalidateOverlappingBlocks(address, true); + + if (UpdateDebugDispatcherFlag()) + System::InterruptExecution(); + return true; } @@ -2346,8 +2358,13 @@ bool CPU::AddBreakpointWithCallback(BreakpointType type, VirtualMemoryAddress ad Breakpoint bp{address, callback, 0, 0, type, false, true}; GetBreakpointList(type).push_back(std::move(bp)); + + if (type == BreakpointType::Execute && s_locals.current_execution_mode != CPUExecutionMode::Interpreter) + CodeCache::InvalidateOverlappingBlocks(address, true); + if (UpdateDebugDispatcherFlag()) System::InterruptExecution(); + return true; } @@ -2370,6 +2387,12 @@ bool CPU::SetBreakpointEnabled(BreakpointType type, VirtualMemoryAddress address if (address == s_locals.last_breakpoint_check_pc && !enabled) s_locals.last_breakpoint_check_pc = INVALID_BREAKPOINT_PC; + if (type == BreakpointType::Execute && s_locals.current_execution_mode != CPUExecutionMode::Interpreter) + CodeCache::InvalidateOverlappingBlocks(address, true); + + if (UpdateDebugDispatcherFlag()) + System::InterruptExecution(); + return true; } @@ -2385,17 +2408,30 @@ bool CPU::RemoveBreakpoint(BreakpointType type, VirtualMemoryAddress address) fmt::format("Removed {} breakpoint at 0x{:08X}.", GetBreakpointTypeName(type), address)); bplist.erase(it); - if (UpdateDebugDispatcherFlag()) - System::InterruptExecution(); if (address == s_locals.last_breakpoint_check_pc) s_locals.last_breakpoint_check_pc = INVALID_BREAKPOINT_PC; + if (type == BreakpointType::Execute && s_locals.current_execution_mode != CPUExecutionMode::Interpreter) + CodeCache::InvalidateOverlappingBlocks(address, true); + + if (UpdateDebugDispatcherFlag()) + System::InterruptExecution(); + return true; } void CPU::ClearBreakpoints(bool include_auto_clear /*= false*/, bool include_callbacks /*= false*/) { + if (s_locals.current_execution_mode != CPUExecutionMode::Interpreter) + { + for (const Breakpoint& bp : s_locals.breakpoints[static_cast(BreakpointType::Execute)]) + { + if (bp.enabled) + CodeCache::InvalidateOverlappingBlocks(bp.address, true); + } + } + if (include_auto_clear && include_callbacks) { for (BreakpointList& bplist : s_locals.breakpoints) @@ -2590,6 +2626,21 @@ ALWAYS_INLINE_RELEASE void CPU::ExecutionBreakpointCheck(u32 pc) } } +u32 CPU::DispatchDebugBreakpoint() +{ + const u32 prev_npc = g_state.npc; + DEV_LOG("Debug execution breakpoint at {:08X}", g_state.current_instruction_pc); + + if (CheckBreakpointList(BreakpointType::Execute, g_state.current_instruction_pc)) + { + s_locals.break_type = ExecutionBreakType::None; + ExitExecution(); + } + + // return true if pc has changed and the rec needs to bail out + return BoolToUInt32(g_state.npc != prev_npc); +} + template ALWAYS_INLINE_RELEASE void CPU::MemoryBreakpointCheck(VirtualMemoryAddress address) { @@ -2791,6 +2842,8 @@ void CPU::CodeCache::InterpretUncachedBlock() if (!FetchInstructionForInterpreterFallback()) return; + // TODO: needs to check debug breakpoints, for when the breakpoint is in a branch delay slot + // At this point, pc contains the last address executed (in the previous block). The instruction has not been fetched // yet. pc shouldn't be updated until the fetch occurs, that way the exception occurs in the delay slot. bool in_branch_delay_slot = false; diff --git a/src/core/cpu_core.h b/src/core/cpu_core.h index 3bc3af20a..c4c21ef94 100644 --- a/src/core/cpu_core.h +++ b/src/core/cpu_core.h @@ -249,7 +249,7 @@ using BreakpointList = std::vector; const char* GetBreakpointTypeName(BreakpointType type); const char* GetBreakpointTypeDisplayName(BreakpointType type); std::optional ParseBreakpointTypeName(std::string_view sv); -bool HasAnyBreakpoints(); +bool HasAnyBreakpoints(BreakpointType type); bool HasBreakpointAtAddress(BreakpointType type, VirtualMemoryAddress address); BreakpointList CopyBreakpointList(bool include_auto_clear = false, bool include_callbacks = false); bool AddBreakpoint(BreakpointType type, VirtualMemoryAddress address, bool auto_clear = false, bool enabled = true); diff --git a/src/core/cpu_core_private.h b/src/core/cpu_core_private.h index 19613e9f2..c4f56ecba 100644 --- a/src/core/cpu_core_private.h +++ b/src/core/cpu_core_private.h @@ -173,6 +173,24 @@ ALWAYS_INLINE constexpr TickCount GetDivTicks() return (36 - 1); } +// cop0 breakpoint check +ALWAYS_INLINE static bool Cop0BreakpointMatchesPC(u32 pc) +{ + const u32 bpc = g_state.cop0_regs.BPC; + const u32 bpcm = g_state.cop0_regs.BPCM; + + // Break condition is "((PC XOR BPC) AND BPCM)=0". + if (bpcm == 0 || ((pc ^ bpc) & bpcm) != 0u) + return false; + else + return true; +} + +// cop0 breakpoint dispatch +bool AreCop0ExecutionBreakpointsActive(); +void DispatchCop0ExecutionBreakpoint(); +u32 DispatchDebugBreakpoint(); + // kernel call interception void HandleA0Syscall(); void HandleB0Syscall(); diff --git a/src/core/cpu_recompiler.cpp b/src/core/cpu_recompiler.cpp index fb9da9856..2871fdc93 100644 --- a/src/core/cpu_recompiler.cpp +++ b/src/core/cpu_recompiler.cpp @@ -350,6 +350,7 @@ bool CPU::Recompiler::Recompiler::TrySwapDelaySlot(Reg rs, Reg rt, Reg rd) return false; const Instruction* next_instruction = inst + 1; + const CodeCache::InstructionInfo* next_iinfo = iinfo + 1; DebugAssert(next_instruction < (m_block->Instructions() + m_block->size)); const Reg opcode_rs = next_instruction->r.rs; @@ -366,6 +367,10 @@ bool CPU::Recompiler::Recompiler::TrySwapDelaySlot(Reg rs, Reg rt, Reg rd) const u32 backup_instruction_pc = m_current_instruction_pc; const bool backup_instruction_delay_slot = m_current_instruction_branch_delay_slot; + // branch delay slots with a breakpoint can't be swapped + if (next_iinfo->is_cop0_breakpoint || next_iinfo->is_debug_breakpoint) + goto is_unsafe; + if (next_instruction->bits == 0) { // nop @@ -1188,6 +1193,13 @@ void CPU::Recompiler::Recompiler::CompileInstruction() m_current_instruction_pc, str); #endif + if (iinfo->is_cop0_breakpoint || iinfo->is_debug_breakpoint) + { + CompileExecutionBreakpointCheck(); + if (m_block_ended) + return; + } + m_cycles++; if (IsNopInstruction(*inst)) diff --git a/src/core/cpu_recompiler.h b/src/core/cpu_recompiler.h index b832baaf5..c2630e46d 100644 --- a/src/core/cpu_recompiler.h +++ b/src/core/cpu_recompiler.h @@ -132,6 +132,8 @@ protected: (FLUSH_CYCLES | FLUSH_GTE_DONE_CYCLE), // GTE cycles needed because it stalls when a GTE instruction is next. FLUSH_FOR_EARLY_BLOCK_EXIT = (FLUSH_FLUSH_MIPS_REGISTERS | FLUSH_CYCLES | FLUSH_GTE_DONE_CYCLE | FLUSH_PC | FLUSH_LOAD_DELAY), + FLUSH_FOR_BREAKPOINT = + (FLUSH_FLUSH_MIPS_REGISTERS | FLUSH_CYCLES | FLUSH_GTE_DONE_CYCLE | FLUSH_INSTRUCTION_BITS | FLUSH_FOR_C_CALL), FLUSH_FOR_INTERPRETER = (FLUSH_FLUSH_MIPS_REGISTERS | FLUSH_INVALIDATE_MIPS_REGISTERS | FLUSH_FREE_CALLER_SAVED_REGISTERS | FLUSH_PC | FLUSH_CYCLES | FLUSH_INSTRUCTION_BITS | FLUSH_LOAD_DELAY | FLUSH_GTE_DONE_CYCLE | FLUSH_INVALIDATE_SPECULATIVE_CONSTANTS), @@ -351,6 +353,9 @@ protected: virtual void Compile_Fallback() = 0; + // returns true if further compilation should be skipped + virtual bool CompileExecutionBreakpointCheck() = 0; + void Compile_j(); virtual void Compile_jr(CompileFlags cf) = 0; void Compile_jr_const(CompileFlags cf); diff --git a/src/core/cpu_recompiler_x64.cpp b/src/core/cpu_recompiler_x64.cpp index 8b9f267d2..23ef29139 100644 --- a/src/core/cpu_recompiler_x64.cpp +++ b/src/core/cpu_recompiler_x64.cpp @@ -580,6 +580,12 @@ void CPU::X64Recompiler::GenerateCall(const void* func, s32 arg1reg /*= -1*/, s3 void CPU::X64Recompiler::EndBlock(const std::optional& newpc, bool do_event_test) { + if (iinfo->is_cop0_breakpoint && m_block_ended) + { + // block already ended by breakpoint + return; + } + if (newpc.has_value()) { if (m_dirty_pc || m_compiler_pc != newpc) @@ -1003,6 +1009,39 @@ void CPU::X64Recompiler::Compile_Fallback() m_load_delay_dirty = EMULATE_LOAD_DELAYS; } +bool CPU::X64Recompiler::CompileExecutionBreakpointCheck() +{ + // flush regs and instruction bits, since cop0 needs it for the delay slot/cop bits + Flush(FLUSH_FOR_BREAKPOINT); + + // cop0 has to come first, because debug can exit execution + if (iinfo->is_cop0_breakpoint) + { + // cop0 always exits regardless of what the debug bp does + cg->call(&CPU::DispatchCop0ExecutionBreakpoint); + if (iinfo->is_debug_breakpoint) + cg->call(&CPU::DispatchDebugBreakpoint); + EndBlock(std::nullopt, true); + return true; + } + + // only a debug bp? + if (iinfo->is_debug_breakpoint) + { + // debug may or may not exit execution, or change pc + cg->call(&CPU::DispatchDebugBreakpoint); + cg->test(cg->eax, cg->eax); + SwitchToFarCode(true, &CodeGenerator::jnz); + BackupHostState(); + EndBlock(std::nullopt, true); + RestoreHostState(); + SwitchToNearCode(false); + } + + // we can continue + return true; +} + void CPU::X64Recompiler::CheckBranchTarget(const Xbyak::Reg32& pcreg) { if (!g_settings.cpu_recompiler_memory_exceptions) @@ -2299,16 +2338,26 @@ void CPU::X64Recompiler::Compile_mtc0(CompileFlags cf) cg->mov(RWARG1, cg->dword[PTR(&g_state.cop0_regs.sr.bits)]); TestInterrupts(RWARG1); } - else if (reg == Cop0Reg::DCIC || reg == Cop0Reg::BPCM) + else if (reg == Cop0Reg::DCIC || reg == Cop0Reg::BPC || reg == Cop0Reg::BPCM) { // need to check whether we're switching to debug mode Flush(FLUSH_FOR_C_CALL); - cg->call(&CPU::UpdateDebugDispatcherFlag); + + // dcic can enable data breakpoints + if (reg == Cop0Reg::DCIC) + { + cg->call(&CPU::UpdateDebugDispatcherFlag); + cg->test(cg->al, cg->al); + SwitchToFarCode(true, &Xbyak::CodeGenerator::jnz); + SwitchToNearCode(false); + } + + cg->call(&CPU::AreCop0ExecutionBreakpointsActive); cg->test(cg->al, cg->al); SwitchToFarCode(true, &Xbyak::CodeGenerator::jnz); BackupHostState(); Flush(FLUSH_FOR_EARLY_BLOCK_EXIT); - cg->call(&CPU::ExitExecution); // does not return + cg->call(&CodeCache::ResetAndExitExecution); // does not return RestoreHostState(); SwitchToNearCode(false); } diff --git a/src/core/cpu_recompiler_x64.h b/src/core/cpu_recompiler_x64.h index 76192f26e..0c9ec0000 100644 --- a/src/core/cpu_recompiler_x64.h +++ b/src/core/cpu_recompiler_x64.h @@ -51,6 +51,8 @@ protected: void Compile_Fallback() override; + bool CompileExecutionBreakpointCheck() override; + void CheckBranchTarget(const Xbyak::Reg32& pcreg); void Compile_jr(CompileFlags cf) override; void Compile_jalr(CompileFlags cf) override;