diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8827b89b81..386f4e6eaf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1414,6 +1414,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/Debugger/WebSocket/GameBroadcaster.h
Core/Debugger/WebSocket/GameSubscriber.cpp
Core/Debugger/WebSocket/GameSubscriber.h
+ Core/Debugger/WebSocket/GPUBufferSubscriber.cpp
+ Core/Debugger/WebSocket/GPUBufferSubscriber.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 d38b535a23..a8765dac4c 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -189,6 +189,7 @@
+
@@ -547,6 +548,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 221dd1c738..6e86aea5b2 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -728,6 +728,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
@@ -1346,6 +1349,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp
index 7bce20cc5e..4504c827f7 100644
--- a/Core/Debugger/WebSocket.cpp
+++ b/Core/Debugger/WebSocket.cpp
@@ -52,6 +52,7 @@
#include "Core/Debugger/WebSocket/CPUCoreSubscriber.h"
#include "Core/Debugger/WebSocket/DisasmSubscriber.h"
#include "Core/Debugger/WebSocket/GameSubscriber.h"
+#include "Core/Debugger/WebSocket/GPUBufferSubscriber.h"
#include "Core/Debugger/WebSocket/HLESubscriber.h"
#include "Core/Debugger/WebSocket/SteppingSubscriber.h"
@@ -67,6 +68,7 @@ static const std::vector subscribers({
{ &WebSocketCPUCoreInit, nullptr },
{ &WebSocketDisasmInit, &WebSocketDisasmShutdown },
{ &WebSocketGameInit, nullptr },
+ { &WebSocketGPUBufferInit, nullptr },
{ &WebSocketHLEInit, nullptr },
{ &WebSocketSteppingInit, &WebSocketSteppingShutdown },
});
diff --git a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp
new file mode 100644
index 0000000000..e6d1105789
--- /dev/null
+++ b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp
@@ -0,0 +1,264 @@
+// 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
+#ifndef USING_QT_UI
+#include
+#include
+#endif
+#include "data/base64.h"
+#include "Common/StringUtils.h"
+#include "Core/Debugger/WebSocket/GPUBufferSubscriber.h"
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+#include "Core/MIPS/MIPSDebugInterface.h"
+#include "Core/Screenshot.h"
+#include "GPU/Debugger/Stepping.h"
+
+void *WebSocketGPUBufferInit(DebuggerEventHandlerMap &map) {
+ // No need to bind or alloc state, these are all global.
+ map["gpu.buffer.screenshot"] = &WebSocketGPUBufferScreenshot;
+ map["gpu.buffer.renderColor"] = &WebSocketGPUBufferRenderColor;
+ map["gpu.buffer.renderDepth"] = &WebSocketGPUBufferRenderDepth;
+ map["gpu.buffer.renderStencil"] = &WebSocketGPUBufferRenderStencil;
+
+ return nullptr;
+}
+
+// Note: Calls req.Respond(). Other data can be added afterward.
+static bool StreamBufferToDataURI(DebuggerRequest &req, const GPUDebugBuffer &buf, bool includeAlpha) {
+#ifdef USING_QT_UI
+ req.Fail("Not supported on Qt yet, pull requests accepted");
+ return false;
+#else
+ u8 *flipbuffer = nullptr;
+ u32 w = (u32)-1;
+ u32 h = (u32)-1;
+ const u8 *buffer = ConvertBufferToScreenshot(buf, includeAlpha, flipbuffer, w, h);
+ if (!buffer) {
+ req.Fail("Internal error converting buffer for PNG encode");
+ return false;
+ }
+
+ png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+ if (!png_ptr) {
+ req.Fail("Internal error setting up PNG encoder (png_ptr)");
+ return false;
+ }
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr) {
+ png_destroy_write_struct(&png_ptr, nullptr);
+ req.Fail("Internal error setting up PNG encoder (info_ptr)");
+ return false;
+ }
+
+ // Speed. Wireless N should give 35 KB/ms. For most devices, zlib/filters will cost more.
+ png_set_compression_strategy(png_ptr, Z_RLE);
+ png_set_compression_level(png_ptr, 1);
+ png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE, PNG_FILTER_NONE);
+
+ auto &json = req.Respond();
+ json.writeInt("width", w);
+ json.writeInt("height", h);
+
+ // Start a value...
+ json.writeRaw("uri", "");
+ req.Flush();
+ // Now we'll write it directly to the stream.
+ req.ws->AddFragment(false, "\"data:image/png;base64,");
+
+ struct Context {
+ DebuggerRequest *req;
+ uint8_t buf[3];
+ size_t bufSize;
+ };
+ Context ctx = { &req, {}, 0 };
+
+ auto write = [](png_structp png_ptr, png_bytep data, png_size_t length) {
+ auto ctx = (Context *)png_get_io_ptr(png_ptr);
+ auto &req = *ctx->req;
+
+ // If we buffered some bytes, fill to 3 bytes for a clean base64 encode.
+ // This way we don't have padding.
+ while (length > 0 && ctx->bufSize > 0 && ctx->bufSize != 3) {
+ ctx->buf[ctx->bufSize++] = data[0];
+ data++;
+ length--;
+ }
+
+ if (ctx->bufSize == 3) {
+ req.ws->AddFragment(false, Base64Encode(ctx->buf, ctx->bufSize));
+ ctx->bufSize = 0;
+ }
+ assert(ctx->bufSize == 0 || length == 0);
+
+ // Save bytes that would result in padding for next time.
+ size_t toBuffer = length % 3;
+ for (size_t i = 0; i < toBuffer; ++i) {
+ ctx->buf[i] = data[length - toBuffer + i];
+ ctx->bufSize++;
+ }
+
+ if (length > toBuffer) {
+ req.ws->AddFragment(false, Base64Encode(data, length - toBuffer));
+ }
+ };
+ auto flush = [](png_structp png_ptr) {
+ // Nothing, just here to prevent stdio flush.
+ };
+
+ png_bytep *row_pointers = new png_bytep[h];
+ u32 stride = includeAlpha ? w * 4 : w * 3;
+ for (u32 i = 0; i < h; ++i) {
+ row_pointers[i] = (u8 *)buffer + stride * i;
+ }
+
+ png_set_write_fn(png_ptr, &ctx, write, flush);
+ int colorType = includeAlpha ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB;
+ png_set_IHDR(png_ptr, info_ptr, w, h, 8, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+ png_set_rows(png_ptr, info_ptr, row_pointers);
+ png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr);
+
+ png_destroy_write_struct(&png_ptr, &info_ptr);
+ delete [] row_pointers;
+ delete [] flipbuffer;
+
+ if (ctx.bufSize > 0) {
+ req.ws->AddFragment(false, Base64Encode(ctx.buf, ctx.bufSize));
+ ctx.bufSize = 0;
+ }
+
+ // End the string.
+ req.ws->AddFragment(false, "\"");
+ return true;
+#endif
+}
+
+static std::string DescribeFormat(GPUDebugBufferFormat fmt) {
+ switch (fmt) {
+ case GPU_DBG_FORMAT_565: return "B5G6R5_UNORM_PACK16";
+ case GPU_DBG_FORMAT_5551: return "A1B5G5R5_UNORM_PACK16";
+ case GPU_DBG_FORMAT_4444: return "A4B4G4R4_UNORM_PACK16";
+ case GPU_DBG_FORMAT_8888: return "R8G8B8A8_UNORM";
+
+ case GPU_DBG_FORMAT_565_REV: return "R5G6B5_UNORM_PACK16";
+ case GPU_DBG_FORMAT_5551_REV: return "R5G5B5A1_UNORM_PACK16";
+ case GPU_DBG_FORMAT_4444_REV: return "R4G4B4A4_UNORM_PACK16";
+
+ case GPU_DBG_FORMAT_5551_BGRA: return "A1R5G5B5_UNORM_PACK16";
+ case GPU_DBG_FORMAT_4444_BGRA: return "A4R4G4B4_UNORM_PACK16";
+ case GPU_DBG_FORMAT_8888_BGRA: return "B8G8R8A8_UNORM";
+
+ case GPU_DBG_FORMAT_FLOAT: return "D32F";
+ case GPU_DBG_FORMAT_16BIT: return "D16";
+ case GPU_DBG_FORMAT_8BIT: return "S8";
+ case GPU_DBG_FORMAT_24BIT_8X: return "D24_X8";
+ case GPU_DBG_FORMAT_24X_8BIT: return "X24_S8";
+
+ case GPU_DBG_FORMAT_FLOAT_DIV_256: return "D32F_DIV_256";
+ case GPU_DBG_FORMAT_24BIT_8X_DIV_256: return "D32F_X8_DIV_256";
+
+ case GPU_DBG_FORMAT_888_RGB: return "R8G8B8_UNORM";
+
+ case GPU_DBG_FORMAT_INVALID:
+ case GPU_DBG_FORMAT_BRSWAP_FLAG:
+ default:
+ return "UNDEFINED";
+ }
+}
+
+// Note: Calls req.Respond(). Other data can be added afterward.
+static bool StreamBufferToBase64(DebuggerRequest &req, const GPUDebugBuffer &buf) {
+ size_t length = buf.GetStride() * buf.GetHeight();
+
+ auto &json = req.Respond();
+ json.writeInt("width", buf.GetStride());
+ json.writeInt("height", buf.GetHeight());
+ json.writeBool("flipped", buf.GetFlipped());
+ json.writeString("format", DescribeFormat(buf.GetFormat()));
+
+ // Start a value without any actual data yet...
+ json.writeRaw("base64", "");
+ req.Flush();
+
+ // Now we'll write it directly to the stream.
+ req.ws->AddFragment(false, "\"");
+ // 65535 is an "even" number of base64 characters.
+ static const size_t CHUNK_SIZE = 65535;
+ for (size_t i = 0; i < length; i += CHUNK_SIZE) {
+ size_t left = std::min(length - i, CHUNK_SIZE);
+ req.ws->AddFragment(false, Base64Encode(buf.GetData() + i, left));
+ }
+ req.ws->AddFragment(false, "\"");
+
+ return true;
+}
+
+static void GenericStreamBuffer(DebuggerRequest &req, std::function func) {
+ if (!currentDebugMIPS->isAlive()) {
+ return req.Fail("CPU not started");
+ }
+ if (coreState != CORE_STEPPING && !GPUStepping::IsStepping()) {
+ return req.Fail("Neither CPU or GPU is stepping");
+ }
+
+ bool includeAlpha = false;
+ if (!req.ParamBool("alpha", &includeAlpha, DebuggerParamType::OPTIONAL))
+ return;
+ std::string type = "uri";
+ if (!req.ParamString("type", &type, DebuggerParamType::OPTIONAL))
+ return;
+ if (type != "uri" && type != "base64")
+ return req.Fail("Parameter 'type' must be either 'uri' or 'base64'");
+
+ const GPUDebugBuffer *buf = nullptr;
+ if (!func(buf)) {
+ return req.Fail("Could not download output");
+ }
+ assert(buf != nullptr);
+
+ if (type == "base64") {
+ StreamBufferToBase64(req, *buf);
+ } else if (type == "uri") {
+ StreamBufferToDataURI(req, *buf, includeAlpha);
+ } else {
+ _assert_(false);
+ }
+}
+
+void WebSocketGPUBufferScreenshot(DebuggerRequest &req) {
+ GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) {
+ return GPUStepping::GPU_GetOutputFramebuffer(buf);
+ });
+}
+
+void WebSocketGPUBufferRenderColor(DebuggerRequest &req) {
+ GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) {
+ return GPUStepping::GPU_GetCurrentFramebuffer(buf, GPU_DBG_FRAMEBUF_RENDER);
+ });
+}
+
+void WebSocketGPUBufferRenderDepth(DebuggerRequest &req) {
+ GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) {
+ return GPUStepping::GPU_GetCurrentDepthbuffer(buf);
+ });
+}
+
+void WebSocketGPUBufferRenderStencil(DebuggerRequest &req) {
+ GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) {
+ return GPUStepping::GPU_GetCurrentStencilbuffer(buf);
+ });
+}
diff --git a/Core/Debugger/WebSocket/GPUBufferSubscriber.h b/Core/Debugger/WebSocket/GPUBufferSubscriber.h
new file mode 100644
index 0000000000..166b206105
--- /dev/null
+++ b/Core/Debugger/WebSocket/GPUBufferSubscriber.h
@@ -0,0 +1,27 @@
+// 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 *WebSocketGPUBufferInit(DebuggerEventHandlerMap &map);
+
+void WebSocketGPUBufferScreenshot(DebuggerRequest &req);
+void WebSocketGPUBufferRenderColor(DebuggerRequest &req);
+void WebSocketGPUBufferRenderDepth(DebuggerRequest &req);
+void WebSocketGPUBufferRenderStencil(DebuggerRequest &req);
diff --git a/GPU/Debugger/Stepping.cpp b/GPU/Debugger/Stepping.cpp
index a7f5ebd9e5..644d833750 100644
--- a/GPU/Debugger/Stepping.cpp
+++ b/GPU/Debugger/Stepping.cpp
@@ -28,6 +28,7 @@ namespace GPUStepping {
enum PauseAction {
PAUSE_CONTINUE,
PAUSE_BREAK,
+ PAUSE_GETOUTPUTBUF,
PAUSE_GETFRAMEBUF,
PAUSE_GETDEPTHBUF,
PAUSE_GETSTENCILBUF,
@@ -87,6 +88,10 @@ static void RunPauseAction() {
case PAUSE_BREAK:
break;
+ case PAUSE_GETOUTPUTBUF:
+ bufferResult = gpuDebug->GetOutputFramebuffer(bufferFrame);
+ break;
+
case PAUSE_GETFRAMEBUF:
bufferResult = gpuDebug->GetCurrentFramebuffer(bufferFrame, bufferType);
break;
@@ -192,6 +197,10 @@ static bool GetBuffer(const GPUDebugBuffer *&buffer, PauseAction type, const GPU
return bufferResult;
}
+bool GPU_GetOutputFramebuffer(const GPUDebugBuffer *&buffer) {
+ return GetBuffer(buffer, PAUSE_GETOUTPUTBUF, bufferFrame);
+}
+
bool GPU_GetCurrentFramebuffer(const GPUDebugBuffer *&buffer, GPUDebugFramebufferType type) {
bufferType = type;
return GetBuffer(buffer, PAUSE_GETFRAMEBUF, bufferFrame);
diff --git a/GPU/Debugger/Stepping.h b/GPU/Debugger/Stepping.h
index f7fe3e02fb..fb24cf072a 100644
--- a/GPU/Debugger/Stepping.h
+++ b/GPU/Debugger/Stepping.h
@@ -31,6 +31,7 @@ namespace GPUStepping {
bool SingleStep();
bool IsStepping();
+ bool GPU_GetOutputFramebuffer(const GPUDebugBuffer *&buffer);
bool GPU_GetCurrentFramebuffer(const GPUDebugBuffer *&buffer, GPUDebugFramebufferType type);
bool GPU_GetCurrentDepthbuffer(const GPUDebugBuffer *&buffer);
bool GPU_GetCurrentStencilbuffer(const GPUDebugBuffer *&buffer);
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 4501775884..ddf7c7a799 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -307,6 +307,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Debugger/WebSocket/DisasmSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/GameBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/GameSubscriber.cpp \
+ $(SRC)/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/HLESubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/LogBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/SteppingBroadcaster.cpp \
diff --git a/ext/native/data/base64.cpp b/ext/native/data/base64.cpp
index 82d428468c..8df7abaa1e 100644
--- a/ext/native/data/base64.cpp
+++ b/ext/native/data/base64.cpp
@@ -4,7 +4,7 @@
std::string Base64Encode(const uint8_t *p, size_t sz) {
const char digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
- size_t unpaddedLength = (4 * sz + 3) / 3;
+ size_t unpaddedLength = (4 * sz + 2) / 3;
std::string result;
result.resize((unpaddedLength + 3) & ~3, '=');