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 \