Files
ppsspp/UI/ImDebugger/ImDisasmView.h
T
Henrik RydgårdandClaude Opus 5 d8a1808b3d ImDebugger: add "Run to here, next frame", gated on the flip count
Prototype of the frame-gated run-to-cursor idea. "Run to here" stops at the
first hit, which isn't what you want for an address hit many times per frame -
you end up stepping through the rest of the current frame to reach the state
you actually care about.

Built on machinery that was already there rather than a new stepping mode: the
one-shot breakpoint behind run-to-cursor already takes a condition (step-into
uses it to pin a step to one thread), and a hit that fails the condition leaves
it armed for the next one. So "the next frame" is just a condition that isn't
true yet - here "flipcount > <now>".

Counting presented frames rather than vblanks matters for a game that doesn't
render at the full refresh rate: at 30fps there are two vblanks per frame, so a
vblank-based condition would let you through halfway into the frame you were
trying to skip. The flip side is that the counter only advances when the
framebuffer actually changed, so if the game has stopped drawing - or is wedged
in the loop you're trying to debug - this never trips and the core keeps
running.

Both counters are exposed to the expression parser, next to
threadid/moduleid/usec/ticks, so they're usable in ordinary breakpoint
conditions and cpu.evaluate too, not just from this menu item: "flipcount" for
presented frames and "vcount" for the PSP's own vblank counter, which is what
sceDisplayGetVcount returns and is the one a game's own timing is written
against.

Verified with a headless session: across a second of emulated time flipcount
went 120 -> 172 and vcount 119 -> 172 (a game rendering every vblank, so they
track).

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

227 lines
5.8 KiB
C++

