diff --git a/CMakeLists.txt b/CMakeLists.txt
index f4661a6d8d..8dc0498dd9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1580,6 +1580,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/Debugger/WebSocket/MemoryInfoSubscriber.h
Core/Debugger/WebSocket/MemorySubscriber.cpp
Core/Debugger/WebSocket/MemorySubscriber.h
+ Core/Debugger/WebSocket/ReplaySubscriber.cpp
+ Core/Debugger/WebSocket/ReplaySubscriber.h
Core/Debugger/WebSocket/SteppingBroadcaster.cpp
Core/Debugger/WebSocket/SteppingBroadcaster.h
Core/Debugger/WebSocket/SteppingSubscriber.cpp
diff --git a/Common/Net/WebsocketServer.cpp b/Common/Net/WebsocketServer.cpp
index 3b5449b489..3b6f98ed27 100644
--- a/Common/Net/WebsocketServer.cpp
+++ b/Common/Net/WebsocketServer.cpp
@@ -341,8 +341,8 @@ bool WebSocketServer::ReadFrame() {
mask = &header[10];
// Read from big endian.
- uint64_t high = (header[2] << 24) | (header[3] << 16) || (header[4] << 8) | (header[5] << 0);
- uint64_t low = (header[6] << 24) | (header[7] << 16) || (header[8] << 8) | (header[9] << 0);
+ uint64_t high = (header[2] << 24) | (header[3] << 16) | (header[4] << 8) | (header[5] << 0);
+ uint64_t low = (header[6] << 24) | (header[7] << 16) | (header[8] << 8) | (header[9] << 0);
sz = (high << 32) | low;
if ((sz & 0x8000000000000000ULL) != 0) {
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index 68e918a7f0..ececc51b5f 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -525,6 +525,7 @@
+
@@ -1076,6 +1077,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index c6a59d5d51..43a36fa022 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -1172,6 +1172,9 @@
Ext\libzip
+
+ Debugger\WebSocket
+
@@ -1886,6 +1889,9 @@
Ext\libzip
+
+ Debugger\WebSocket
+
diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp
index f3f83254af..3d85f4b6fd 100644
--- a/Core/Debugger/WebSocket.cpp
+++ b/Core/Debugger/WebSocket.cpp
@@ -59,6 +59,7 @@
#include "Core/Debugger/WebSocket/InputSubscriber.h"
#include "Core/Debugger/WebSocket/MemoryInfoSubscriber.h"
#include "Core/Debugger/WebSocket/MemorySubscriber.h"
+#include "Core/Debugger/WebSocket/ReplaySubscriber.h"
#include "Core/Debugger/WebSocket/SteppingSubscriber.h"
typedef DebuggerSubscriber *(*SubscriberInit)(DebuggerEventHandlerMap &map);
@@ -73,6 +74,7 @@ static const std::vector subscribers({
&WebSocketInputInit,
&WebSocketMemoryInfoInit,
&WebSocketMemoryInit,
+ &WebSocketReplayInit,
&WebSocketSteppingInit,
});
diff --git a/Core/Debugger/WebSocket/ReplaySubscriber.cpp b/Core/Debugger/WebSocket/ReplaySubscriber.cpp
new file mode 100644
index 0000000000..3b548a25e5
--- /dev/null
+++ b/Core/Debugger/WebSocket/ReplaySubscriber.cpp
@@ -0,0 +1,157 @@
+// 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
+#include "Common/Data/Encoding/Base64.h"
+#include "Common/Swap.h"
+#include "Core/HLE/sceRtc.h"
+#include "Core/Replay.h"
+#include "Core/System.h"
+#include "Core/Debugger/WebSocket/ReplaySubscriber.h"
+
+DebuggerSubscriber *WebSocketReplayInit(DebuggerEventHandlerMap &map) {
+ // No need to bind or alloc state, these are all global.
+ map["replay.begin"] = &WebSocketReplayBegin;
+ map["replay.abort"] = &WebSocketReplayAbort;
+ map["replay.flush"] = &WebSocketReplayFlush;
+ map["replay.execute"] = &WebSocketReplayExecute;
+ map["replay.status"] = &WebSocketReplayStatus;
+ map["replay.time.get"] = &WebSocketReplayTimeGet;
+ map["replay.time.set"] = &WebSocketReplayTimeSet;
+
+ return nullptr;
+}
+
+// Begin or resume recording of replay data (replay.begin)
+//
+// If a replay was previously being played back, this will keep any executed replay data up to
+// this point for the next flush. To discard, break the CPU, abort, and then begin.
+//
+// No parameters.
+//
+// Empty response.
+void WebSocketReplayBegin(DebuggerRequest &req) {
+ ReplayBeginSave();
+ req.Respond();
+}
+
+// Abort any replay execution or recording (replay.abort)
+//
+// This stops executing any replay and discards any in progress recording.
+//
+// No parameters.
+//
+// Response (same event name) with no extra data.
+void WebSocketReplayAbort(DebuggerRequest &req) {
+ ReplayAbort();
+ req.Respond();
+}
+
+// Flush current recording data (replay.flush)
+//
+// Flushes event data and returns it. Note when combining, you must decode first.
+//
+// No parameters.
+//
+// Response (same event name):
+// - version: unsigned integer, version number of data.
+// - base64: base64 encode of binary data.
+void WebSocketReplayFlush(DebuggerRequest &req) {
+ if (!PSP_IsInited())
+ return req.Fail("Game not running");
+
+ std::vector data;
+ ReplayFlushBlob(&data);
+
+ JsonWriter &json = req.Respond();
+ json.writeInt("version", ReplayVersion());
+ json.writeString("base64", Base64Encode(data.data(), data.size()));
+}
+
+// Begin executing a replay (replay.execute)
+//
+// Parameters:
+// - version: unsigned integer, same version from replay.flush.
+// - base64: base64 encoded replay data.
+//
+// Response (same event name) with no extra data.
+void WebSocketReplayExecute(DebuggerRequest &req) {
+ if (!PSP_IsInited())
+ return req.Fail("Game not running");
+
+ uint32_t version = -1;
+ if (!req.ParamU32("version", &version))
+ return;
+ std::string encoded;
+ if (!req.ParamString("base64", &encoded))
+ return;
+
+ std::vector data = Base64Decode(encoded.data(), encoded.size());
+ if (!ReplayExecuteBlob(version, data))
+ return req.Fail("Invalid replay data or version");
+
+ req.Respond();
+}
+
+// Get replay status (replay.status)
+//
+// No parameters.
+//
+// Response (same event name):
+// - executing: boolean if a replay is being executed.
+// - saving: boolean if a replay is being recorded.
+void WebSocketReplayStatus(DebuggerRequest &req) {
+ JsonWriter &json = req.Respond();
+ json.writeBool("executing", ReplayIsExecuting());
+ json.writeBool("saving", ReplayIsSaving());
+}
+
+// Get the base RTC (real time clock) time for replay data (replay.time.get)
+//
+// The base time is constant during a game session, and represents the "power on" time of the
+// emulated PSP.
+//
+// No parameters.
+//
+// Response (same event name):
+// - value: unsigned integer, may have more than 32 integer bits.
+void WebSocketReplayTimeGet(DebuggerRequest &req) {
+ if (!PSP_IsInited())
+ return req.Fail("Game not running");
+
+ JsonWriter &json = req.Respond();
+ json.writeUint("value", RtcBaseTime());
+}
+
+// Overwrite the base RTC time (replay.time.set)
+//
+// Parameters:
+// - value: unsigned integer.
+//
+// Empty response.
+void WebSocketReplayTimeSet(DebuggerRequest &req) {
+ if (!PSP_IsInited())
+ return req.Fail("Game not running");
+
+ uint32_t value;
+ if (!req.ParamU32("value", &value, false)) {
+ return;
+ }
+
+ RtcSetBaseTime((int32_t)value);
+ req.Respond();
+}
diff --git a/Core/Debugger/WebSocket/ReplaySubscriber.h b/Core/Debugger/WebSocket/ReplaySubscriber.h
new file mode 100644
index 0000000000..901e54f60e
--- /dev/null
+++ b/Core/Debugger/WebSocket/ReplaySubscriber.h
@@ -0,0 +1,30 @@
+// 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/.
+
+#pragma once
+
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+
+DebuggerSubscriber *WebSocketReplayInit(DebuggerEventHandlerMap &map);
+
+void WebSocketReplayBegin(DebuggerRequest &req);
+void WebSocketReplayAbort(DebuggerRequest &req);
+void WebSocketReplayFlush(DebuggerRequest &req);
+void WebSocketReplayExecute(DebuggerRequest &req);
+void WebSocketReplayStatus(DebuggerRequest &req);
+void WebSocketReplayTimeGet(DebuggerRequest &req);
+void WebSocketReplayTimeSet(DebuggerRequest &req);
diff --git a/Core/Replay.cpp b/Core/Replay.cpp
index 93b0968c25..e94eebe04b 100644
--- a/Core/Replay.cpp
+++ b/Core/Replay.cpp
@@ -116,7 +116,7 @@ struct ReplayFileInfo {
struct ReplayItem {
ReplayItemHeader info;
- std::vector data;
+ std::vector data;
ReplayItem(ReplayItemHeader h) : info(h) {
}
@@ -136,7 +136,16 @@ static uint8_t lastAnalog[2][2]{};
static size_t replayDiskPos = 0;
static bool diskFailed = false;
-void ReplayExecuteBlob(const std::vector &data) {
+bool ReplayExecuteBlob(int version, const std::vector &data) {
+ if (version < REPLAY_VERSION_MIN || version > REPLAY_VERSION_CURRENT) {
+ ERROR_LOG(SYSTEM, "Bad replay data version: %d", version);
+ return false;
+ }
+ if (data.size() == 0) {
+ ERROR_LOG(SYSTEM, "Empty replay data");
+ return false;
+ }
+
ReplayAbort();
// Rough estimate.
@@ -167,6 +176,7 @@ void ReplayExecuteBlob(const std::vector &data) {
replayState = ReplayState::EXECUTE;
INFO_LOG(SYSTEM, "Executing replay with %lld items", (long long)replayItems.size());
+ return true;
}
bool ReplayExecuteFile(const Path &filename) {
@@ -178,7 +188,8 @@ bool ReplayExecuteFile(const Path &filename) {
return false;
}
- std::vector data;
+ int version = -1;
+ std::vector data;
auto loadData = [&]() {
// TODO: Maybe stream instead.
size_t sz = File::GetFileSize(fp);
@@ -206,6 +217,9 @@ bool ReplayExecuteFile(const Path &filename) {
WARN_LOG(SYSTEM, "Replay version %d scary and futuristic, trying anyway", fh.version);
}
+ RtcSetBaseTime((int32_t)fh.rtcBaseSeconds, 0);
+ version = fh.version;
+
data.resize(sz);
if (fread(&data[0], sz, 1, fp) != 1) {
@@ -218,7 +232,7 @@ bool ReplayExecuteFile(const Path &filename) {
if (loadData()) {
fclose(fp);
- ReplayExecuteBlob(data);
+ ReplayExecuteBlob(version, data);
return true;
}
@@ -243,7 +257,7 @@ void ReplayBeginSave() {
replayState = ReplayState::SAVE;
}
-void ReplayFlushBlob(std::vector *data) {
+void ReplayFlushBlob(std::vector *data) {
size_t sz = replayItems.size() * sizeof(ReplayItemHeader);
// Add in any side data.
for (const auto &item : replayItems) {
@@ -289,7 +303,7 @@ bool ReplayFlushFile(const Path &filename) {
size_t c = replayItems.size();
if (success && c != 0) {
// TODO: Maybe stream instead.
- std::vector data;
+ std::vector data;
ReplayFlushBlob(&data);
success = fwrite(&data[0], data.size(), 1, fp) == 1;
@@ -304,6 +318,10 @@ bool ReplayFlushFile(const Path &filename) {
return success;
}
+int ReplayVersion() {
+ return REPLAY_VERSION_CURRENT;
+}
+
void ReplayAbort() {
replayItems.clear();
replayExecPos = 0;
@@ -319,6 +337,14 @@ void ReplayAbort() {
diskFailed = false;
}
+bool ReplayIsExecuting() {
+ return replayState == ReplayState::EXECUTE;
+}
+
+bool ReplayIsSaving() {
+ return replayState == ReplayState::SAVE;
+}
+
static void ReplaySaveCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t t) {
if (lastButtons != buttons) {
replayItems.push_back(ReplayItemHeader(ReplayAction::BUTTONS, t, buttons));
diff --git a/Core/Replay.h b/Core/Replay.h
index 2229a6dee8..8ed514108c 100644
--- a/Core/Replay.h
+++ b/Core/Replay.h
@@ -49,24 +49,30 @@ enum class ReplayAction : uint8_t {
struct PSPFileInfo;
// Replay from data in memory. Does not manipulate base time / RNG state.
-void ReplayExecuteBlob(const std::vector &data);
+bool ReplayExecuteBlob(int version, const std::vector &data);
// Replay from data in a file. Returns false if invalid.
bool ReplayExecuteFile(const Path &filename);
-// Returns whether there are unexected events to replay.
+// Returns whether there are unexecuted events to replay.
bool ReplayHasMoreEvents();
// Begin recording. If currently executing, discards unexecuted events.
void ReplayBeginSave();
// Flush buffered events to memory. Continues recording (next call will receive new events only.)
// No header is flushed with this operation - don't mix with ReplayFlushFile().
-void ReplayFlushBlob(std::vector *data);
+void ReplayFlushBlob(std::vector *data);
// Flush buffered events to file. Continues recording (next call will receive new events only.)
// Do not call with a different filename before ReplayAbort().
bool ReplayFlushFile(const Path &filename);
+// Get current replay data version.
+int ReplayVersion();
// Abort any execute or record operation in progress.
void ReplayAbort();
+// Check if replay data is being executed or saved.
+bool ReplayIsExecuting();
+bool ReplayIsSaving();
+
void ReplayApplyCtrl(uint32_t &buttons, uint8_t analog[2][2], uint64_t t);
uint32_t ReplayApplyDisk(ReplayAction action, uint32_t result, uint64_t t);
uint64_t ReplayApplyDisk64(ReplayAction action, uint64_t result, uint64_t t);
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index bd5f53edeb..f3f026c4d6 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -405,6 +405,7 @@
+
@@ -636,6 +637,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index 7d70fe9b42..89b790f3c4 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -700,6 +700,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
@@ -1701,6 +1704,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index d699a76b80..1f0b6efd83 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -422,6 +422,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/MemorySubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/MemoryInfoSubscriber.cpp \
+ $(SRC)/Core/Debugger/WebSocket/ReplaySubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/SteppingSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/WebSocketUtils.cpp \