mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 09:45:24 +02:00
Eight events answered nothing at all: cpu.stepping, cpu.resume, gpu.stats.feed
and the five stepping requests. Their documented contract was "no immediate
response, an event follows", which leaves a client unable to tell an accepted
request from one that was dropped - and forces any request/response
correlation to carry a hardcoded list of events that don't answer. wsdbg's
--sync doesn't have that list, so it waits for the next message and treats
whatever broadcast arrives first as the answer, silently misattributing every
later response in the script.
Fixed centrally in the dispatch loop rather than in the eight handlers: if a
handler finishes without having sent anything, send an empty response carrying
its ticket. That also covers handlers added later, which is the part a
per-handler fix wouldn't.
The asynchronous event that reports the real outcome is unchanged and still
follows. The two are easy to tell apart - the acknowledgement carries the
ticket from the request, a broadcast has none:
-> {"event":"cpu.stepInto","ticket":3}
<- {"event":"cpu.stepInto","ticket":3}
<- {"event":"cpu.stepping","pc":142622896,"reason":"cpu.stepInto",...}
Existing clients ignore events they didn't ask for, and this adds a message
rather than changing or removing one, so nothing that worked before breaks.
pspautotests 314/314.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
214 lines
6.4 KiB
C++
214 lines
6.4 KiB
C++
// Copyright (c) 2021- 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 <mutex>
|
|
#include <vector>
|
|
|
|
#include "Common/Data/Text/StringWriter.h"
|
|
#include "Core/Debugger/WebSocket/GPUStatsSubscriber.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/HW/Display.h"
|
|
#include "Core/System.h"
|
|
|
|
struct CollectedStats {
|
|
float vps;
|
|
float fps;
|
|
float actual_fps;
|
|
char statbuf[4096];
|
|
std::vector<float> frameTimes;
|
|
std::vector<float> sleepTimes;
|
|
int frameTimePos;
|
|
};
|
|
|
|
struct DebuggerGPUStatsEvent {
|
|
const CollectedStats &s;
|
|
const std::string &ticket;
|
|
|
|
operator std::string() {
|
|
JsonWriter j;
|
|
j.begin();
|
|
j.writeString("event", "gpu.stats.get");
|
|
if (!ticket.empty())
|
|
j.writeRaw("ticket", ticket);
|
|
j.pushDict("fps");
|
|
j.writeFloat("actual", s.actual_fps);
|
|
j.writeFloat("target", s.fps);
|
|
j.pop();
|
|
j.pushDict("vblanksPerSecond");
|
|
j.writeFloat("actual", s.vps);
|
|
j.writeFloat("target", 60.0 / 1.001);
|
|
j.pop();
|
|
j.writeString("info", s.statbuf);
|
|
j.pushDict("timing");
|
|
j.pushArray("frames");
|
|
for (double t : s.frameTimes)
|
|
j.writeFloat(t);
|
|
j.pop();
|
|
j.pushArray("sleep");
|
|
for (double t : s.sleepTimes)
|
|
j.writeFloat(t);
|
|
j.pop();
|
|
j.writeInt("pos", s.frameTimePos);
|
|
j.pop();
|
|
j.end();
|
|
return j.str();
|
|
}
|
|
};
|
|
|
|
struct WebSocketGPUStatsState : public DebuggerSubscriber {
|
|
WebSocketGPUStatsState();
|
|
~WebSocketGPUStatsState();
|
|
void Get(DebuggerRequest &req);
|
|
void Feed(DebuggerRequest &req);
|
|
|
|
void Broadcast(net::WebSocketServer *ws) override;
|
|
|
|
static void FlipForwarder(void *thiz);
|
|
void FlipListener();
|
|
|
|
protected:
|
|
bool forced_ = false;
|
|
bool sendNext_ = false;
|
|
bool sendFeed_ = false;
|
|
|
|
std::string lastTicket_;
|
|
std::mutex pendingLock_;
|
|
std::vector<CollectedStats> pendingStats_;
|
|
};
|
|
|
|
DebuggerSubscriber *WebSocketGPUStatsInit(DebuggerEventHandlerMap &map) {
|
|
auto p = new WebSocketGPUStatsState();
|
|
map["gpu.stats.get"] = [p](DebuggerRequest &req) { p->Get(req); };
|
|
map["gpu.stats.feed"] = [p](DebuggerRequest &req) { p->Feed(req); };
|
|
|
|
return p;
|
|
}
|
|
|
|
WebSocketGPUStatsState::WebSocketGPUStatsState() {
|
|
__DisplayListenFlip(&WebSocketGPUStatsState::FlipForwarder, this);
|
|
}
|
|
|
|
WebSocketGPUStatsState::~WebSocketGPUStatsState() {
|
|
// PSP_ForceDebugStats bumps a plain counter, so do it on the CPU thread that owns it - see
|
|
// Core_RunOnCPUThread() in Core.h.
|
|
if (forced_)
|
|
Core_RunOnCPUThread([] { PSP_ForceDebugStats(false); });
|
|
__DisplayForgetFlip(&WebSocketGPUStatsState::FlipForwarder, this);
|
|
}
|
|
|
|
void WebSocketGPUStatsState::FlipForwarder(void *thiz) {
|
|
WebSocketGPUStatsState *p = (WebSocketGPUStatsState *)thiz;
|
|
p->FlipListener();
|
|
}
|
|
|
|
void WebSocketGPUStatsState::FlipListener() {
|
|
if (!sendNext_ && !sendFeed_)
|
|
return;
|
|
|
|
// Okay, collect the data (we'll actually send at next Broadcast.)
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
pendingStats_.resize(pendingStats_.size() + 1);
|
|
CollectedStats &stats = pendingStats_[pendingStats_.size() - 1];
|
|
|
|
__DisplayGetFPS(&stats.vps, &stats.fps, &stats.actual_fps);
|
|
|
|
StringWriter w(stats.statbuf);
|
|
__DisplayGetDebugStats(w);
|
|
|
|
int valid;
|
|
float *sleepHistory;
|
|
float *history = __DisplayGetFrameTimes(&valid, &stats.frameTimePos, &sleepHistory);
|
|
|
|
stats.frameTimes.resize(valid);
|
|
stats.sleepTimes.resize(valid);
|
|
if (valid > 0) {
|
|
memcpy(&stats.frameTimes[0], history, sizeof(float) * valid);
|
|
memcpy(&stats.sleepTimes[0], sleepHistory, sizeof(float) * valid);
|
|
}
|
|
|
|
sendNext_ = false;
|
|
}
|
|
|
|
// Get next GPU stats (gpu.stats.get)
|
|
//
|
|
// No parameters.
|
|
//
|
|
// Response (same event name):
|
|
// - fps: object with "actual" and "target" properties, representing frames per second.
|
|
// - vblanksPerSecond: object with "actual" and "target" properties, for vblank cycles.
|
|
// - info: string, representation of backend-dependent statistics.
|
|
// - timing: object with properties:
|
|
// - frames: array of numbers, each representing the time taken for a frame.
|
|
// - sleep: array of numbers, each representing the delay time waiting for next frame.
|
|
// - pos: number, index of the current frame (not always last.)
|
|
//
|
|
// Note: stats are returned after the next flip completes (paused if CPU or GPU in break.)
|
|
// Note: info and timing may not be accurate if certain settings are disabled.
|
|
// Note: sending this event with no ticket will not trigger a response! (TODO: maybe fix this?)
|
|
void WebSocketGPUStatsState::Get(DebuggerRequest &req) {
|
|
if (PSP_GetBootState() != BootState::Complete)
|
|
return req.Fail("CPU not started");
|
|
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
sendNext_ = true;
|
|
|
|
const JsonNode *value = req.data.get("ticket");
|
|
lastTicket_ = value ? json_stringify(value) : "";
|
|
}
|
|
|
|
// Setup GPU stats feed (gpu.stats.feed)
|
|
//
|
|
// Parameters:
|
|
// - enable: optional boolean, pass false to stop the feed.
|
|
//
|
|
// Response (same event name) with no extra data, acknowledging the request. Stats events then
|
|
// arrive each frame (as gpu.stats.get.)
|
|
//
|
|
// Note: info and timing will be accurate after the first frame.
|
|
void WebSocketGPUStatsState::Feed(DebuggerRequest &req) {
|
|
if (PSP_GetBootState() != BootState::Complete)
|
|
return req.Fail("CPU not started");
|
|
bool enable = true;
|
|
if (!req.ParamBool("enable", &enable, DebuggerParamType::OPTIONAL))
|
|
return;
|
|
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
sendFeed_ = enable;
|
|
if (forced_ != enable) {
|
|
Core_RunOnCPUThread([enable] { PSP_ForceDebugStats(enable); });
|
|
forced_ = enable;
|
|
}
|
|
}
|
|
|
|
void WebSocketGPUStatsState::Broadcast(net::WebSocketServer *ws) {
|
|
std::lock_guard<std::mutex> guard(pendingLock_);
|
|
if (lastTicket_.empty() && !sendFeed_) {
|
|
pendingStats_.clear();
|
|
return;
|
|
}
|
|
|
|
// To be safe, make sure we only send one if we're doing a get.
|
|
if (!sendFeed_ && pendingStats_.size() > 1)
|
|
pendingStats_.resize(1);
|
|
|
|
for (size_t i = 0; i < pendingStats_.size(); ++i) {
|
|
ws->Send(DebuggerGPUStatsEvent{ pendingStats_[i], lastTicket_ });
|
|
lastTicket_.clear();
|
|
}
|
|
pendingStats_.clear();
|
|
}
|