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.cpp b/Core/Core.cpp index bb02a1c08d..51e2fcaab4 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -37,6 +37,7 @@ #include "Core/System.h" #include "Core/Debugger/Breakpoints.h" #include "Core/MIPS/MIPS.h" +#include "GPU/Debugger/Stepping.h" #ifdef _WIN32 #include "Common/CommonWindows.h" @@ -280,6 +281,9 @@ void Core_ProcessStepping() { return; } + // Or any GPU actions. + GPUStepping::SingleStep(); + // We're not inside jit now, so it's safe to clear the breakpoints. CBreakPoints::ClearTemporaryBreakPoints(); host->UpdateDisassembly(); 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/BreakpointSubscriber.cpp b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp index aa6681cb82..ffe1797ce5 100644 --- a/Core/Debugger/WebSocket/BreakpointSubscriber.cpp +++ b/Core/Debugger/WebSocket/BreakpointSubscriber.cpp @@ -121,6 +121,18 @@ struct WebSocketCPUBreakpointParams { } }; +// Add a new CPU instruction breakpoint (cpu.breakpoint.add) +// +// Parameters: +// - address: unsigned integer address of instruction to break at. +// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - condition: optional string expression to evaluate - breakpoint does not trip if false. +// - logFormat: optional string to log when breakpoint trips, may include {expression} parts. +// +// Response (same event name) with no extra data. +// +// Note: will replace any breakpoint at the same address. void WebSocketCPUBreakpointAdd(DebuggerRequest &req) { WebSocketCPUBreakpointParams params; if (!params.Parse(req)) @@ -131,6 +143,16 @@ void WebSocketCPUBreakpointAdd(DebuggerRequest &req) { req.Respond(); } +// Update a CPU instruction breakpoint (cpu.breakpoint.update) +// +// Parameters: +// - address: unsigned integer address of instruction to break at. +// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - condition: optional string expression to evaluate - breakpoint does not trip if false. +// - logFormat: optional string to log when breakpoint trips, may include {expression} parts. +// +// Response (same event name) with no extra data. void WebSocketCPUBreakpointUpdate(DebuggerRequest &req) { WebSocketCPUBreakpointParams params; if (!params.Parse(req)) @@ -143,6 +165,12 @@ void WebSocketCPUBreakpointUpdate(DebuggerRequest &req) { req.Respond(); } +// Remove a CPU instruction breakpoint (cpu.breakpoint.remove) +// +// Parameters: +// - address: unsigned integer address of instruction to break at. +// +// Response (same event name) with no extra data. void WebSocketCPUBreakpointRemove(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) { return req.Fail("CPU not started"); @@ -156,6 +184,19 @@ void WebSocketCPUBreakpointRemove(DebuggerRequest &req) { req.Respond(); } +// List all CPU instruction breakpoints (cpu.breakpoint.list) +// +// No parameters. +// +// Response (same event name): +// - breakpoints: array of objects, each with properties: +// - address: unsigned integer address of instruction to break at. +// - enabled: boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - condition: null, or string expression to evaluate - breakpoint does not trip if false. +// - logFormat: null, or string to log when breakpoint trips, may include {expression} parts. +// - symbol: null, or string label or symbol at breakpoint address. +// - code: string disassembly of breakpoint address. void WebSocketCPUBreakpointList(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) { return req.Fail("CPU not started"); @@ -278,6 +319,22 @@ struct WebSocketMemoryBreakpointParams { } }; +// Add a new memory breakpoint (memory.breakpoint.add) +// +// Parameters: +// - address: unsigned integer address for the start of the memory range. +// - size: unsigned integer specifying size of memory range. +// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - read: optional boolean, whether to trip on any read to this address. +// - write: optional boolean, whether to trip on any write to this address. +// - change: optional boolean, whether to trip on a write to this address which modifies data +// (or any write that may modify data.) +// - logFormat: optional string to log when breakpoint trips, may include {expression} parts. +// +// Response (same event name) with no extra data. +// +// Note: will replace any breakpoint that has the same start address and size. void WebSocketMemoryBreakpointAdd(DebuggerRequest &req) { WebSocketMemoryBreakpointParams params; if (!params.Parse(req)) @@ -288,6 +345,20 @@ void WebSocketMemoryBreakpointAdd(DebuggerRequest &req) { req.Respond(); } +// Update a memory breakpoint (memory.breakpoint.update) +// +// Parameters: +// - address: unsigned integer address for the start of the memory range. +// - size: unsigned integer specifying size of memory range. +// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - read: optional boolean, whether to trip on any read to this address. +// - write: optional boolean, whether to trip on any write to this address. +// - change: optional boolean, whether to trip on a write to this address which modifies data +// (or any write that may modify data.) +// - logFormat: optional string to log when breakpoint trips, may include {expression} parts. +// +// Response (same event name) with no extra data. void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req) { WebSocketMemoryBreakpointParams params; if (!params.Parse(req)) @@ -302,6 +373,13 @@ void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req) { req.Respond(); } +// Remove a memory breakpoint (memory.breakpoint.remove) +// +// Parameters: +// - address: unsigned integer address for the start of the memory range. +// - size: unsigned integer specifying size of memory range. +// +// Response (same event name) with no extra data. void WebSocketMemoryBreakpointRemove(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) { return req.Fail("CPU not started"); @@ -318,6 +396,22 @@ void WebSocketMemoryBreakpointRemove(DebuggerRequest &req) { req.Respond(); } +// List all memory breakpoints (memory.breakpoint.list) +// +// No parameters. +// +// Response (same event name): +// - breakpoints: array of objects, each with properties: +// - address: unsigned integer address for the start of the memory range. +// - size: unsigned integer specifying size of memory range. +// - enabled: boolean, whether to actually enter stepping when this breakpoint trips. +// - log: optional boolean, whether to log when this breakpoint trips. +// - read: optional boolean, whether to trip on any read to this address. +// - write: optional boolean, whether to trip on any write to this address. +// - change: optional boolean, whether to trip on a write to this address which modifies data +// (or any write that may modify data.) +// - logFormat: null, or string to log when breakpoint trips, may include {expression} parts. +// - symbol: null, or string label or symbol at breakpoint address. void WebSocketMemoryBreakpointList(DebuggerRequest &req) { if (!currentDebugMIPS->isAlive()) { return req.Fail("CPU not started"); diff --git a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp new file mode 100644 index 0000000000..915426755b --- /dev/null +++ b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp @@ -0,0 +1,332 @@ +// 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); + } +} + +// Retrieve a screenshot (gpu.buffer.screenshot) +// +// Parameters: +// - type: either 'uri' or 'base64'. +// - alpha: boolean to include the alpha channel for 'uri' type (not normally useful for screenshots.) +// +// Response (same event name) for 'uri' type: +// - width: numeric width of screenshot. +// - height: numeric height of screenshot. +// - uri: data: URI of PNG image for display. +// +// Response (same event name) for 'base64' type: +// - width: numeric width of screenshot (also stride, in pixels, of binary data.) +// - height: numeric height of screenshot. +// - flipped: boolean to indicate whether buffer is vertically flipped. +// - format: string indicating format, such as 'R8G8B8A8_UNORM' or 'B8G8R8A8_UNORM'. +// - base64: base64 encode of binary data. +void WebSocketGPUBufferScreenshot(DebuggerRequest &req) { + GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) { + return GPUStepping::GPU_GetOutputFramebuffer(buf); + }); +} + +// Retrieve current color render buffer (gpu.buffer.renderColor) +// +// Parameters: +// - type: either 'uri' or 'base64'. +// - alpha: boolean to include the alpha channel for 'uri' type. +// +// Response (same event name) for 'uri' type: +// - width: numeric width of render buffer (may include stride.) +// - height: numeric height of render buffer. +// - uri: data: URI of PNG image for display. +// +// Response (same event name) for 'base64' type: +// - width: numeric width of render buffer (also stride, in pixels, of binary data.) +// - height: numeric height of render buffer. +// - flipped: boolean to indicate whether buffer is vertically flipped. +// - format: string indicating format, such as 'R8G8B8A8_UNORM' or 'B8G8R8A8_UNORM'. +// - base64: base64 encode of binary data. +void WebSocketGPUBufferRenderColor(DebuggerRequest &req) { + GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) { + return GPUStepping::GPU_GetCurrentFramebuffer(buf, GPU_DBG_FRAMEBUF_RENDER); + }); +} + +// Retrieve current depth render buffer (gpu.buffer.renderDepth) +// +// Parameters: +// - type: either 'uri' or 'base64'. +// - alpha: true to use alpha to encode depth, otherwise red for 'uri' type. +// +// Response (same event name) for 'uri' type: +// - width: numeric width of render buffer (may include stride.) +// - height: numeric height of render buffer. +// - uri: data: URI of PNG image for display. +// +// Response (same event name) for 'base64' type: +// - width: numeric width of render buffer (also stride, in pixels, of binary data.) +// - height: numeric height of render buffer. +// - flipped: boolean to indicate whether buffer is vertically flipped. +// - format: string indicating format, such as 'D16', 'D24_X8' or 'D32F'. +// - base64: base64 encode of binary data. +void WebSocketGPUBufferRenderDepth(DebuggerRequest &req) { + GenericStreamBuffer(req, [](const GPUDebugBuffer *&buf) { + return GPUStepping::GPU_GetCurrentDepthbuffer(buf); + }); +} + +// Retrieve current stencil render buffer (gpu.buffer.renderStencil) +// +// Parameters: +// - type: either 'uri' or 'base64'. +// - alpha: true to use alpha to encode stencil, otherwise red for 'uri' type. +// +// Response (same event name) for 'uri' type: +// - width: numeric width of render buffer (may include stride.) +// - height: numeric height of render buffer. +// - uri: data: URI of PNG image for display. +// +// Response (same event name) for 'base64' type: +// - width: numeric width of render buffer (also stride, in pixels, of binary data.) +// - height: numeric height of render buffer. +// - flipped: boolean to indicate whether buffer is vertically flipped. +// - format: string indicating format, such as 'X24_S8' or 'S8'. +// - base64: base64 encode of binary data. +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/Common/FramebufferCommon.cpp b/GPU/Common/FramebufferCommon.cpp index db23b60868..928f2751ee 100644 --- a/GPU/Common/FramebufferCommon.cpp +++ b/GPU/Common/FramebufferCommon.cpp @@ -1937,7 +1937,7 @@ bool FramebufferManagerCommon::GetFramebuffer(u32 fb_address, int fb_stride, GEB // TODO: Maybe should handle flipY inside CopyFramebufferToMemorySync somehow? bool flipY = (GetGPUBackend() == GPUBackend::OPENGL && !useBufferedRendering_) ? true : false; - buffer.Allocate(w, h, GE_FORMAT_8888, flipY, true); + buffer.Allocate(w, h, GE_FORMAT_8888, flipY); bool retval = draw_->CopyFramebufferToMemorySync(bound, Draw::FB_COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, buffer.GetData(), w); gpuStats.numReadbacks++; // After a readback we'll have flushed and started over, need to dirty a bunch of things to be safe. @@ -2015,8 +2015,12 @@ bool FramebufferManagerCommon::GetStencilbuffer(u32 fb_address, int fb_stride, G bool FramebufferManagerCommon::GetOutputFramebuffer(GPUDebugBuffer &buffer) { int w, h; draw_->GetFramebufferDimensions(nullptr, &w, &h); - buffer.Allocate(w, h, GE_FORMAT_8888, false, true); - bool retval = draw_->CopyFramebufferToMemorySync(nullptr, Draw::FB_COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, buffer.GetData(), w); + Draw::DataFormat fmt = draw_->PreferredFramebufferReadbackFormat(nullptr); + // Ignore preferred formats other than BGRA. + if (fmt != Draw::DataFormat::B8G8R8A8_UNORM) + fmt = Draw::DataFormat::R8G8B8A8_UNORM; + buffer.Allocate(w, h, fmt == Draw::DataFormat::R8G8B8A8_UNORM ? GPU_DBG_FORMAT_8888 : GPU_DBG_FORMAT_8888_BGRA, false); + bool retval = draw_->CopyFramebufferToMemorySync(nullptr, Draw::FB_COLOR_BIT, 0, 0, w, h, fmt, buffer.GetData(), w); // That may have unbound the framebuffer, rebind to avoid crashes when debugging. RebindFramebuffer(); return retval; diff --git a/GPU/Debugger/Stepping.cpp b/GPU/Debugger/Stepping.cpp index c765d15b76..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, @@ -66,6 +67,9 @@ static void SetPauseAction(PauseAction act, bool waitComplete = true) { pauseAction = act; pauseLock.unlock(); + if (coreState == CORE_STEPPING && act != PAUSE_CONTINUE) + Core_UpdateSingleStep(); + actionComplete = false; pauseWait.notify_all(); while (waitComplete && !actionComplete) { @@ -84,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; @@ -117,13 +125,41 @@ static void RunPauseAction() { pauseAction = PAUSE_BREAK; } +bool SingleStep() { + std::unique_lock guard(pauseLock); + if (coreState != CORE_RUNNING && coreState != CORE_NEXTFRAME && coreState != CORE_STEPPING) { + // Shutting down, don't try to step. + actionComplete = true; + actionWait.notify_all(); + return false; + } + if (!gpuDebug || pauseAction == PAUSE_CONTINUE) { + actionComplete = true; + actionWait.notify_all(); + return false; + } + + gpuDebug->NotifySteppingEnter(); + isStepping = true; + + RunPauseAction(); + + gpuDebug->NotifySteppingExit(); + isStepping = false; + return true; +} + bool EnterStepping(std::function callback) { std::unique_lock guard(pauseLock); if (coreState != CORE_RUNNING && coreState != CORE_NEXTFRAME) { // Shutting down, don't try to step. + actionComplete = true; + actionWait.notify_all(); return false; } if (!gpuDebug) { + actionComplete = true; + actionWait.notify_all(); return false; } @@ -152,7 +188,7 @@ bool IsStepping() { } static bool GetBuffer(const GPUDebugBuffer *&buffer, PauseAction type, const GPUDebugBuffer &resultBuffer) { - if (!isStepping) { + if (!isStepping && coreState != CORE_STEPPING) { return false; } @@ -161,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); @@ -184,7 +224,7 @@ bool GPU_GetCurrentClut(const GPUDebugBuffer *&buffer) { } bool GPU_SetCmdValue(u32 op) { - if (!isStepping) { + if (!isStepping && coreState != CORE_STEPPING) { return false; } diff --git a/GPU/Debugger/Stepping.h b/GPU/Debugger/Stepping.h index d99e8af904..fb24cf072a 100644 --- a/GPU/Debugger/Stepping.h +++ b/GPU/Debugger/Stepping.h @@ -28,8 +28,10 @@ namespace GPUStepping { // Begins stepping and calls callback while inside a lock preparing stepping. // This would be a good place to deliver a message to code that stepping is ready. bool EnterStepping(std::function callback); + 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 402036ec2d..13fc2838c3 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, '='); diff --git a/ext/native/net/sinks.cpp b/ext/native/net/sinks.cpp index b3ad60fcc9..e011f33453 100644 --- a/ext/native/net/sinks.cpp +++ b/ext/native/net/sinks.cpp @@ -341,6 +341,12 @@ bool OutputSink::Flush(bool allowBlock) { return true; } +void OutputSink::Discard() { + read_ = 0; + write_ = 0; + valid_ = 0; +} + void OutputSink::Drain() { // Avoid small reads if possible. if (valid_ > PRESSURE) { diff --git a/ext/native/net/sinks.h b/ext/native/net/sinks.h index 308c784fb0..fa5546c917 100644 --- a/ext/native/net/sinks.h +++ b/ext/native/net/sinks.h @@ -51,6 +51,7 @@ public: bool Printf(const char *fmt, ...); bool Flush(bool allowBlock = true); + void Discard(); bool Empty(); diff --git a/ext/native/net/websocket_server.cpp b/ext/native/net/websocket_server.cpp index a5fcb81107..397616a058 100644 --- a/ext/native/net/websocket_server.cpp +++ b/ext/native/net/websocket_server.cpp @@ -259,6 +259,8 @@ bool WebSocketServer::Process(float timeout) { // Since select said it was readable, we assume this means disconnect. closeReason_ = WebSocketClose::ABNORMAL; open_ = false; + // Kill any remaining output too. + out_->Discard(); return false; } diff --git a/ext/native/thin3d/GLQueueRunner.cpp b/ext/native/thin3d/GLQueueRunner.cpp index fa423a5780..6f7ccc917d 100644 --- a/ext/native/thin3d/GLQueueRunner.cpp +++ b/ext/native/thin3d/GLQueueRunner.cpp @@ -1085,13 +1085,27 @@ void GLQueueRunner::PerformReadback(const GLRStep &pass) { CHECK_GL_ERROR_IF_DEBUG(); - // Always read back in 8888 format. - const GLuint internalFormat = GL_RGBA; - const GLuint format = GL_RGBA; - const GLuint type = GL_UNSIGNED_BYTE; - const int srcAlignment = 4; + // Always read back in 8888 format for the color aspect. + GLuint internalFormat = GL_RGBA; + GLuint format = GL_RGBA; + GLuint type = GL_UNSIGNED_BYTE; + int srcAlignment = 4; int dstAlignment = (int)DataFormatSizeInBytes(pass.readback.dstFormat); +#ifndef USING_GLES2 + if (pass.readback.aspectMask & GL_DEPTH_BUFFER_BIT) { + internalFormat = GL_DEPTH_COMPONENT; + format = GL_DEPTH_COMPONENT; + type = GL_FLOAT; + srcAlignment = 4; + } else if (pass.readback.aspectMask & GL_STENCIL_BUFFER_BIT) { + internalFormat = GL_STENCIL_INDEX; + format = GL_STENCIL_INDEX; + type = GL_UNSIGNED_BYTE; + srcAlignment = 1; + } +#endif + int pixelStride = pass.readback.srcRect.w; // Apply the correct alignment. glPixelStorei(GL_PACK_ALIGNMENT, srcAlignment); @@ -1102,7 +1116,7 @@ void GLQueueRunner::PerformReadback(const GLRStep &pass) { GLRect2D rect = pass.readback.srcRect; - bool convert = pass.readback.dstFormat != DataFormat::R8G8B8A8_UNORM; + bool convert = internalFormat == GL_RGBA && pass.readback.dstFormat != DataFormat::R8G8B8A8_UNORM; int tempSize = srcAlignment * rect.w * rect.h; int readbackSize = dstAlignment * rect.w * rect.h; diff --git a/ext/native/thin3d/VulkanRenderManager.cpp b/ext/native/thin3d/VulkanRenderManager.cpp index e593442798..48ad0b3d25 100644 --- a/ext/native/thin3d/VulkanRenderManager.cpp +++ b/ext/native/thin3d/VulkanRenderManager.cpp @@ -457,8 +457,7 @@ bool VulkanRenderManager::CopyFramebufferToMemorySync(VKRFramebuffer *src, int a case VK_FORMAT_R8G8B8A8_UNORM: srcFormat = Draw::DataFormat::R8G8B8A8_UNORM; break; default: _assert_(false); } - } - else { + } else { // Backbuffer. if (!(vulkan_->GetSurfaceCapabilities().supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) { ELOG("Copying from backbuffer not supported, can't take screenshots"); diff --git a/ext/native/thin3d/thin3d.cpp b/ext/native/thin3d/thin3d.cpp index 72afd1cfe3..ddc70e2c7e 100644 --- a/ext/native/thin3d/thin3d.cpp +++ b/ext/native/thin3d/thin3d.cpp @@ -421,7 +421,18 @@ void ConvertFromBGRA8888(uint8_t *dst, const uint8_t *src, uint32_t dstStride, u // Must skip stride in the cases below. Some games pack data into the cracks, like MotoGP. const uint32_t *src32 = (const uint32_t *)src; - if (format == Draw::DataFormat::R8G8B8A8_UNORM) { + if (format == Draw::DataFormat::B8G8R8A8_UNORM) { + uint32_t *dst32 = (uint32_t *)dst; + if (src == dst) { + return; + } else { + for (uint32_t y = 0; y < height; ++y) { + memcpy(dst32, src32, width * 4); + src32 += srcStride; + dst32 += dstStride; + } + } + } else if (format == Draw::DataFormat::R8G8B8A8_UNORM) { uint32_t *dst32 = (uint32_t *)dst; for (uint32_t y = 0; y < height; ++y) { ConvertBGRA8888ToRGBA8888(dst32, src32, width); diff --git a/ext/native/thin3d/thin3d.h b/ext/native/thin3d/thin3d.h index 3c95516ce0..73088939fb 100644 --- a/ext/native/thin3d/thin3d.h +++ b/ext/native/thin3d/thin3d.h @@ -567,6 +567,9 @@ public: virtual bool CopyFramebufferToMemorySync(Framebuffer *src, int channelBits, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride) { return false; } + virtual DataFormat PreferredFramebufferReadbackFormat(Framebuffer *src) { + return DataFormat::R8G8B8A8_UNORM; + } // These functions should be self explanatory. // Binding a zero render target means binding the backbuffer. diff --git a/ext/native/thin3d/thin3d_gl.cpp b/ext/native/thin3d/thin3d_gl.cpp index 8fdd744b83..f803aeb069 100644 --- a/ext/native/thin3d/thin3d_gl.cpp +++ b/ext/native/thin3d/thin3d_gl.cpp @@ -720,6 +720,10 @@ static void LogReadPixelsError(GLenum error) { #endif bool OpenGLContext::CopyFramebufferToMemorySync(Framebuffer *src, int channelBits, int x, int y, int w, int h, Draw::DataFormat dataFormat, void *pixels, int pixelStride) { + if (gl_extensions.IsGLES && (channelBits & FB_COLOR_BIT) == 0) { + // Can't readback depth or stencil on GLES. + return false; + } OpenGLFramebuffer *fb = (OpenGLFramebuffer *)src; GLuint aspect = 0; if (channelBits & FB_COLOR_BIT) diff --git a/ext/native/thin3d/thin3d_vulkan.cpp b/ext/native/thin3d/thin3d_vulkan.cpp index 065bf4c7f0..26bf8206c9 100644 --- a/ext/native/thin3d/thin3d_vulkan.cpp +++ b/ext/native/thin3d/thin3d_vulkan.cpp @@ -379,6 +379,7 @@ public: void CopyFramebufferImage(Framebuffer *src, int level, int x, int y, int z, Framebuffer *dst, int dstLevel, int dstX, int dstY, int dstZ, int width, int height, int depth, int channelBits) override; bool BlitFramebuffer(Framebuffer *src, int srcX1, int srcY1, int srcX2, int srcY2, Framebuffer *dst, int dstX1, int dstY1, int dstX2, int dstY2, int channelBits, FBBlitFilter filter) override; bool CopyFramebufferToMemorySync(Framebuffer *src, int channelBits, int x, int y, int w, int h, Draw::DataFormat format, void *pixels, int pixelStride) override; + DataFormat PreferredFramebufferReadbackFormat(Framebuffer *src) override; // These functions should be self explanatory. void BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp) override; @@ -1407,6 +1408,17 @@ bool VKContext::CopyFramebufferToMemorySync(Framebuffer *srcfb, int channelBits, return renderManager_.CopyFramebufferToMemorySync(src ? src->GetFB() : nullptr, aspectMask, x, y, w, h, format, (uint8_t *)pixels, pixelStride); } +DataFormat VKContext::PreferredFramebufferReadbackFormat(Framebuffer *src) { + if (src) { + return DrawContext::PreferredFramebufferReadbackFormat(src); + } + + if (vulkan_->GetSwapchainFormat() == VK_FORMAT_B8G8R8A8_UNORM) { + return Draw::DataFormat::B8G8R8A8_UNORM; + } + return DrawContext::PreferredFramebufferReadbackFormat(src); +} + void VKContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp) { VKFramebuffer *fb = (VKFramebuffer *)fbo; VKRRenderPassAction color = (VKRRenderPassAction)rp.color;