#pragma once
#include <vector>
#include <string>
#include <set>
#include <algorithm>
#include "ext/imgui/imgui.h"
#include "Common/CommonTypes.h"
#include "Common/Math/geom2d.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/MIPS/MIPSDebugInterface.h"
struct ImConfig;
struct ImControl;
class MIPSState;
// Corresponds to CtrlDisAsmView
// TODO: Fold out common code.
class ImDisasmView {
public:
ImDisasmView();
~ImDisasmView();
// Public variables bounds to imgui checkboxes
bool followPC_ = true;
void Draw(ImDrawList *drawList, ImControl &control);
void PopupMenu(MIPSState *mips, ImControl &control);
void NotifyStep();
void ScrollRelative(int amount);
void onChar(int c);
void onMouseDown(float x, float y, int button);
void onMouseUp(float x, float y, int button);
void onMouseMove(float x, float y, int button);
void scrollAddressIntoView();
bool curAddressIsVisible();
void ScanVisibleFunctions();
void clearFunctions() { g_disassemblyManager.clear(); };
void getOpcodeText(u32 address, char *dest, int bufsize);
u32 yToAddress(float y);
void setDebugger(MIPSDebugInterface *deb) {
if (debugger_ != deb) {
debugger_ = deb;
curAddress_ = debugger_->GetPC();
g_disassemblyManager.setCpu(deb);
}
}
MIPSDebugInterface *getDebugger() {
return debugger_;
}
void scrollStepping(u32 newPc);
void gotoAddr(unsigned int addr) {
if (positionLocked_ != 0)
return;
u32 windowEnd = g_disassemblyManager.getNthNextAddress(windowStart_, visibleRows_);
u32 newAddress = g_disassemblyManager.getStartAddress(addr);
if (newAddress < windowStart_ || newAddress >= windowEnd) {
windowStart_ = g_disassemblyManager.getNthPreviousAddress(newAddress, visibleRows_ / 2);
}
setCurAddress(newAddress);
ScanVisibleFunctions();
}
void GotoPC() {
gotoAddr(debugger_->GetPC());
}
void GotoRA() {
gotoAddr(debugger_->GetRA());
}
u32 getSelection() {
return curAddress_;
}
void setShowMode(bool s) {
showHex_ = s;
}
void toggleBreakpoint(bool toggleEnabled = false);
void editBreakpoint(ImConfig &cfg);
void setCurAddress(u32 newAddress, bool extend = false) {
newAddress = g_disassemblyManager.getStartAddress(newAddress);
const u32 after = g_disassemblyManager.getNthNextAddress(newAddress, 1);
curAddress_ = newAddress;
selectRangeStart_ = extend ? std::min(selectRangeStart_, newAddress) : newAddress;
selectRangeEnd_ = extend ? std::max(selectRangeEnd_, after) : after;
updateStatusBarText();
}
void LockPosition() {
positionLocked_++;
}
void UnlockPosition() {
positionLocked_--;
_assert_(positionLocked_ >= 0);
}
void Search(std::string_view needle);
void SearchNext(bool forward);
// Check these every frame!
const std::string &StatusBarText() const {
return statusBarText_;
}
bool SymbolMapReloaded() {
bool retval = mapReloaded_;
mapReloaded_ = false;
return retval;
}
private:
enum class CopyInstructionsMode {
OPCODES,
DISASM,
ADDRESSES,
};
void ProcessKeyboardShortcuts(bool focused);
// Plants the one-shot "run to cursor" breakpoint and resumes. With nextFrame, hits are ignored
// until the next vblank, so the rest of the current frame is skipped over.
void RunToAddress(u32 address, bool nextFrame);
// Requests the assemble popup - the actual input happens in PopupMenu(), since ImGui popups
// can only be opened and drawn from inside the frame that owns them.
void assembleOpcode(u32 address, std::string_view defaultText);
// Applies what was typed into that popup. Returns false and fills in assembleError_ if it
// couldn't be assembled, so the popup can stay open and show why.
bool applyAssembly(u32 address, std::string_view op);
std::string disassembleRange(u32 start, u32 size);
void disassembleToFile();
void FollowBranch();
void calculatePixelPositions();
bool getDisasmAddressText(u32 address, char *dest, size_t bufSize, bool abbreviateLabels, bool showData);
void updateStatusBarText();
void drawBranchLine(ImDrawList *list, Bounds rc, std::map<u32, float> &addressPositions, const BranchLine &line);
void CopyInstructions(u32 startAddr, u32 endAddr, CopyInstructionsMode mode);
std::set<std::string> getSelectedLineArguments();
void drawArguments(ImDrawList *list, Bounds rc, const DisassemblyLineInfo &line, float x, float y, ImColor textColor, const std::set<std::string> &currentArguments);
u32 curAddress_ = 0;
u32 selectRangeStart_ = 0;
u32 selectRangeEnd_ = 0;
float rowHeight_ = 0.f;
float charWidth_ = 0.f;
bool bpPopup_ = false;
bool hasFocus_ = true;
bool showHex_ = false;
MIPSDebugInterface *debugger_ = nullptr;
u32 windowStart_ = 0;
int visibleRows_ = 1;
bool displaySymbols_ = true;
struct {
int addressStart;
int opcodeStart;
int argumentsStart;
int arrowsStart;
} pixelPositions_{};
std::vector<u32> jumpStack_;
std::string searchQuery_;
int matchAddress_;
bool searching_ = false;
bool keyTaken = false;
bool mapReloaded_ = false;
int positionLocked_ = 0;
std::string statusBarText_;
u32 funcBegin_ = 0;
char funcNameTemp_[128]{};
bool assemblePopup_ = false;
u32 assembleAddress_ = 0;
char assembleTemp_[256]{};
std::string assembleError_;
};
// Corresponds to the CDisasm dialog
class ImDisasmWindow {
public:
void Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImControl &control, CoreState coreState);
ImDisasmView &View() {
return disasmView_;
}
void NotifyStep() {
disasmView_.NotifyStep();
}
void DirtySymbolMap() {
symsDirty_ = true;
}
const char *Title() const {
return "CPU Debugger";
}
private:
// We just keep the state directly in the window. Can refactor later.
enum {
INVALID_ADDR = 0xFFFFFFFF,
};
u32 gotoAddr_ = 0x08800000;
// Symbol cache
std::vector<SymbolEntry> symCache_;
bool symsDirty_ = true;
int selectedSymbol_ = -1;
char selectedSymbolName_[128];
ImDisasmView disasmView_;
char searchTerm_[64]{};
};