diff --git a/CMakeLists.txt b/CMakeLists.txt index b787be7a77..2f9a21e4f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1432,6 +1432,8 @@ add_library(${CoreLibName} ${CoreLinkType} Core/Debugger/WebSocket/GameSubscriber.h Core/Debugger/WebSocket/GPUBufferSubscriber.cpp Core/Debugger/WebSocket/GPUBufferSubscriber.h + Core/Debugger/WebSocket/GPURecordSubscriber.cpp + Core/Debugger/WebSocket/GPURecordSubscriber.h Core/Debugger/WebSocket/HLESubscriber.cpp Core/Debugger/WebSocket/HLESubscriber.h Core/Debugger/WebSocket/LogBroadcaster.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 068241f0c6..c5a39b54be 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -188,6 +188,7 @@ + @@ -549,6 +550,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index ba79121304..b459851f73 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -734,6 +734,9 @@ HLE\Libraries + + Debugger\WebSocket + @@ -1361,6 +1364,9 @@ HLE\Libraries + + Debugger\WebSocket + diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index 38101b553e..6175279363 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -53,6 +53,7 @@ #include "Core/Debugger/WebSocket/DisasmSubscriber.h" #include "Core/Debugger/WebSocket/GameSubscriber.h" #include "Core/Debugger/WebSocket/GPUBufferSubscriber.h" +#include "Core/Debugger/WebSocket/GPURecordSubscriber.h" #include "Core/Debugger/WebSocket/HLESubscriber.h" #include "Core/Debugger/WebSocket/SteppingSubscriber.h" @@ -63,6 +64,7 @@ static const std::vector subscribers({ &WebSocketDisasmInit, &WebSocketGameInit, &WebSocketGPUBufferInit, + &WebSocketGPURecordInit, &WebSocketHLEInit, &WebSocketSteppingInit, }); diff --git a/Core/Debugger/WebSocket/GPURecordSubscriber.cpp b/Core/Debugger/WebSocket/GPURecordSubscriber.cpp new file mode 100644 index 0000000000..961a4d0d04 --- /dev/null +++ b/Core/Debugger/WebSocket/GPURecordSubscriber.cpp @@ -0,0 +1,107 @@ +// 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 "data/base64.h" +#include "Common/FileUtil.h" +#include "Core/Debugger/WebSocket/GPURecordSubscriber.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" +#include "Core/System.h" +#include "GPU/Debugger/Record.h" + +struct WebSocketGPURecordState : public DebuggerSubscriber { + ~WebSocketGPURecordState() override; + void Dump(DebuggerRequest &req); + + void Broadcast(net::WebSocketServer *ws) override; + +protected: + bool pending_ = false; + std::string lastTicket_; + std::string lastFilename_; +}; + +DebuggerSubscriber *WebSocketGPURecordInit(DebuggerEventHandlerMap &map) { + auto p = new WebSocketGPURecordState(); + map["gpu.record.dump"] = std::bind(&WebSocketGPURecordState::Dump, p, std::placeholders::_1); + + return p; +} + +WebSocketGPURecordState::~WebSocketGPURecordState() { + // Clear the callback to hopefully avoid a crash. + if (pending_) + GPURecord::SetCallback(nullptr); +} + +// 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) { + if (!PSP_IsInited()) + return req.Fail("CPU not started"); + + if (!GPURecord::Activate()) + return req.Fail("Recording already in progress"); + + pending_ = true; + GPURecord::SetCallback([=](const std::string &filename) { + lastFilename_ = filename; + pending_ = false; + }); + + 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 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(); + } +} diff --git a/Core/Debugger/WebSocket/GPURecordSubscriber.h b/Core/Debugger/WebSocket/GPURecordSubscriber.h new file mode 100644 index 0000000000..31a1f6b42d --- /dev/null +++ b/Core/Debugger/WebSocket/GPURecordSubscriber.h @@ -0,0 +1,22 @@ +// 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/. + +#pragma once + +#include "Core/Debugger/WebSocket/WebSocketUtils.h" + +DebuggerSubscriber *WebSocketGPURecordInit(DebuggerEventHandlerMap &map); diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index fe8e0a527c..1fa237a600 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -48,6 +48,7 @@ static const int VERSION = 2; static bool active = false; static bool nextFrame = false; static bool writePending = false; +static std::function writeCallback; enum class CommandType : u8 { INIT = 0, @@ -409,7 +410,7 @@ static void WriteCompressed(FILE *fp, const void *p, size_t sz) { delete [] compressed; } -static void WriteRecording() { +static std::string WriteRecording() { FlushRegisters(); EmitDisplayBuf(); @@ -430,6 +431,8 @@ static void WriteRecording() { WriteCompressed(fp, pushbuf.data(), bufsz); fclose(fp); + + return filename; } static void GetVertDataSizes(int vcount, const void *indices, u32 &vbytes, u32 &ibytes) { @@ -641,8 +644,31 @@ bool IsActivePending() { return nextFrame || active; } -void Activate() { - nextFrame = true; +bool Activate() { + if (!nextFrame) { + nextFrame = true; + return true; + } + return false; +} + +void SetCallback(const std::function callback) { + writeCallback = callback; +} + +static void FinishRecording() { + // We're done - this was just to write the result out. + std::string filename = WriteRecording(); + commands.clear(); + pushbuf.clear(); + + NOTICE_LOG(SYSTEM, "Recording finished"); + writePending = false; + active = false; + + if (writeCallback) + writeCallback(filename); + writeCallback = nullptr; } void NotifyCommand(u32 pc) { @@ -650,14 +676,7 @@ void NotifyCommand(u32 pc) { return; } if (writePending) { - WriteRecording(); - commands.clear(); - pushbuf.clear(); - - writePending = false; - // We're done - this was just to write the result out. - NOTICE_LOG(SYSTEM, "Recording finished"); - active = false; + FinishRecording(); return; } diff --git a/GPU/Debugger/Record.h b/GPU/Debugger/Record.h index d040d82a89..230b361d19 100644 --- a/GPU/Debugger/Record.h +++ b/GPU/Debugger/Record.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include "Common/CommonTypes.h" @@ -24,7 +25,9 @@ namespace GPURecord { bool IsActive(); bool IsActivePending(); -void Activate(); +bool Activate(); +// Call only if Activate() returns true. +void SetCallback(const std::function callback); void NotifyCommand(u32 pc); void NotifyMemcpy(u32 dest, u32 src, u32 sz); diff --git a/android/jni/Android.mk b/android/jni/Android.mk index b399653d63..1ed3b4eb69 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -310,6 +310,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/Debugger/WebSocket/GameBroadcaster.cpp \ $(SRC)/Core/Debugger/WebSocket/GameSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp \ + $(SRC)/Core/Debugger/WebSocket/GPURecordSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/HLESubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \ $(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \