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 \