diff --git a/Common/Data/Format/JSONWriter.h b/Common/Data/Format/JSONWriter.h
index 0938c4e809..72a15ada4b 100644
--- a/Common/Data/Format/JSONWriter.h
+++ b/Common/Data/Format/JSONWriter.h
@@ -57,6 +57,32 @@ public:
return result;
}
+ // RAII helpers: push in the constructor, pop in the destructor. Prefer these over manually
+ // paired pushDict()/pushArray() + pop() calls - a forgotten or early-returned pop() silently
+ // produces malformed JSON (extra or missing closing brace/bracket) rather than a compile or
+ // even runtime error, and that gets easy to miss once nesting goes a few levels deep (e.g.
+ // an array of per-item dicts, each with their own nested array).
+ class DictScope {
+ public:
+ explicit DictScope(JsonWriter &w) : w_(w) { w_.pushDict(); }
+ DictScope(JsonWriter &w, const std::string &name) : w_(w) { w_.pushDict(name); }
+ ~DictScope() { w_.pop(); }
+ DictScope(const DictScope &) = delete;
+ DictScope &operator=(const DictScope &) = delete;
+ private:
+ JsonWriter &w_;
+ };
+ class ArrayScope {
+ public:
+ explicit ArrayScope(JsonWriter &w) : w_(w) { w_.pushArray(); }
+ ArrayScope(JsonWriter &w, const std::string &name) : w_(w) { w_.pushArray(name); }
+ ~ArrayScope() { w_.pop(); }
+ ArrayScope(const ArrayScope &) = delete;
+ ArrayScope &operator=(const ArrayScope &) = delete;
+ private:
+ JsonWriter &w_;
+ };
+
enum {
NORMAL = 0,
PRETTY = 1,
diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index b7c36137d9..f2106e43a5 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -313,6 +313,8 @@ add_library(Core STATIC
Debugger/WebSocket/GPURecordSubscriber.h
Debugger/WebSocket/GPUStatsSubscriber.cpp
Debugger/WebSocket/GPUStatsSubscriber.h
+ Debugger/WebSocket/HLEKernelObjectSubscriber.cpp
+ Debugger/WebSocket/HLEKernelObjectSubscriber.h
Debugger/WebSocket/HLESubscriber.cpp
Debugger/WebSocket/HLESubscriber.h
Debugger/WebSocket/InputBroadcaster.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index a9f98de6a3..2b4c3acb22 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -452,6 +452,7 @@
+
@@ -992,6 +993,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index e47b6ce596..83ea1b310b 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -754,6 +754,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
@@ -2031,6 +2034,9 @@
Debugger\WebSocket
+
+ Debugger\WebSocket
+
Debugger\WebSocket
diff --git a/Core/Debugger/WebSocket.cpp b/Core/Debugger/WebSocket.cpp
index abbdcded06..90673cddce 100644
--- a/Core/Debugger/WebSocket.cpp
+++ b/Core/Debugger/WebSocket.cpp
@@ -61,6 +61,7 @@
#include "Core/Debugger/WebSocket/GPUDisasmSubscriber.h"
#include "Core/Debugger/WebSocket/GPURecordSubscriber.h"
#include "Core/Debugger/WebSocket/GPUStatsSubscriber.h"
+#include "Core/Debugger/WebSocket/HLEKernelObjectSubscriber.h"
#include "Core/Debugger/WebSocket/HLESubscriber.h"
#include "Core/Debugger/WebSocket/InputSubscriber.h"
#include "Core/Debugger/WebSocket/LogConfigSubscriber.h"
@@ -80,6 +81,7 @@ static const std::vector subscribers({
&WebSocketGPUDisasmInit,
&WebSocketGPURecordInit,
&WebSocketGPUStatsInit,
+ &WebSocketHLEKernelObjectInit,
&WebSocketHLEInit,
&WebSocketInputInit,
&WebSocketLogConfigInit,
diff --git a/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.cpp b/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.cpp
new file mode 100644
index 0000000000..530e26c399
--- /dev/null
+++ b/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.cpp
@@ -0,0 +1,451 @@
+// 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/.
+
+// Introspection for kernel synchronization objects (event flags, mutexes, semaphores, message
+// pipes, callbacks) plus a generic overview across every kernel object type at once. These read
+// straight off the live KernelObject-derived classes (exposed in their respective sceKernel*.h
+// headers for exactly this purpose) rather than going through a kernel-side accessor function -
+// deliberately: nothing here ever mutates kernel state, only reads already-public fields, so
+// there's no reason to route through the owning module. That also means "waiting threads" lists
+// below are shown exactly as the kernel currently has them - a thread that's actually done
+// waiting but hasn't been pruned by the next real syscall on that object yet may still appear.
+
+#include "Core/Core.h"
+#include "Core/HLE/sceKernel.h"
+#include "Core/HLE/sceKernelEventFlag.h"
+#include "Core/HLE/sceKernelMutex.h"
+#include "Core/HLE/sceKernelSemaphore.h"
+#include "Core/HLE/sceKernelMsgPipe.h"
+#include "Core/HLE/sceKernelThread.h"
+#include "Core/Debugger/WebSocket/HLEKernelObjectSubscriber.h"
+#include "Core/Debugger/WebSocket/WebSocketUtils.h"
+
+DebuggerSubscriber *WebSocketHLEKernelObjectInit(DebuggerEventHandlerMap &map) {
+ map["hle.object.list"] = &WebSocketHLEObjectList;
+
+ map["hle.eventflag.list"] = &WebSocketHLEEventFlagList;
+ map["hle.eventflag.info"] = &WebSocketHLEEventFlagInfo;
+ map["hle.mutex.list"] = &WebSocketHLEMutexList;
+ map["hle.mutex.info"] = &WebSocketHLEMutexInfo;
+ map["hle.semaphore.list"] = &WebSocketHLESemaphoreList;
+ map["hle.semaphore.info"] = &WebSocketHLESemaphoreInfo;
+ map["hle.msgpipe.list"] = &WebSocketHLEMsgPipeList;
+ map["hle.msgpipe.info"] = &WebSocketHLEMsgPipeInfo;
+ map["hle.callback.list"] = &WebSocketHLECallbackList;
+ map["hle.callback.info"] = &WebSocketHLECallbackInfo;
+ return nullptr;
+}
+
+// Resolves uid to a T*, or fails the request and returns null (mismatched type counts as not
+// found, same as the HLE syscalls themselves treat it - see KernelObjectPool::Get().)
+template
+static T *RequireKernelObject(DebuggerRequest &req, uint32_t uid) {
+ u32 error;
+ T *obj = kernelObjects.Get((SceUID)uid, error);
+ if (!obj) {
+ req.Fail("No matching object found for that uid");
+ return nullptr;
+ }
+ return obj;
+}
+
+// Writes a plain {threadId} entry for each id in a wait list that's just SceUIDs (Mutex,
+// Semaphore) - already inside an array context (ArrayScope) opened by the caller.
+static void WriteThreadIdWaitList(JsonWriter &json, const std::vector &threadIDs) {
+ for (SceUID threadID : threadIDs) {
+ JsonWriter::DictScope d(json);
+ json.writeInt("threadId", threadID);
+ }
+}
+
+// List every live kernel object of every type (hle.object.list)
+//
+// A coarse overview across all kernel object kinds at once (threads, modules, semaphores, event
+// flags, mutexes, msgpipes, callbacks, ...) - for the sync primitive types also covered by
+// hle.eventflag.*/hle.mutex.*/etc., use those for full detail; this just gives uid/type/name/a
+// one-line summary for everything, useful for figuring out what's alive before drilling in.
+//
+// Parameters:
+// - type: optional string, only include objects whose type name equals this (as returned in
+// each entry's own 'type' field - e.g. "EventFlag", "Mutex", "Semaphore", "MsgPipe",
+// "CallBack", "Thread", "Module", ...; case sensitive, matches KernelObject::GetTypeName().)
+//
+// Response (same event name):
+// - objects: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle.
+// - type: string type name.
+// - name: string name the object was created with.
+// - quickInfo: string, a short type-specific one-line summary (whatever that type finds
+// most useful - not machine-parseable, for human/log-style display only.)
+void WebSocketHLEObjectList(DebuggerRequest &req) {
+ std::string typeFilter;
+ if (!req.ParamString("type", &typeFilter, DebuggerParamType::OPTIONAL))
+ return;
+
+ // Route the actual kernel object reads to the CPU thread instead of poking at them directly
+ // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope objects(json, "objects");
+ kernelObjects.IterateAll([&](int uid, KernelObject *obj) -> bool {
+ if (!typeFilter.empty() && typeFilter != obj->GetTypeName())
+ return true;
+
+ char quickInfo[256];
+ obj->GetQuickInfo(quickInfo, sizeof(quickInfo));
+
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("type", obj->GetTypeName());
+ json.writeString("name", obj->GetName());
+ json.writeString("quickInfo", quickInfo);
+ return true;
+ });
+ });
+}
+
+// List all event flags (hle.eventflag.list)
+//
+// No parameters.
+//
+// Response (same event name):
+// - eventFlags: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle - pass to hle.eventflag.info for detail.
+// - name: string name.
+// - currentPattern: unsigned integer, the flag's current bit pattern.
+// - numWaitThreads: integer, how many threads are currently waiting on it.
+void WebSocketHLEEventFlagList(DebuggerRequest &req) {
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope arr(json, "eventFlags");
+ kernelObjects.Iterate([&](int uid, EventFlag *e) -> bool {
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("name", e->nef.name);
+ json.writeUint("currentPattern", e->nef.currentPattern);
+ json.writeInt("numWaitThreads", (int)e->waitingThreads.size());
+ return true;
+ });
+ });
+}
+
+// Get full detail for one event flag (hle.eventflag.info)
+//
+// Parameters:
+// - uid: unsigned integer kernel object handle (as returned by hle.eventflag.list.)
+//
+// Response (same event name):
+// - name: string name.
+// - attr: unsigned integer creation attributes.
+// - initPattern: unsigned integer, the bit pattern the flag was created with.
+// - currentPattern: unsigned integer, the flag's current bit pattern.
+// - waitingThreads: array of objects, each with properties:
+// - threadId: integer thread uid.
+// - bits: unsigned integer, the bit pattern this thread is waiting to match.
+// - matchAny: boolean, true if any one bit matches (OR), false if all bits must (AND.)
+// - clear: boolean, true if matched bits get cleared from the pattern on match.
+// - clearAll: boolean, true if the whole pattern gets cleared to 0 on match.
+void WebSocketHLEEventFlagInfo(DebuggerRequest &req) {
+ uint32_t uid;
+ if (!req.ParamU32("uid", &uid))
+ return;
+
+ Core_RunOnCPUThread([&] {
+ EventFlag *e = RequireKernelObject(req, uid);
+ if (!e)
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.writeString("name", e->nef.name);
+ json.writeUint("attr", e->nef.attr);
+ json.writeUint("initPattern", e->nef.initPattern);
+ json.writeUint("currentPattern", e->nef.currentPattern);
+ {
+ JsonWriter::ArrayScope waiters(json, "waitingThreads");
+ for (const EventFlagTh &t : e->waitingThreads) {
+ JsonWriter::DictScope d(json);
+ json.writeInt("threadId", t.threadID);
+ json.writeUint("bits", t.bits);
+ // PSP_EVENT_WAIT* bits, see sceKernelWaitEventFlag()'s 'wait' parameter -
+ // 0x01=OR, 0x10=clear-all-on-match, 0x20=clear-matched-on-match.
+ json.writeBool("matchAny", (t.wait & 0x01) != 0);
+ json.writeBool("clear", (t.wait & 0x20) != 0);
+ json.writeBool("clearAll", (t.wait & 0x10) != 0);
+ }
+ }
+ });
+}
+
+// List all mutexes (hle.mutex.list)
+//
+// No parameters.
+//
+// Response (same event name):
+// - mutexes: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle - pass to hle.mutex.info for detail.
+// - name: string name.
+// - lockThread: integer thread uid currently holding the lock, or -1 if unlocked.
+// - lockLevel: integer recursion depth (only >1 possible if created with the recursive attr.)
+// - numWaitThreads: integer, how many threads are currently waiting to lock it.
+void WebSocketHLEMutexList(DebuggerRequest &req) {
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope arr(json, "mutexes");
+ kernelObjects.Iterate([&](int uid, PSPMutex *m) -> bool {
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("name", m->nm.name);
+ json.writeInt("lockThread", m->nm.lockThread);
+ json.writeInt("lockLevel", m->nm.lockLevel);
+ json.writeInt("numWaitThreads", (int)m->waitingThreads.size());
+ return true;
+ });
+ });
+}
+
+// Get full detail for one mutex (hle.mutex.info)
+//
+// Parameters:
+// - uid: unsigned integer kernel object handle (as returned by hle.mutex.list.)
+//
+// Response (same event name):
+// - name: string name.
+// - attr: unsigned integer creation attributes.
+// - initialCount: integer lock count the mutex was created with.
+// - lockThread: integer thread uid currently holding the lock, or -1 if unlocked.
+// - lockLevel: integer recursion depth.
+// - waitingThreads: array of objects, each with properties:
+// - threadId: integer thread uid waiting to lock this mutex.
+void WebSocketHLEMutexInfo(DebuggerRequest &req) {
+ uint32_t uid;
+ if (!req.ParamU32("uid", &uid))
+ return;
+
+ Core_RunOnCPUThread([&] {
+ PSPMutex *m = RequireKernelObject(req, uid);
+ if (!m)
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.writeString("name", m->nm.name);
+ json.writeUint("attr", m->nm.attr);
+ json.writeInt("initialCount", m->nm.initialCount);
+ json.writeInt("lockThread", m->nm.lockThread);
+ json.writeInt("lockLevel", m->nm.lockLevel);
+ {
+ JsonWriter::ArrayScope waiters(json, "waitingThreads");
+ WriteThreadIdWaitList(json, m->waitingThreads);
+ }
+ });
+}
+
+// List all semaphores (hle.semaphore.list)
+//
+// No parameters.
+//
+// Response (same event name):
+// - semaphores: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle - pass to hle.semaphore.info for detail.
+// - name: string name.
+// - currentCount: integer current count.
+// - numWaitThreads: integer, how many threads are currently waiting on it.
+void WebSocketHLESemaphoreList(DebuggerRequest &req) {
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope arr(json, "semaphores");
+ kernelObjects.Iterate([&](int uid, PSPSemaphore *sema) -> bool {
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("name", sema->ns.name);
+ json.writeInt("currentCount", sema->ns.currentCount);
+ json.writeInt("numWaitThreads", (int)sema->waitingThreads.size());
+ return true;
+ });
+ });
+}
+
+// Get full detail for one semaphore (hle.semaphore.info)
+//
+// Parameters:
+// - uid: unsigned integer kernel object handle (as returned by hle.semaphore.list.)
+//
+// Response (same event name):
+// - name: string name.
+// - attr: unsigned integer creation attributes.
+// - initCount: integer count the semaphore was created with.
+// - currentCount: integer current count.
+// - maxCount: integer maximum count.
+// - waitingThreads: array of objects, each with properties:
+// - threadId: integer thread uid waiting on this semaphore.
+void WebSocketHLESemaphoreInfo(DebuggerRequest &req) {
+ uint32_t uid;
+ if (!req.ParamU32("uid", &uid))
+ return;
+
+ Core_RunOnCPUThread([&] {
+ PSPSemaphore *sema = RequireKernelObject(req, uid);
+ if (!sema)
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.writeString("name", sema->ns.name);
+ json.writeUint("attr", sema->ns.attr);
+ json.writeInt("initCount", sema->ns.initCount);
+ json.writeInt("currentCount", sema->ns.currentCount);
+ json.writeInt("maxCount", sema->ns.maxCount);
+ {
+ JsonWriter::ArrayScope waiters(json, "waitingThreads");
+ WriteThreadIdWaitList(json, sema->waitingThreads);
+ }
+ });
+}
+
+// List all message pipes (hle.msgpipe.list)
+//
+// No parameters.
+//
+// Response (same event name):
+// - msgPipes: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle - pass to hle.msgpipe.info for detail.
+// - name: string name.
+// - freeSize: integer free bytes currently in the pipe's buffer.
+// - numSendWaitThreads: integer threads currently blocked trying to send.
+// - numReceiveWaitThreads: integer threads currently blocked trying to receive.
+void WebSocketHLEMsgPipeList(DebuggerRequest &req) {
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope arr(json, "msgPipes");
+ kernelObjects.Iterate([&](int uid, MsgPipe *m) -> bool {
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("name", m->nmp.name);
+ json.writeInt("freeSize", m->nmp.freeSize);
+ json.writeInt("numSendWaitThreads", (int)m->sendWaitingThreads.size());
+ json.writeInt("numReceiveWaitThreads", (int)m->receiveWaitingThreads.size());
+ return true;
+ });
+ });
+}
+
+static void WriteMsgPipeWaitList(JsonWriter &json, const std::vector &threads) {
+ for (const MsgPipeWaitingThread &t : threads) {
+ JsonWriter::DictScope d(json);
+ json.writeInt("threadId", t.threadID);
+ json.writeUint("bufSize", t.bufSize);
+ json.writeUint("freeSize", t.freeSize);
+ json.writeInt("waitMode", t.waitMode);
+ }
+}
+
+// Get full detail for one message pipe (hle.msgpipe.info)
+//
+// Parameters:
+// - uid: unsigned integer kernel object handle (as returned by hle.msgpipe.list.)
+//
+// Response (same event name):
+// - name: string name.
+// - attr: unsigned integer creation attributes.
+// - bufSize: integer total buffer size in bytes.
+// - freeSize: integer free bytes currently in the buffer.
+// - sendWaitingThreads / receiveWaitingThreads: array of objects, each with properties:
+// - threadId: integer thread uid.
+// - bufSize: unsigned integer size of this thread's own send/receive buffer.
+// - freeSize: unsigned integer bytes not yet transferred for this thread.
+// - waitMode: integer, 0 = wait for the full transfer (SCE_KERNEL_MPW_FULL), 1 = accept a
+// partial transfer (SCE_KERNEL_MPW_ASAP.)
+void WebSocketHLEMsgPipeInfo(DebuggerRequest &req) {
+ uint32_t uid;
+ if (!req.ParamU32("uid", &uid))
+ return;
+
+ Core_RunOnCPUThread([&] {
+ MsgPipe *m = RequireKernelObject(req, uid);
+ if (!m)
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.writeString("name", m->nmp.name);
+ json.writeUint("attr", m->nmp.attr);
+ json.writeInt("bufSize", m->nmp.bufSize);
+ json.writeInt("freeSize", m->nmp.freeSize);
+ {
+ JsonWriter::ArrayScope waiters(json, "sendWaitingThreads");
+ WriteMsgPipeWaitList(json, m->sendWaitingThreads);
+ }
+ {
+ JsonWriter::ArrayScope waiters(json, "receiveWaitingThreads");
+ WriteMsgPipeWaitList(json, m->receiveWaitingThreads);
+ }
+ });
+}
+
+// List all callbacks (hle.callback.list)
+//
+// No parameters.
+//
+// Response (same event name):
+// - callbacks: array of objects, each with properties:
+// - uid: unsigned integer kernel object handle - pass to hle.callback.info for detail.
+// - name: string name.
+// - threadId: integer uid of the thread this callback belongs to.
+// - notifyCount: integer, incremented each time the callback is queued to run.
+void WebSocketHLECallbackList(DebuggerRequest &req) {
+ Core_RunOnCPUThread([&] {
+ JsonWriter &json = req.Respond();
+ JsonWriter::ArrayScope arr(json, "callbacks");
+ kernelObjects.Iterate([&](int uid, PSPCallback *c) -> bool {
+ JsonWriter::DictScope d(json);
+ json.writeUint("uid", uid);
+ json.writeString("name", c->nc.name);
+ json.writeInt("threadId", c->nc.threadId);
+ json.writeInt("notifyCount", c->nc.notifyCount);
+ return true;
+ });
+ });
+}
+
+// Get full detail for one callback (hle.callback.info)
+//
+// Parameters:
+// - uid: unsigned integer kernel object handle (as returned by hle.callback.list.)
+//
+// Response (same event name):
+// - name: string name.
+// - threadId: integer uid of the thread this callback belongs to.
+// - entrypoint: unsigned integer address of the callback function.
+// - commonArgument: unsigned integer, the 'common' argument passed to every invocation.
+// - notifyCount: integer, incremented each time the callback is queued to run.
+// - notifyArg: integer, the argument from the most recent notification.
+void WebSocketHLECallbackInfo(DebuggerRequest &req) {
+ uint32_t uid;
+ if (!req.ParamU32("uid", &uid))
+ return;
+
+ Core_RunOnCPUThread([&] {
+ PSPCallback *c = RequireKernelObject(req, uid);
+ if (!c)
+ return;
+
+ JsonWriter &json = req.Respond();
+ json.writeString("name", c->nc.name);
+ json.writeInt("threadId", c->nc.threadId);
+ json.writeUint("entrypoint", c->nc.entrypoint);
+ json.writeUint("commonArgument", c->nc.commonArgument);
+ json.writeInt("notifyCount", c->nc.notifyCount);
+ json.writeInt("notifyArg", c->nc.notifyArg);
+ });
+}
diff --git a/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.h b/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.h
new file mode 100644
index 0000000000..4d3c9fa834
--- /dev/null
+++ b/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.h
@@ -0,0 +1,35 @@
+// 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 *WebSocketHLEKernelObjectInit(DebuggerEventHandlerMap &map);
+
+void WebSocketHLEObjectList(DebuggerRequest &req);
+
+void WebSocketHLEEventFlagList(DebuggerRequest &req);
+void WebSocketHLEEventFlagInfo(DebuggerRequest &req);
+void WebSocketHLEMutexList(DebuggerRequest &req);
+void WebSocketHLEMutexInfo(DebuggerRequest &req);
+void WebSocketHLESemaphoreList(DebuggerRequest &req);
+void WebSocketHLESemaphoreInfo(DebuggerRequest &req);
+void WebSocketHLEMsgPipeList(DebuggerRequest &req);
+void WebSocketHLEMsgPipeInfo(DebuggerRequest &req);
+void WebSocketHLECallbackList(DebuggerRequest &req);
+void WebSocketHLECallbackInfo(DebuggerRequest &req);
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index 83fc21a149..386a560f9f 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -111,6 +111,7 @@
+
@@ -387,6 +388,7 @@
+
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters
index fda2d43b13..7d65f6d17d 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj.filters
+++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters
@@ -24,6 +24,7 @@
+
@@ -443,6 +444,7 @@
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index d46c49a8f9..9847580b1d 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -667,6 +667,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/GPURecordSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/GPUStatsSubscriber.cpp \
+ $(SRC)/Core/Debugger/WebSocket/HLEKernelObjectSubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/HLESubscriber.cpp \
$(SRC)/Core/Debugger/WebSocket/InputBroadcaster.cpp \
$(SRC)/Core/Debugger/WebSocket/InputSubscriber.cpp \
diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md
index 1062d414a8..0f304b391d 100644
--- a/docs/WebSocketDebugger.md
+++ b/docs/WebSocketDebugger.md
@@ -117,6 +117,7 @@ file - this is just an index.
| GE display list disassembly | `gpu.displaylist.disasm` - like `memory.disasm` but for GE command words (`CLEARMODE`, `PRIM`, etc.) instead of CPU instructions; also supports `compact=true` | `GPUDisasmSubscriber.cpp` |
| HLE | `hle.thread.list/wake/stop`, `hle.func.list/add/remove/removeRange/rename/scan`, `hle.module.list`, `hle.module.saveSymbols/loadSymbols` (save/load one module's symbols to/from its standard `PSP/SYSTEM/SYMBOLS/_.ppsym` file, shared across any game that loads the same module - see `SymbolMap::GetModuleSymbolsPath`), `hle.backtrace` | `HLESubscriber.cpp` |
| Data symbols | `hle.data.list/add/remove/rename` - label discovered data (structs, tables, buffers) with a name/type, same idea as `hle.func.*` but for `ST_DATA` symbols | `HLESubscriber.cpp` |
+| Kernel objects | `hle.object.list` (every live kernel object of every type at once, with an optional `type` filter - uid/type/name/one-line summary only); `hle.eventflag.list/info`, `hle.mutex.list/info`, `hle.semaphore.list/info`, `hle.msgpipe.list/info`, `hle.callback.list/info` (per-type full detail, including waiting-thread lists) - all read-only, never mutate kernel state | `HLEKernelObjectSubscriber.cpp` |
| GPU stats | `gpu.stats.get`, `gpu.stats.feed` | `GPUStatsSubscriber.cpp` |
| GPU recording | `gpu.record.dump` | `GPURecordSubscriber.cpp` |
| GPU buffers | `gpu.buffer.screenshot`, `gpu.buffer.renderColor/renderDepth/renderStencil`, `gpu.buffer.texture`, `gpu.buffer.clut` | `GPUBufferSubscriber.cpp` |