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, '=');