diff --git a/CMakeLists.txt b/CMakeLists.txt
index 17d2c1054d14..afdb6718b327 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1438,6 +1438,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/Debugger/WebSocket/GameSubscriber.h
Core/Debugger/WebSocket/LogBroadcaster.cpp
Core/Debugger/WebSocket/LogBroadcaster.h
+ Core/Debugger/WebSocket/MemorySubscriber.cpp
+ Core/Debugger/WebSocket/MemorySubscriber.h
Core/Debugger/WebSocket/SteppingBroadcaster.cpp
Core/Debugger/WebSocket/SteppingBroadcaster.h
Core/Debugger/WebSocket/WebSocketUtils.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index 4e8826e6e14e..8a12d7c86729 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -189,6 +189,7 @@
+
@@ -541,6 +542,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 56f4f143f4b7..551c9c680c28 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -716,6 +716,7 @@
Debugger\WebSocket
+
@@ -1319,6 +1320,7 @@
Debugger\WebSocket
+
diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp
index 5b323bce4bce..ffcb228df635 100644
--- a/Core/Debugger/WebSocket.cpp
+++ b/Core/Debugger/WebSocket.cpp
@@ -49,6 +49,7 @@
#include "Core/Debugger/WebSocket/CPUCoreSubscriber.h"
#include "Core/Debugger/WebSocket/GameSubscriber.h"
+#include "Core/Debugger/WebSocket/MemorySubscriber.h"
typedef void *(*SubscriberInit)(DebuggerEventHandlerMap &map);
typedef void (*Subscribershutdown)(void *p);
@@ -60,6 +61,7 @@ struct SubscriberInfo {
static const std::vector subscribers({
{ &WebSocketCPUCoreInit, nullptr },
{ &WebSocketGameInit, nullptr },
+ { &WebSocketMemoryInit, &WebSocketMemoryShutdown },
});
// To handle webserver restart, keep track of how many running.
diff --git a/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp
index 3d42a46df358..2d6ee2f9e827 100644
--- a/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp
+++ b/Core/Debugger/WebSocket/CPUCoreSubscriber.cpp
@@ -296,7 +296,7 @@ void WebSocketCPUSetReg(DebuggerRequest &req) {
}
uint32_t val;
- if (!req.ParamU32OrFloatBits("value", &val)) {
+ if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
diff --git a/Core/Debugger/WebSocket/MemorySubscriber.cpp b/Core/Debugger/WebSocket/MemorySubscriber.cpp
new file mode 100644
index 000000000000..0a6c3f39e918
--- /dev/null
+++ b/Core/Debugger/WebSocket/MemorySubscriber.cpp
@@ -0,0 +1,237 @@
+// 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 "base/stringutil.h"
+#include "Core/Debugger/Breakpoints.h"
+#include "Core/Debugger/DisassemblyManager.h"
+#include "Core/Debugger/WebSocket/MemorySubscriber.h"
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+#include "Core/MemMap.h"
+#include "Core/MIPS/MIPSDebugInterface.h"
+
+struct WebSocketMemoryState {
+ void Base(DebuggerRequest &req);
+ void Disasm(DebuggerRequest &req);
+
+protected:
+ void WriteDisasmLine(JsonWriter &json, const DisassemblyLineInfo &l);
+ void WriteBranchGuide(JsonWriter &json, const BranchLine &l);
+
+ DisassemblyManager disasm_;
+};
+
+void *WebSocketMemoryInit(DebuggerEventHandlerMap &map) {
+ auto p = new WebSocketMemoryState();
+ map["memory.base"] = std::bind(&WebSocketMemoryState::Base, p, std::placeholders::_1);
+ map["memory.disasm"] = std::bind(&WebSocketMemoryState::Disasm, p, std::placeholders::_1);
+
+ return p;
+}
+
+void WebSocketMemoryShutdown(void *p) {
+ delete static_cast(p);
+}
+
+void WebSocketMemoryState::WriteDisasmLine(JsonWriter &json, const DisassemblyLineInfo &l) {
+ u32 addr = l.info.opcodeAddress;
+ json.pushDict();
+ if (l.type == DISTYPE_OPCODE)
+ json.writeString("type", "opcode");
+ else if (l.type == DISTYPE_MACRO)
+ json.writeString("type", "macro");
+ else if (l.type == DISTYPE_DATA)
+ json.writeString("type", "data");
+ else if (l.type == DISTYPE_OTHER)
+ json.writeString("type", "other");
+
+ json.writeFloat("address", addr);
+ json.writeInt("addressSize", l.totalSize);
+ json.writeFloat("encoding", l.info.encodedOpcode.encoding);
+ int c = currentDebugMIPS->getColor(addr) & 0x00FFFFFF;
+ json.writeString("backgroundColor", StringFromFormat("#%02x%02x%02x", c & 0xFF, (c >> 8) & 0xFF, c >> 16));
+ json.writeString("name", l.name);
+ json.writeString("params", l.params);
+
+ const std::string addressSymbol = g_symbolMap->GetLabelString(addr);
+ if (addressSymbol.empty())
+ json.writeRaw("symbol", "null");
+ else
+ json.writeString("symbol", addressSymbol);
+
+ bool enabled;
+ // TODO: Account for bp inside macro?
+ if (CBreakPoints::IsAddressBreakPoint(addr, &enabled)) {
+ json.pushDict("breakpoint");
+ json.writeBool("enabled", enabled);
+ auto cond = CBreakPoints::GetBreakPointCondition(addr);
+ if (cond)
+ json.writeString("expression", cond->expressionString);
+ else
+ json.writeRaw("expression", "null");
+ json.pop();
+ } else {
+ json.writeRaw("breakpoint", "null");
+ }
+
+ json.writeBool("isCurrentPC", currentDebugMIPS->GetPC() == addr);
+ if (l.info.isBranch) {
+ json.pushDict("branch");
+ if (!l.info.isBranchToRegister) {
+ json.writeFloat("targetAddress", l.info.branchTarget);
+ json.writeRaw("register", "null");
+ } else {
+ json.writeRaw("targetAddress", "null");
+ json.writeInt("register", l.info.branchRegisterNum);
+ }
+ json.writeBool("isLinked", l.info.isLinkedBranch);
+ json.writeBool("isLikely", l.info.isLikelyBranch);
+ json.pop();
+ } else {
+ json.writeRaw("branch", "null");
+ }
+
+ if (l.info.hasRelevantAddress) {
+ json.pushDict("relevantData");
+ json.writeFloat("address", l.info.relevantAddress);
+ if (Memory::IsValidRange(l.info.relevantAddress, 4))
+ json.writeFloat("uintValue", Memory::ReadUnchecked_U32(l.info.relevantAddress));
+ else
+ json.writeRaw("uintValue", "null");
+ json.pop();
+ } else {
+ json.writeRaw("relevantData", "null");
+ }
+
+ if (l.info.isConditional)
+ json.writeBool("conditionMet", l.info.conditionMet);
+ else
+ json.writeRaw("conditionMet", "null");
+
+ if (l.info.isDataAccess) {
+ json.pushDict("dataAccess");
+ json.writeFloat("address", l.info.dataAddress);
+ json.writeInt("size", l.info.dataSize);
+
+ std::string dataSymbol = g_symbolMap->GetLabelString(l.info.dataAddress);
+ std::string valueSymbol;
+ if (!Memory::IsValidRange(l.info.dataAddress, l.info.dataSize))
+ json.writeRaw("uintValue", "null");
+ else if (l.info.dataSize == 1)
+ json.writeFloat("uintValue", Memory::ReadUnchecked_U8(l.info.dataAddress));
+ else if (l.info.dataSize == 2)
+ json.writeFloat("uintValue", Memory::ReadUnchecked_U16(l.info.dataAddress));
+ else if (l.info.dataSize >= 4) {
+ u32 data = Memory::ReadUnchecked_U32(l.info.dataAddress);
+ valueSymbol = g_symbolMap->GetLabelString(data);
+ json.writeFloat("uintValue", data);
+ }
+
+ if (!dataSymbol.empty())
+ json.writeString("symbol", dataSymbol);
+ else
+ json.writeRaw("symbol", "null");
+ if (!valueSymbol.empty())
+ json.writeString("valueSymbol", valueSymbol);
+ else
+ json.writeRaw("valueSymbol", "null");
+ json.pop();
+ } else {
+ json.writeRaw("dataAccess", "null");
+ }
+
+ json.pop();
+}
+
+void WebSocketMemoryState::WriteBranchGuide(JsonWriter &json, const BranchLine &l) {
+ json.pushDict();
+ json.writeFloat("top", l.first);
+ json.writeFloat("bottom", l.second);
+ if (l.type == LINE_UP)
+ json.writeString("direction", "up");
+ else if (l.type == LINE_DOWN)
+ json.writeString("direction", "down");
+ else if (l.type == LINE_RIGHT)
+ json.writeString("direction", "right");
+ json.writeInt("lane", l.laneIndex);
+ json.pop();
+}
+
+void WebSocketMemoryState::Base(DebuggerRequest &req) {
+ JsonWriter &json = req.Respond();
+ json.writeString("addressHex", StringFromFormat("%016llx", Memory::base));
+}
+
+void WebSocketMemoryState::Disasm(DebuggerRequest &req) {
+ if (!currentDebugMIPS->isAlive() || !Memory::IsActive()) {
+ return req.Fail("CPU not started");
+ }
+
+ uint32_t start, end, count;
+ if (!req.ParamU32("address", &start))
+ return;
+ if (req.ParamU32("count", &count)) {
+ // Let's assume everything is two instructions.
+ disasm_.analyze(start - 4, count * 8 + 8);
+ start = disasm_.getStartAddress(start);
+ if (start == -1)
+ req.ParamU32("address", &start);
+ end = disasm_.getNthNextAddress(start, count);
+ } else if (req.ParamU32("end", &end)) {
+ // Let's assume everything is two instructions.
+ disasm_.analyze(start - 4, end - start + 8);
+ start = disasm_.getStartAddress(start);
+ if (start == -1)
+ req.ParamU32("address", &start);
+
+ // Correct end and calculate count based on it.
+ // This accounts for macros as one line, although two instructions.
+ u32 stop = end;
+ count = 0;
+ for (end = start; end < stop; end = disasm_.getNthNextAddress(end, 1)) {
+ count++;
+ }
+ } else {
+ // Error message already sent.
+ return;
+ }
+
+ bool displaySymbols = true;
+ if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.pushDict("range");
+ json.writeFloat("start", start);
+ json.writeFloat("end", end);
+ json.pop();
+
+ json.pushArray("lines");
+ DisassemblyLineInfo line;
+ u32 addr = start;
+ for (u32 i = 0; i < count; ++i) {
+ disasm_.getLine(addr, displaySymbols, line);
+ WriteDisasmLine(json, line);
+ addr += line.totalSize;
+ }
+ json.pop();
+
+ json.pushArray("branchGuides");
+ auto branchGuides = disasm_.getBranchLines(start, end - start);
+ for (auto bl : branchGuides)
+ WriteBranchGuide(json, bl);
+ json.pop();
+}
diff --git a/Core/Debugger/WebSocket/MemorySubscriber.h b/Core/Debugger/WebSocket/MemorySubscriber.h
new file mode 100644
index 000000000000..d66e851a2213
--- /dev/null
+++ b/Core/Debugger/WebSocket/MemorySubscriber.h
@@ -0,0 +1,23 @@
+// 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"
+
+void *WebSocketMemoryInit(DebuggerEventHandlerMap &map);
+void WebSocketMemoryShutdown(void *p);
diff --git a/Core/Debugger/WebSocket/WebSocketUtils.cpp b/Core/Debugger/WebSocket/WebSocketUtils.cpp
index 8d75122018ae..140ff8da826f 100644
--- a/Core/Debugger/WebSocket/WebSocketUtils.cpp
+++ b/Core/Debugger/WebSocket/WebSocketUtils.cpp
@@ -74,19 +74,35 @@ static bool U32FromString(const char *str, uint32_t *out, bool allowFloat) {
return false;
}
-bool DebuggerRequest::ParamU32(const char *name, uint32_t *out) {
+bool DebuggerRequest::ParamU32(const char *name, uint32_t *out, bool allowFloatBits, DebuggerParamType type) {
+ bool allowLoose = type == DebuggerParamType::REQUIRED_LOOSE || type == DebuggerParamType::OPTIONAL_LOOSE;
+ bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
+
const JsonNode *node = data.get(name);
if (!node) {
- Fail(StringFromFormat("Missing '%s' parameter", name));
- return false;
+ if (required)
+ Fail(StringFromFormat("Missing '%s' parameter", name));
+ return !required;
}
if (node->value.getTag() == JSON_NUMBER) {
double val = node->value.toNumber();
bool isInteger = trunc(val) == val;
- if (!isInteger) {
- Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
+ if (!isInteger && !allowLoose) {
+ // JSON doesn't give a great way to differentiate ints and floats.
+ // Let's play it safe and require a string.
+ if (allowFloatBits)
+ Fail(StringFromFormat("Could not parse '%s' parameter: use a string for non integer values", name));
+ else
+ Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
return false;
+ } else if (!isInteger && allowFloatBits) {
+ union {
+ float f;
+ uint32_t u;
+ } bits = { (float)val };
+ *out = bits.u;
+ return true;
}
if (val < 0 && val >= std::numeric_limits::min()) {
@@ -96,61 +112,87 @@ bool DebuggerRequest::ParamU32(const char *name, uint32_t *out) {
} else if (val >= 0 && val <= std::numeric_limits::max()) {
*out = (uint32_t)val;
return true;
+ } else if (allowLoose) {
+ *out = val >= 0 ? std::numeric_limits::max() : std::numeric_limits::min();
+ return true;
}
- Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range", name));
+ if (allowFloatBits)
+ Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range (use string for float)", name));
+ else
+ Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range", name));
return false;
}
if (node->value.getTag() != JSON_STRING) {
- Fail(StringFromFormat("Invalid '%s' parameter type", name));
- return false;
+ if (required || node->value.getTag() != JSON_NULL) {
+ Fail(StringFromFormat("Invalid '%s' parameter type", name));
+ return false;
+ }
+ return true;
}
- if (U32FromString(node->value.toString(), out, false))
+ if (U32FromString(node->value.toString(), out, allowFloatBits))
return true;
- Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
+ if (allowFloatBits)
+ Fail(StringFromFormat("Could not parse '%s' parameter: number expected", name));
+ else
+ Fail(StringFromFormat("Could not parse '%s' parameter: integer required", name));
return false;
}
-bool DebuggerRequest::ParamU32OrFloatBits(const char *name, uint32_t *out) {
+bool DebuggerRequest::ParamBool(const char *name, bool *out, DebuggerParamType type) {
+ bool allowLoose = type == DebuggerParamType::REQUIRED_LOOSE || type == DebuggerParamType::OPTIONAL_LOOSE;
+ bool required = type == DebuggerParamType::REQUIRED || type == DebuggerParamType::REQUIRED_LOOSE;
+
const JsonNode *node = data.get(name);
if (!node) {
- Fail(StringFromFormat("Missing '%s' parameter", name));
- return false;
+ if (required)
+ Fail(StringFromFormat("Missing '%s' parameter", name));
+ return !required;
}
if (node->value.getTag() == JSON_NUMBER) {
double val = node->value.toNumber();
- bool isInteger = trunc(val) == val;
-
- // JSON doesn't give a great way to differentiate ints and floats.
- // Let's play it safe and require a string.
- if (!isInteger) {
- Fail(StringFromFormat("Could not parse '%s' parameter: use a string for non integer values", name));
- return false;
- }
-
- if (val < 0 && val >= std::numeric_limits::min()) {
- // Convert to unsigned representation.
- *out = (uint32_t)(int32_t)val;
- return true;
- } else if (val >= 0 && val <= std::numeric_limits::max()) {
- *out = (uint32_t)val;
+ if (val == 1.0 || val == 0.0 || allowLoose) {
+ *out = val != 0.0;
return true;
}
- Fail(StringFromFormat("Could not parse '%s' parameter: outside 32 bit range (use string for float)", name));
+ Fail(StringFromFormat("Could not parse '%s' parameter: should be true/1 or false/0", name));
return false;
}
+ if (node->value.getTag() == JSON_TRUE) {
+ *out = true;
+ return true;
+ }
+ if (node->value.getTag() == JSON_FALSE) {
+ *out = false;
+ return true;
+ }
if (node->value.getTag() != JSON_STRING) {
- Fail(StringFromFormat("Invalid '%s' parameter type", name));
- return false;
+ if (type == DebuggerParamType::REQUIRED || node->value.getTag() != JSON_NULL) {
+ Fail(StringFromFormat("Invalid '%s' parameter type", name));
+ return false;
+ }
+ return true;
}
- if (U32FromString(node->value.toString(), out, true))
+ const std::string s = node->value.toString();
+ if (s == "1" || s == "true") {
+ *out = true;
+ return true;
+ }
+ if (s == "0" || s == "false" || (s == "" && allowLoose)) {
+ *out = false;
return true;
+ }
+
+ if (allowLoose) {
+ *out = true;
+ return true;
+ }
- Fail(StringFromFormat("Could not parse '%s' parameter: number expected", name));
+ Fail(StringFromFormat("Could not parse '%s' parameter: boolean required", name));
return false;
}
diff --git a/Core/Debugger/WebSocket/WebSocketUtils.h b/Core/Debugger/WebSocket/WebSocketUtils.h
index 6e4e62d36c86..132789b26dfe 100644
--- a/Core/Debugger/WebSocket/WebSocketUtils.h
+++ b/Core/Debugger/WebSocket/WebSocketUtils.h
@@ -59,6 +59,13 @@ struct DebuggerErrorEvent {
}
};
+enum class DebuggerParamType {
+ REQUIRED,
+ OPTIONAL,
+ REQUIRED_LOOSE,
+ OPTIONAL_LOOSE,
+};
+
struct DebuggerRequest {
DebuggerRequest(const char *n, net::WebSocketServer *w, const JsonGet &d)
: name(n), ws(w), data(d) {
@@ -73,8 +80,8 @@ struct DebuggerRequest {
responseSent_ = true;
}
- bool ParamU32(const char *name, uint32_t *out);
- bool ParamU32OrFloatBits(const char *name, uint32_t *out);
+ bool ParamU32(const char *name, uint32_t *out, bool allowFloatBits = false, DebuggerParamType type = DebuggerParamType::REQUIRED);
+ bool ParamBool(const char *name, bool *out, DebuggerParamType type = DebuggerParamType::REQUIRED);
JsonWriter &Respond();
void Finish();
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 2f6dc1ecdb83..3aa06ab038af 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -304,6 +304,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Debugger/WebSocket/GameBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/GameSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \
+ $(SRC)/Core/Debugger/WebSocket/MemorySubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/WebSocketUtils.cpp \
$(SRC)/Core/Dialog/PSPDialog.cpp \