Files
ppsspp/Core/Core.h
T
Henrik RydgårdandClaude Opus 5 bb5d7d5b65 Debugger: report structured breakpoint hits, including log-only ones
A breakpoint hit reached a WebSocket client as two fields on cpu.stepping: a
reason string and one address. Everything else the hit site knew was formatted
into a log line and dropped.

What was missing per kind:

- exec: hit count, condition, symbol.
- memory: the address actually accessed, read vs write, size, and who did it.
  The address that reached the client was the *start of the watched range*, so a
  client watching 4KB learned only that something in it was touched.
- register: which register. Entirely - the event carried pc and nothing else.

There's now a BreakpointHit captured where the hit happens and carried through
Core_Break() on the stepping reason, rendered as a "hit" object on cpu.stepping.
It's absent rather than empty when the break wasn't a breakpoint (a pause, a
savestate load, an exception), so presence is the test. relatedAddress keeps
reporting the range start for compatibility; hit.address is the accurate one.
The formatter is shared with the new event below, so the two can't drift.

And a new cpu.breakpoint.hit broadcast fires on *every* hit whose condition
passes, whether or not it stops the CPU. That's the part that makes log-only
breakpoints usable for automation: until now their only trace was a line in the
log stream, so a client couldn't count hits, or react to one, without scraping
text. Same "hit" object, plus a sequence number.

Volume needed handling, since a log-only breakpoint in a hot loop produces
events far faster than a connection drains them - measured 13719 hits in three
seconds of one homebrew's draw function. The per-connection queue is capped and
drops rather than growing without bound, and the sequence number is what makes
that honest: a gap tells a client exactly how many it missed. Clients that don't
want the traffic at all can disallow the new "breakpoint" broadcast category.
Building the hit record is skipped entirely when no debugger is connected, which
is one relaxed atomic load on that path.

Verified against a running game, all three kinds. The memory case shows why the
address/range split matters - accessed address 200540160 against a watched range
starting at 200941120, with source "ThreadFillStack" identifying the HLE call
responsible.

libretro gets stubs: it builds Core.cpp and Breakpoints.cpp but not
Core/Debugger/WebSocket.cpp.

pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-18 10:59:08 +02:00

