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 \