mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 17:49:29 +01:00
Merge pull request #22836 from hrydgard/headless-ppge-slots
Headless/debugger fixes, and a crash loading states after a system dialog
This commit is contained in:
8 files changed
+48
-19
No files matched your search
+19
-2
@@ -254,10 +254,11 @@ void Core_RegisterDebugOutputListeners(std::function<void(DebugOutputChannel, st
|
||||
}
|
||||
|
||||
void Core_SendDebugOutput(LogLevel level, std::string_view string) {
|
||||
// Logged even with a listener: headless only passes the listener's output on when it collects a
|
||||
// test's output, and these are crash reports.
|
||||
GENERIC_LOG(Log::sceIo, level, "%.*s", STR_VIEW(string));
|
||||
if (g_debugOutputListener) {
|
||||
g_debugOutputListener(DebugOutputChannel::Debug, string);
|
||||
} else {
|
||||
GENERIC_LOG(Log::sceIo, level, "%.*s", STR_VIEW(string));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +713,22 @@ void Core_Break(BreakReason reason, u32 relatedAddress, const BreakpointHit *hit
|
||||
System_Notify(SystemNotification::DEBUG_MODE_CHANGE);
|
||||
}
|
||||
|
||||
void Core_ReportBreakWithoutRunning(BreakReason reason, u32 relatedAddress) {
|
||||
if (coreState != CORE_STEPPING_CPU) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_stepMutex);
|
||||
if (!g_cpuStepCommand.empty()) {
|
||||
// A step is about to run, and its own stop will be reported.
|
||||
return;
|
||||
}
|
||||
g_breakReason = reason;
|
||||
g_breakHit = BreakpointHit{};
|
||||
g_cpuStepCommand.reason = reason;
|
||||
g_cpuStepCommand.relatedAddr = relatedAddress;
|
||||
steppingCounter++;
|
||||
}
|
||||
|
||||
// Free-threaded (or at least should be)
|
||||
void Core_Resume() {
|
||||
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
|
||||
|
||||
@@ -117,6 +117,9 @@ struct BreakpointHit {
|
||||
// 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);
|
||||
// While stepping: reports a stop for reason without running, for a run whose stop condition already
|
||||
// holds (cpu.runUntilTime with a time that's already passed), so a debugger waiting for the stop gets one.
|
||||
void Core_ReportBreakWithoutRunning(BreakReason reason, u32 relatedAddress = 0);
|
||||
|
||||
// Resumes execution. Works both when stepping the CPU and the GE.
|
||||
void Core_Resume();
|
||||
|
||||
+6
-1
@@ -567,6 +567,9 @@ void DoState(PointerWrap &p) {
|
||||
return;
|
||||
// States are saved between slices.
|
||||
otherCPUsRan = 0;
|
||||
// A debugger's run-until deadline keeps the time it had left, not its absolute time, which
|
||||
// belongs to the timeline being replaced.
|
||||
const s64 deadlineLeftUs = p.mode == PointerWrap::MODE_READ && breakDeadlineUs ? breakDeadlineUs - (s64)GetGlobalTimeUs() : 0;
|
||||
|
||||
int n = (int)event_types.size();
|
||||
int current = n;
|
||||
@@ -632,7 +635,9 @@ void DoState(PointerWrap &p) {
|
||||
lastGlobalTimeUs = 0;
|
||||
}
|
||||
if (p.mode == PointerWrap::MODE_READ) {
|
||||
// A debugger's run-until deadline is in emulated us, and the ticks it maps to just changed.
|
||||
if (breakDeadlineUs) {
|
||||
breakDeadlineUs = (s64)GetGlobalTimeUs() + std::max(deadlineLeftUs, (s64)0);
|
||||
}
|
||||
RecomputeBreakDeadline();
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,8 @@ void WebSocketGameState::StateOperation(DebuggerRequest &req, StateOp op) {
|
||||
if (useSlot) {
|
||||
if (!req.ParamU32("slot", &slot))
|
||||
return;
|
||||
if (slot >= (uint32_t)g_Config.iSaveStateSlotCount)
|
||||
// The slot count is how many the app's UI shows. Headless has no UI to show them in.
|
||||
if (slot >= (uint32_t)g_Config.iSaveStateSlotCount && !System_GetPropertyBool(SYSPROP_IS_HEADLESS))
|
||||
return req.Fail("Invalid slot");
|
||||
} else if ((op == StateOp::LOAD || op == StateOp::SAVE) && !req.ParamString("path", &path)) {
|
||||
return;
|
||||
|
||||
@@ -293,6 +293,9 @@ void WebSocketSteppingState::RunUntil(DebuggerRequest &req) {
|
||||
// - us: emulated time right now.
|
||||
// A cpu.stepping event follows once it gets there. Note that anything else that stops the CPU
|
||||
// first - a breakpoint, an exception - cancels the deadline, same as it cancels a step.
|
||||
// A time that has already passed (relativeUs 0, say) doesn't run at all: the response and the
|
||||
// cpu.stepping event come right away. A savestate loaded during the run moves the deadline along
|
||||
// with the clock, so it still runs for the time that was left.
|
||||
void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) {
|
||||
if (!currentDebugMIPS->isAlive()) {
|
||||
return req.Fail("CPU not started");
|
||||
@@ -318,15 +321,14 @@ void WebSocketSteppingState::RunUntilTime(DebuggerRequest &req) {
|
||||
const u64 nowUs = CoreTiming::GetGlobalTimeUs();
|
||||
const u64 targetUs = absolute ? (u64)requested : nowUs + (u64)requested;
|
||||
if (targetUs <= nowUs) {
|
||||
req.Fail("Target time has already passed");
|
||||
return;
|
||||
// Already there. A client waiting for the stop would otherwise never get one.
|
||||
Core_ReportBreakWithoutRunning(BreakReason::RunUntilTime, currentMIPS->pc);
|
||||
} else {
|
||||
CoreTiming::SetBreakDeadlineUs(targetUs);
|
||||
PrepareResume();
|
||||
Core_Resume();
|
||||
}
|
||||
|
||||
CoreTiming::SetBreakDeadlineUs(targetUs);
|
||||
|
||||
PrepareResume();
|
||||
Core_Resume();
|
||||
|
||||
JsonWriter &json = req.Respond();
|
||||
json.writeFloat("targetUs", (double)targetUs);
|
||||
json.writeFloat("us", (double)nowUs);
|
||||
|
||||
@@ -239,10 +239,6 @@ void __PPGeSetupListArgs() {
|
||||
}
|
||||
|
||||
void __PPGeInit() {
|
||||
// PPGe isn't really important for headless, and LoadZIM takes a long time (well not that long, but unnecessary).
|
||||
// TODO: Load it lazily.
|
||||
const bool skipZIM = System_GetPropertyBool(SYSPROP_IS_HEADLESS);
|
||||
|
||||
u8 *imageData[12]{};
|
||||
int width[12]{};
|
||||
int height[12]{};
|
||||
@@ -250,8 +246,8 @@ void __PPGeInit() {
|
||||
|
||||
// TODO: Load the atlas on a thread!
|
||||
|
||||
bool loadedZIM = !skipZIM && LoadZIM("ppge_atlas.zim", width, height, &flags, imageData);
|
||||
if (!skipZIM && !loadedZIM) {
|
||||
bool loadedZIM = LoadZIM("ppge_atlas.zim", width, height, &flags, imageData);
|
||||
if (!loadedZIM) {
|
||||
ERROR_LOG(Log::sceGe, "Failed to load ppge_atlas.zim.\n\nPlace it in the directory \"assets\" under your PPSSPP directory.\n\nPPGe stuff will not be drawn.");
|
||||
}
|
||||
|
||||
|
||||
@@ -718,6 +718,11 @@ TextureApplyResult TextureCacheCommon::ApplyTexture(bool doBind) {
|
||||
rehash = true;
|
||||
}
|
||||
|
||||
// The PPGe atlas is host data, not in emulated memory, and never changes.
|
||||
if (entry->status & TexStatus::IS_PPGE_ATLAS) {
|
||||
rehash = false;
|
||||
}
|
||||
|
||||
// got one!
|
||||
VERBOSE_LOG(Log::TexCache, "%08x: Texture found in cache. Rehash flag = %d", texaddr, (int)rehash);
|
||||
|
||||
|
||||
@@ -271,9 +271,9 @@ file - this is just an index.
|
||||
|
||||
| Category | Events | File |
|
||||
|---|---|---|
|
||||
| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game, applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree; all but this one are refused in a session booted into the XMB, whose discs come and go), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `version` | `GameSubscriber.cpp` |
|
||||
| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game (any slot in headless, otherwise one the app's slot count shows), applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree; all but this one are refused in a session booted into the XMB, whose discs come and go), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `version` | `GameSubscriber.cpp` |
|
||||
| CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` |
|
||||
| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` |
|
||||
| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop; a time already passed, such as `relativeUs` 0, stops right away without running, and a savestate loaded during the run keeps the time that was left), `cpu.nextHLE` | `SteppingSubscriber.cpp` |
|
||||
| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |
|
||||
| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write`. The numeric ones report the result as both `value` and `uintValue` - the latter is what `cpu.getReg`/`cpu.getAllRegs` call it, so a client can read either without caring which event answered | `MemorySubscriber.cpp` |
|
||||
| Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` |
|
||||
|
||||
Reference in new issue
Block a user