325 lines
11 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include <cstdint>
#include <functional>
#include <mutex>
#include <string>
#include <string_view>
#include "Common/CommonTypes.h"
#include "Core/ConfigValues.h"
class GraphicsContext;
// For platforms that don't call Run
void Core_SetGraphicsContext(GraphicsContext *ctx);
// Returns false when an UI exit state is detected.
void Core_Stop();
// X11, sigh.
#ifdef None
#undef None
#endif
enum class CPUStepType {
None,
Into,
Over,
Out,
Frame,
};
// Must be set when breaking.
enum class BreakReason {
None,
AssertChoice,
DebugBreak,
DebugStep,
DebugStepInto,
UIFocus,
AfterFrame,
MemoryException,
CpuException,
BreakInstruction,
SavestateLoad,
SavestateSave,
SavestateRewind,
SavestateCrash,
MemoryBreakpoint,
CpuBreakpoint,
RegBreakpoint,
MemoryAccess, // ???
JitBranchDebug,
BreakOnBoot,
RABreak,
AddBreakpoint,
FrameAdvance,
UIPause,
HLEDebugBreak,
RunUntilTime,
};
const char *BreakReasonToString(BreakReason reason);
enum class BreakpointKind {
None,
Exec,
Memory,
Register,
};
// What tripped a breakpoint, captured where it happened.
//
// All three kinds know a lot more than the single address Core_Break() carries - which register,
// which byte of a watched range, read or write, how big - and until this existed it was formatted
// straight into a log line and discarded, so a debugger over the wire couldn't see any of it. For
// a memcheck in particular the address that reached the client was the start of the watched range,
// not the address actually touched.
struct BreakpointHit {
BreakpointKind kind = BreakpointKind::None;
u32 pc = 0; // The instruction responsible.
u32 address = 0; // Exec: the instruction itself. Memory: the address actually accessed.
int size = 0; // Memory only, in bytes.
bool write = false; // Memory only.
int reg = -1; // Register only: a GPR index.
// Which breakpoint this was, so a client can match it against cpu.breakpoint.list and friends.
// For a memcheck that's the watched range, which is exactly what 'address' is not.
u32 rangeStart = 0;
u32 rangeEnd = 0;
u32 numHits = 0;
bool logged = false; // Had the LOG action.
bool paused = false; // Had the PAUSE action, so the CPU stopped for it.
std::string condition; // Empty when unconditional.
// Memory only: who performed the access - "interpret", "CPU", "HLE", or an allocation tag.
// Copied rather than kept as a pointer; callers pass buffers that are gone by the time the
// event gets formatted.
std::string source;
};
// Async, called from gui
// hit is optional detail for the breakpoint kinds, forwarded to the debugger. Only stored when
// the break actually takes effect, so a rejected Core_Break() can't leave a stale one behind.
void Core_Break(BreakReason reason, u32 relatedAddress = 0, const BreakpointHit *hit = nullptr);
// Resumes execution. Works both when stepping the CPU and the GE.
void Core_Resume();
BreakReason Core_BreakReason();
// This should be called externally.
// Can fail if another step type was requested this frame.
// stepSize is always in instructions (4 bytes each), never bytes - see Core_PerformCPUStep in Core.cpp.
bool Core_RequestCPUStep(CPUStepType stepType);
bool Core_NextFrame();
void Core_SwitchToGe(); // Switches from CPU emulation to GE display list execution.
// Changes every time we enter stepping.
int Core_GetSteppingCounter();
struct SteppingReason {
BreakReason reason;
u32 relatedAddress = 0;
// Only filled in when the break came from a breakpoint - kind is None otherwise.
BreakpointHit hit;
};
SteppingReason Core_GetSteppingReason();
enum class CoreLifecycle {
STARTING,
// Note: includes failure cases. Guaranteed call after STARTING.
START_COMPLETE,
STOPPING,
// Guaranteed call after STOPPING.
STOPPED,
// Sometimes called for save states. Guaranteed sequence, and never during STARTING or STOPPING.
MEMORY_REINITING,
MEMORY_REINITED,
};
// RUNNING must be at 0, NEXTFRAME must be at 1.
enum CoreState {
// Emulation is running normally.
CORE_RUNNING_CPU = 0,
// Emulation was running normally, just reached the end of a frame.
CORE_NEXTFRAME,
// Set this when running to bounce out from the dispatcher and just go back in again. Useful for things like cache clears.
CORE_REENTER_DISPATCH,
// Emulation is paused, CPU thread is sleeping.
CORE_STEPPING_CPU, // Can be used for recoverable runtime errors (ignored memory exceptions)
// Core is not running.
CORE_POWERDOWN,
// Unrecoverable runtime error. Recoverable errors should use CORE_STEPPING.
CORE_RUNTIME_ERROR,
// Stepping the GPU. When done, will switch over to STEPPING_CPU.
CORE_STEPPING_GE,
// Running the GPU. When done, will switch over to RUNNING_CPU.
CORE_RUNNING_GE,
};
const char *CoreStateToString(CoreState state);
// Callback is called on the Emu thread.
typedef void (* CoreLifecycleFunc)(CoreLifecycle stage);
void Core_ListenLifecycle(CoreLifecycleFunc func);
void Core_NotifyLifecycle(CoreLifecycle stage);
bool Core_IsStepping();
bool Core_IsActive();
bool Core_IsInactive();
// Warning: these three are only used on Windows - debugger integration.
void Core_StateProcessed();
void Core_WaitInactive();
void Core_SetPowerSaving(bool mode);
bool Core_GetPowerSaving();
void Core_RunLoopUntil(u64 globalticks);
void Core_ReenterDispatcher(); // If you've done things that mess with caches, call this so we can run deferred operations.
// Runs a function on the CPU thread - the thread that calls Core_RunLoopUntil (and thus, indirectly,
// NativeFrame). Useful for code running on unrelated threads (like the WebSocket debugger) that needs to
// safely touch state that's otherwise only ever touched from that thread (breakpoints, stepping, etc.),
// instead of poking at it directly from wherever the call happens to come from.
//
// Safe to call from any thread, including the CPU thread itself (in which case func just runs immediately).
// Blocks the calling thread until func has actually run, so don't call this from the CPU thread with
// something that would itself try to wait on the CPU thread - that'll deadlock.
//
// Drained at the top of every Core_RunLoopUntil() iteration, so it's reached continuously (in a tight
// spin) while the CPU is stepping/paused, and at least once per call (i.e. about once per host frame)
// even while it's fully running.
void Core_RunOnCPUThread(std::function<void()> func);
// Held while the core is being torn down (CPU_Shutdown) or its memory map reinitialized. Take it on
// any thread other than the CPU thread before reading core state - emulated memory, the symbol map,
// kernel objects - so none of it can be freed mid-read. Recursive, so nesting is fine.
//
// It is not a lock on memory *access*: it doesn't stop the CPU thread mutating anything, only stop
// it going away. If you also need a stable snapshot, take g_frameMutex first - see the ordering
// rule in AGENTS.md.
class CoreShutdownLock {
public:
CoreShutdownLock();
~CoreShutdownLock();
};
CoreShutdownLock Core_LockAgainstShutdown();
// Drains the queue Core_RunOnCPUThread() feeds. Normally called from the top of every
// Core_RunLoopUntil() iteration, but that function is only reached while a game is actually
// loaded/running (via EmuScreen) - so NativeFrame() (UI/NativeApp.cpp) also calls this directly,
// just before it calls into the screen manager's render(), so queued work doesn't hang forever
// waiting for a CPU loop that isn't running (e.g. from the main menu with no game loaded).
// Called from the CPU thread only - which is whatever thread NativeFrame() itself runs on.
void Core_ProcessCPUQueue();
// Guards CPU-thread-owned debugger state (breakpoints, symbol map, registers, memory, etc.)
// against concurrent unsynchronized reads from other threads' paint handlers.
//
// Held by NativeFrame() for the span where it actually touches that state: running the CPU
// (Core_RunLoopUntil(), including draining Core_RunOnCPUThread()'s queue), processing breakpoints,
// and running the ImGui debugger. Not held for the rest of NativeFrame (input handling, present/
// vsync waits, frame pacing, etc).
//
// A paint handler on another thread (e.g. a legacy Win32 debugger window) that wants to read that
// state directly - without the overhead/latency of routing through Core_RunOnCPUThread(), which
// would be too heavy for something called on every WM_PAINT - should hold this lock for the
// duration of the read instead. Since WM_PAINT only fires reactively rather than every frame, and
// NativeFrame's locked span is normally just a couple of milliseconds, this should rarely block
// for long.
extern std::mutex g_frameMutex;
extern volatile CoreState coreState;
extern volatile bool coreStatePending;
void Core_UpdateState(CoreState newState);
enum class MemoryExceptionType {
NONE,
UNKNOWN,
READ_WORD,
WRITE_WORD,
HLE_READ,
HLE_WRITE,
READ_BLOCK,
WRITE_BLOCK,
ALIGNMENT,
};
enum class ExecExceptionType {
JUMP,
THREAD,
ILLEGAL,
};
void Core_MemoryException(u32 address, u32 accessSize, u32 pc, MemoryExceptionType type, std::string_view additionalInfo = "");
void Core_ExecException(u32 address, u32 pc, ExecExceptionType type);
void Core_BreakException(u32 pc);
// Call when loading save states, etc.
void Core_ResetException();
// Used by headless/pspautotest to collect data for the diffs. Crash reports are also sent here.
// Log level is only used if the listener is not registered.
enum class LogLevel : int;
enum GEBufferFormat : uint8_t;
struct DebugScreenshotDesc {
const uint8_t *data;
u32 stride;
u32 height;
GEBufferFormat format;
};
void Core_SendDebugOutput(LogLevel level, std::string_view string);
void Core_SendDebugScreenshot(const DebugScreenshotDesc &desc);
void Core_RegisterDebugOutputListeners(std::function<void(std::string_view)> listener, std::function<void(const DebugScreenshotDesc &)> screenshotListener);
class MIPSState;
// Shortcut, just calls Core_MemoryException with automatically determined parameters (function name, etc).
void Core_MemoryExceptionHLE(MIPSState *mips, u32 address, u32 accessSize, MemoryExceptionType type);
enum class MIPSExceptionType {
NONE,
MEMORY,
BREAK,
BAD_EXEC_ADDR,
};
struct MIPSExceptionInfo {
MIPSExceptionType type;
std::string info;
std::string stackTrace; // if available.
// Memory exception info
MemoryExceptionType memory_type;
uint32_t pc;
uint32_t address;
uint32_t accessSize;
uint32_t ra = 0;
// Reuses pc and address from memory type, where address is the failed destination.
ExecExceptionType exec_type;
};
const MIPSExceptionInfo &Core_GetExceptionInfo();
const char *ExceptionTypeAsString(MIPSExceptionType type);
const char *MemoryExceptionTypeAsString(MemoryExceptionType type);
const char *ExecExceptionTypeAsString(ExecExceptionType type);