From 2ee4f2fadb7f350ebb53400eddd653e67085ef6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 14 Aug 2026 10:48:14 +0200 Subject: [PATCH] Debugger: add gpu.displaylist.disasm - a GE display list decoder over the WebSocket API Decoding a GE display list previously meant memory.read-ing the raw bytes and hand-decoding each 32-bit command word against GPU/ge_constants.h's GECommand enum - which is exactly what it took to find this session's actual headline VSH boot finding (a display list that clears the screen once, sets up per-icon render state 6 times, and never issues a single further draw call - see docs/VSHBootInvestigation.md Attempt 22). That manual process is real, repeatable, and error-prone by hand; PPSSPP already has a proper GE disassembler (GPU/GeDisasm.cpp's GeDisassembleOp(), and GPUCommon::DisassembleOpRange() built on top of it) used by the ImGui/Windows GE debugger UI - it just wasn't reachable from the WebSocket API. New Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp exposes gpu->DisassembleOpRange() as gpu.displaylist.disasm, mirroring memory.disasm's own parameter conventions (address+count or address+end, capped at 10000 commands) and compact mode (one string per command, "AAAAAAAA desc", instead of the full {address,cmd,op,desc} object) added in the previous commit. GE command words live in normal guest RAM like CPU code, so - unlike gpu.buffer.* - this doesn't require the CPU/GPU to be paused first, matching memory.disasm's own live-read behavior. Added to all 6 build systems that compile the WebSocket debugger (CMakeLists.txt, Core.vcxproj(.filters), UWP's CoreUWP.vcxproj(.filters), android/jni/Android.mk - libretro doesn't build any Debugger/WebSocket files at all, so nothing to add there). Verified live via PPSSPPHeadless + wsdbg against a real demo ELF: both compact and full-JSON modes correctly decode real GE command words (NOP/ NOP_FF) with no errors. UnitTest.exe all: 49/49 passed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- Core/CMakeLists.txt | 2 + Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/Debugger/WebSocket.cpp | 2 + .../WebSocket/GPUDisasmSubscriber.cpp | 109 ++++++++++++++++++ Core/Debugger/WebSocket/GPUDisasmSubscriber.h | 24 ++++ UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + android/jni/Android.mk | 1 + 9 files changed, 150 insertions(+) create mode 100644 Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp create mode 100644 Core/Debugger/WebSocket/GPUDisasmSubscriber.h diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 7b43453ad7..d0c96c8018 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -307,6 +307,8 @@ add_library(Core STATIC Debugger/WebSocket/ClientConfigSubscriber.h Debugger/WebSocket/GPUBufferSubscriber.cpp Debugger/WebSocket/GPUBufferSubscriber.h + Debugger/WebSocket/GPUDisasmSubscriber.cpp + Debugger/WebSocket/GPUDisasmSubscriber.h Debugger/WebSocket/GPURecordSubscriber.cpp Debugger/WebSocket/GPURecordSubscriber.h Debugger/WebSocket/GPUStatsSubscriber.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 3bcc2b4067..55c9a0d4d7 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -449,6 +449,7 @@ + @@ -987,6 +988,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 10cba764bb..b29f7341b6 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -754,6 +754,9 @@ Debugger\WebSocket + + Debugger\WebSocket + HLE\Libraries @@ -2025,6 +2028,9 @@ Debugger\WebSocket + + Debugger\WebSocket + Core diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp index c3778c20a2..7603ac6192 100644 --- a/Core/Debugger/WebSocket.cpp +++ b/Core/Debugger/WebSocket.cpp @@ -54,6 +54,7 @@ #include "Core/Debugger/WebSocket/DisasmSubscriber.h" #include "Core/Debugger/WebSocket/GameSubscriber.h" #include "Core/Debugger/WebSocket/GPUBufferSubscriber.h" +#include "Core/Debugger/WebSocket/GPUDisasmSubscriber.h" #include "Core/Debugger/WebSocket/GPURecordSubscriber.h" #include "Core/Debugger/WebSocket/GPUStatsSubscriber.h" #include "Core/Debugger/WebSocket/HLESubscriber.h" @@ -71,6 +72,7 @@ static const std::vector subscribers({ &WebSocketDisasmInit, &WebSocketGameInit, &WebSocketGPUBufferInit, + &WebSocketGPUDisasmInit, &WebSocketGPURecordInit, &WebSocketGPUStatsInit, &WebSocketHLEInit, diff --git a/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp b/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp new file mode 100644 index 0000000000..37d050c5f5 --- /dev/null +++ b/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp @@ -0,0 +1,109 @@ +// Copyright (c) 2026- 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 + +#include "Common/StringUtils.h" +#include "Core/Debugger/WebSocket/GPUDisasmSubscriber.h" +#include "Core/Debugger/WebSocket/WebSocketUtils.h" +#include "Core/MemMap.h" +#include "GPU/GPU.h" +#include "GPU/GPUCommon.h" +#include "GPU/Common/GPUDebugInterface.h" + +DebuggerSubscriber *WebSocketGPUDisasmInit(DebuggerEventHandlerMap &map) { + // No need to bind or alloc state, this is all global (the "gpu" global and raw memory reads). + map["gpu.displaylist.disasm"] = &WebSocketGPUDisplayListDisasm; + return nullptr; +} + +// Disassemble a range of GE display list memory into named commands (gpu.displaylist.disasm) +// +// GE command words live in normal guest RAM (same as CPU code), so this decodes memory +// directly rather than requiring the CPU/GPU to be paused first - unlike gpu.buffer.*, which +// reads live host-side render target/texture data and does need CORE_STEPPING_CPU. +// +// Parameters (by count): +// - address: number specifying the start address (a display list address, e.g. from +// sceGe.cpp's "Starting DL execution at ..." log, or dlid via hle - not a CPU code address). +// - count: number of GE command words to decode. +// - compact: optional boolean, default false. See "lines" below. +// +// Parameters (by end address): +// - address: number specifying the start address. +// - end: number which must be after the start address (exclusive). +// - compact: optional boolean, default false. See "lines" below. +// +// Response (same event name): +// - lines: with compact=false (default), array of objects: +// - address: address of this command word. +// - cmd: unsigned integer, the command byte (op >> 24). +// - op: unsigned integer, the raw 32-bit command word. +// - desc: string description of the command (name and decoded parameters). +// with compact=true, array of strings instead, one per command, formatted as +// "AAAAAAAA desc" - meant for skimming a display list by eye instead of parsing full JSON, +// same idea as memory.disasm's own compact mode. +void WebSocketGPUDisplayListDisasm(DebuggerRequest &req) { + if (!gpu) { + return req.Fail("No GPU active (game not booted?)"); + } + if (!Memory::IsActive()) { + return req.Fail("Memory not active"); + } + + // Mirrors memory.disasm's own limit - keeps a client typo (e.g. count=0xFFFFFFFF) from + // blocking the debugger connection for an unreasonable amount of time. + static const uint32_t MAX_RANGE = 10000; + + uint32_t start; + if (!req.ParamU32("address", &start)) + return; + uint32_t end; + uint32_t count = 0; + if (req.ParamU32("count", &count, false, DebuggerParamType::OPTIONAL) && count != 0) { + count = std::min(count, MAX_RANGE); + end = start + count * 4; + } else if (req.ParamU32("end", &end, false, DebuggerParamType::OPTIONAL)) { + end = std::max(start, end); + if (end - start > MAX_RANGE * 4) + end = start + MAX_RANGE * 4; + } else { + return req.Fail("Must specify either 'count' or 'end'"); + } + + bool compact = false; + if (!req.ParamBool("compact", &compact, DebuggerParamType::OPTIONAL)) + return; + + std::vector ops = gpu->DisassembleOpRange(start, end); + + JsonWriter &json = req.Respond(); + json.pushArray("lines"); + for (const GPUDebugOp &op : ops) { + if (compact) { + json.writeString(StringFromFormat("%08x %s", op.pc, op.desc.c_str())); + } else { + json.pushDict(); + json.writeUint("address", op.pc); + json.writeUint("cmd", op.cmd); + json.writeUint("op", op.op); + json.writeString("desc", op.desc); + json.pop(); + } + } + json.pop(); +} diff --git a/Core/Debugger/WebSocket/GPUDisasmSubscriber.h b/Core/Debugger/WebSocket/GPUDisasmSubscriber.h new file mode 100644 index 0000000000..9e955dd262 --- /dev/null +++ b/Core/Debugger/WebSocket/GPUDisasmSubscriber.h @@ -0,0 +1,24 @@ +// Copyright (c) 2026- 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" + +DebuggerSubscriber *WebSocketGPUDisasmInit(DebuggerEventHandlerMap &map); + +void WebSocketGPUDisplayListDisasm(DebuggerRequest &req); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index baa6e53fb5..c04ada6bc4 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -108,6 +108,7 @@ + @@ -382,6 +383,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 87972f19a5..5112630af1 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -21,6 +21,7 @@ + @@ -438,6 +439,7 @@ + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 204f05032f..34af103035 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -664,6 +664,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/Debugger/WebSocket/GameBroadcaster.cpp \ $(SRC)/Core/Debugger/WebSocket/GameSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp \ + $(SRC)/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/GPURecordSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp \ $(SRC)/Core/Debugger/WebSocket/HLESubscriber.cpp \