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 \