Files
ppsspp/Core/Debugger/WebSocket/GPURecordSubscriber.cpp
Henrik RydgårdandClaude Opus 5 a0f223d933 Move the last debugger core access onto the CPU thread
Three things still touched CPU-thread-owned state from the WebSocket thread:

- Breakpoint conditions were compiled in Parse(), and resolving symbols in an
  expression goes through g_symbolMap, which is destroyed on shutdown. Compiled
  inside the queued callback now, before anything is mutated, so a bad
  expression still fails without leaving a breakpoint behind.
- gpu.record.dump dereferenced the gpu pointer, which is created and destroyed
  on the CPU thread.
- gpu.stats.feed bumped PSP_ForceDebugStats' plain counter.

Also makes g_bootState atomic - it's read as a fast-fail from the debugger
thread all over while the CPU and loader threads move it along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-17 13:11:16 +02:00

127 lines
3.7 KiB
C++

// Copyright (c) 2018- 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/.
#include "Common/Data/Encoding/Base64.h"
#include "Common/File/FileUtil.h"
#include "Core/Core.h"
#include "Core/Debugger/WebSocket/GPURecordSubscriber.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/System.h"
#include "GPU/Debugger/Record.h"
#include "GPU/GPU.h"
#include "GPU/GPUCommon.h"
struct WebSocketGPURecordState : public DebuggerSubscriber {
~WebSocketGPURecordState();
void Dump(DebuggerRequest &req);
void Broadcast(net::WebSocketServer *ws) override;
protected:
bool pending_ = false;
std::string lastTicket_;
Path lastFilename_;
};
DebuggerSubscriber *WebSocketGPURecordInit(DebuggerEventHandlerMap &map) {
auto p = new WebSocketGPURecordState();
map["gpu.record.dump"] = [p](DebuggerRequest &req) { p->Dump(req); };
return p;
}
WebSocketGPURecordState::~WebSocketGPURecordState() {
// Clear the callback to hopefully avoid a crash. On the CPU thread, since gpu itself is
// destroyed over there - see Core_RunOnCPUThread() in Core.h.
if (pending_) {
Core_RunOnCPUThread([&] {
if (gpu)
gpu->GetRecorder()->ClearCallback();
});
}
}
// Begin recording (gpu.record.dump)
//
// No parameters.
//
// Response (same event name):
// - uri: data: URI containing debug dump data.
//
// Note: recording may take a moment.
void WebSocketGPURecordState::Dump(DebuggerRequest &req) {
// gpu is created and destroyed on the CPU thread, so ask it for a recording over there rather
// than dereferencing it from this WebSocket handler thread.
bool started = false;
bool haveGPU = false;
Core_RunOnCPUThread([&] {
haveGPU = PSP_GetBootState() == BootState::Complete && gpu != nullptr;
if (!haveGPU)
return;
started = gpu->GetRecorder()->RecordNextFrame([=](const Path &filename) {
lastFilename_ = filename;
pending_ = false;
});
});
if (!haveGPU) {
return req.Fail("CPU not started");
}
if (!started) {
return req.Fail("Recording already in progress");
}
pending_ = true;
const JsonNode *value = req.data.get("ticket");
lastTicket_ = value ? json_stringify(value) : "";
}
// This handles the asynchronous gpu.record.dump response.
void WebSocketGPURecordState::Broadcast(net::WebSocketServer *ws) {
if (!lastFilename_.empty()) {
FILE *fp = File::OpenCFile(lastFilename_, "rb");
if (!fp) {
lastFilename_.clear();
return;
}
// We write directly to the stream since this is a large chunk of data.
ws->AddFragment(false, R"({"event":"gpu.record.dump")");
if (!lastTicket_.empty()) {
ws->AddFragment(false, R"(,"ticket":)");
ws->AddFragment(false, lastTicket_);
}
ws->AddFragment(false, R"(,"uri":"data:application/octet-stream;base64,)");
// Divisible by 3 for base64 reasons.
const size_t BUF_SIZE = 16383;
std::vector<uint8_t> buf;
buf.resize(BUF_SIZE);
while (!feof(fp)) {
size_t bytes = fread(&buf[0], 1, BUF_SIZE, fp);
ws->AddFragment(false, Base64Encode(&buf[0], bytes));
}
fclose(fp);
ws->AddFragment(true, R"("})");
lastFilename_.clear();
lastTicket_.clear();
}
}