ppsspp_read8
Read an unsigned 8-bit byte from PSP memory at a given physical address. Returns address and value in decimal and hex for single-byte fields like status flags and counters.
Instructions
PURPOSE: Read an unsigned 8-bit byte from PSP memory at the given physical address. USAGE: Use for single-byte status flags, counters, and 8-bit fields. For 16/32-bit values use ppsspp_read16/read32 (one call instead of multi-byte assembly); for spans use ppsspp_read_range. BEHAVIOR: No side effects — pure read. Returns an error if the address isn't a valid PSP memory address (PPSSPP validates against the PSP's mapped regions). RETURNS: Single line 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | PSP physical address. PSP memory layout: user RAM starts at 0x08800000 (or 0x08000000 — varies by firmware allocation), kernel RAM at 0x08000000-0x087FFFFF, VRAM at 0x04000000-0x041FFFFF, scratchpad at 0x00010000-0x00013FFF, hardware regs at 0xBC000000+. Most game state lives in user RAM. Note PPSSPP may also accept 0x88xxxxxx kernel-mode mirrors of the same physical memory. |
Implementation Reference
- src/tools.ts:434-437 (handler)Handler for the ppsspp_read8 tool. It calls PPSSPP's 'memory.read_u8' via the WebSocket client and formats the result as 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.
case "ppsspp_read8": { const r = await pp.call<{ value: number }>("memory.read_u8", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } - src/tools.ts:69-76 (schema)Input schema for ppsspp_read8: requires an integer 'address' param with a minimum of 0.
inputSchema: { type: "object", required: ["address"], properties: { address: { type: "integer", minimum: 0, description: ADDRESS_PARAM_DESC }, }, additionalProperties: false, }, - src/tools.ts:62-77 (registration)Tool registration entry for ppsspp_read8 in the TOOLS array, including its name and description.
{ name: "ppsspp_read8", description: "PURPOSE: Read an unsigned 8-bit byte from PSP memory at the given physical address. " + "USAGE: Use for single-byte status flags, counters, and 8-bit fields. For 16/32-bit values use ppsspp_read16/read32 (one call instead of multi-byte assembly); for spans use ppsspp_read_range. " + "BEHAVIOR: No side effects — pure read. Returns an error if the address isn't a valid PSP memory address (PPSSPP validates against the PSP's mapped regions). " + "RETURNS: Single line 'ADDR_HEX: VAL_DEC (0xVAL_HEX)'.", inputSchema: { type: "object", required: ["address"], properties: { address: { type: "integer", minimum: 0, description: ADDRESS_PARAM_DESC }, }, additionalProperties: false, }, }, - src/tools.ts:405-613 (registration)The registerTools function that registers all tools (including ppsspp_read8) as MCP request handlers on the server. The switch-case dispatches the tool name to the handler.
export function registerTools(server: Server, pp: PpssppClient): void { server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); server.setRequestHandler(CallToolRequestSchema, async (req) => { const { name, arguments: args = {} } = req.params; const p = args as Record<string, unknown>; const a = () => p.address as number; switch (name) { case "ppsspp_ping": { const r = await pp.call<{ version?: string; name?: string }>("version"); return ok(`pong (${r.name ?? "PPSSPP"} ${r.version ?? "(unknown version)"})`); } case "ppsspp_get_info": { const status = await pp.call<{ game?: { id?: string; title?: string; version?: string } | null; paused?: boolean; stepping?: boolean }>("game.status"); const lines: string[] = []; if (status.game) { lines.push(`Title: ${status.game.title ?? "(unavailable)"}`); lines.push(`Disc ID: ${status.game.id ?? "(unavailable)"}`); lines.push(`Version: ${status.game.version ?? "(unavailable)"}`); } else { lines.push("No game loaded."); } const state = status.stepping ? "stepping (paused)" : status.paused ? "paused" : "running"; lines.push(`State: ${state}`); return ok(lines.join("\n")); } case "ppsspp_read8": { const r = await pp.call<{ value: number }>("memory.read_u8", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read16": { const r = await pp.call<{ value: number }>("memory.read_u16", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read32": { const r = await pp.call<{ value: number }>("memory.read_u32", { address: a() }); return ok(`${addrHex(a())}: ${fmtHex(r.value)}`); } case "ppsspp_read_range": { const r = await pp.call<{ base64: string }>("memory.read", { address: a(), size: p.size }); const bytes = Buffer.from(r.base64 ?? "", "base64"); const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0").toUpperCase()).join(" "); return ok(`${addrHex(a())} [${bytes.length} bytes]:\n${hex}`); } case "ppsspp_read_string": { const r = await pp.call<{ value: string }>("memory.readString", { address: a(), type: "utf-8" }); return ok(`${addrHex(a())}: ${JSON.stringify(r.value ?? "")}`); } case "ppsspp_write8": { await pp.call("memory.write_u8", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write16": { await pp.call("memory.write_u16", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write32": { await pp.call("memory.write_u32", { address: a(), value: p.value }); return ok(`Wrote ${fmtHex(p.value)} → ${addrHex(a())}`); } case "ppsspp_write_range": { const bytes = Buffer.from(p.bytes as number[]); const base64 = bytes.toString("base64"); await pp.call("memory.write", { address: a(), base64 }); return ok(`Wrote ${bytes.length} bytes → ${addrHex(a())}`); } case "ppsspp_press_buttons": { await pp.call("input.buttons.send", { buttons: p.buttons }); const pressed = Object.entries(p.buttons as Record<string, boolean>) .filter(([, v]) => v).map(([k]) => k); return ok(`Set buttons: ${pressed.length ? pressed.join("+") : "(all released)"}`); } case "ppsspp_press_button": { await pp.call("input.buttons.press", { button: p.button, duration: p.duration ?? 1 }); return ok(`Pressed ${p.button} for ${p.duration ?? 1} frames (auto-released)`); } case "ppsspp_send_analog": { await pp.call("input.analog.send", { stick: p.stick, x: p.x, y: p.y }); return ok(`Set analog stick ${p.stick} to (${p.x}, ${p.y})`); } case "ppsspp_pause": { // cpu.stepping is fire-and-forget per PPSSPP source ("No immediate // response. Once CPU is stepping, a 'cpu.stepping' event will be // sent."). Send it, then poll cpu.status until stepping=true. await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); return ok("Emulation paused"); } case "ppsspp_resume": { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false); return ok("Emulation resumed"); } case "ppsspp_step": { const r = await pp.call<{ pc?: number }>("cpu.stepInto"); return ok(`Stepped one instruction. PC: ${r.pc !== undefined ? addrHex(r.pc) : "(unknown)"}`); } case "ppsspp_reset": { await pp.call("game.reset"); return ok("Game reset"); } case "ppsspp_screenshot": { // PPSSPP's gpu.buffer.* events all require CORE_STEPPING_CPU (or GPU // stepping) state — they fail with "Neither CPU or GPU is stepping" // otherwise. We transparently pause→capture→resume so callers can // screenshot any time without managing pause state. If the emulator // was already paused, we leave it paused. // // source='render' (default) uses gpu.buffer.renderColor → reads the // active GPU render target. Safer: GPU_GetCurrentFramebuffer hits a // different code path than the crash-prone GPU_GetOutputFramebuffer. // // source='output' uses gpu.buffer.screenshot → reads the final // composited output (what's on screen, post scaling/shaders). Can // CRASH PPSSPP on some games: upstream has an `_assert_(buf != nullptr)` // after GPU_GetOutputFramebuffer that fires when the function returns // true with a null buffer (observed on some homebrew). We can't catch // a process abort from outside, but v0.1.2's auto-reconnect means MCP // recovers when PPSSPP is relaunched. const source = (p.source as string | undefined) ?? "render"; const event = source === "output" ? "gpu.buffer.screenshot" : "gpu.buffer.renderColor"; const statusBefore = await pp.call<{ stepping?: boolean; paused?: boolean }>("cpu.status"); const wasStepping = !!statusBefore.stepping; if (!wasStepping) { await pp.fireAndForget("cpu.stepping"); await pp.waitForState((s) => s.stepping === true); } try { // type: "base64" returns the raw base64 payload; the default "uri" // returns a "data:image/png;base64,..." prefix which we'd have to strip. const r = await pp.call<{ base64?: string; uri?: string }>(event, { type: "base64" }); let b64 = r.base64; if (!b64 && r.uri) { // Belt-and-suspenders: if PPSSPP returned a URI anyway, strip the prefix. const m = /^data:image\/png;base64,(.*)$/.exec(r.uri); if (m) b64 = m[1]; } if (!b64) { throw new Error(`PPSSPP did not return screenshot data from ${event} (no game loaded, or framebuffer not readable?)`); } return { content: [ { type: "text" as const, text: `Screenshot captured (source: ${source}, event: ${event}).` }, { type: "image" as const, data: b64, mimeType: "image/png" }, ], }; } finally { if (!wasStepping) { try { await pp.fireAndForget("cpu.resume"); await pp.waitForState((s) => s.stepping === false, { timeoutMs: 2000 }); } catch { /* best-effort */ } } } } case "ppsspp_get_registers": { // PPSSPP's cpu.getAllRegs returns categories with PARALLEL arrays: // { categories: [{ name, registerNames: [...], uintValues: [...], floatValues: [...] }] } // Not an array of {name, value} objects as I first assumed. const r = await pp.call<{ categories?: Array<{ name: string; registerNames?: string[]; uintValues?: number[]; floatValues?: string[]; }>; }>("cpu.getAllRegs"); const lines: string[] = []; for (const cat of r.categories ?? []) { lines.push(`── ${cat.name} ──`); const names = cat.registerNames ?? []; const vals = cat.uintValues ?? []; for (let i = 0; i < Math.max(names.length, vals.length); i++) { const nm = names[i] ?? `r${i}`; const v = vals[i]; lines.push(` ${nm.padEnd(8)} = ${v !== undefined ? addrHex(v) : "(unavailable)"}`); } } return ok(lines.join("\n") || "(no registers returned)"); } case "ppsspp_breakpoint_add": { await pp.call("cpu.breakpoint.add", { address: a() }); return ok(`Breakpoint added at ${addrHex(a())}`); } case "ppsspp_breakpoint_remove": { await pp.call("cpu.breakpoint.remove", { address: a() }); return ok(`Breakpoint removed at ${addrHex(a())}`); } case "ppsspp_breakpoint_list": { const r = await pp.call<{ breakpoints?: Array<{ address: number; enabled?: boolean; condition?: string }> }>("cpu.breakpoint.list"); const bps = r.breakpoints ?? []; if (bps.length === 0) return ok("No breakpoints set."); const lines = bps.map((b) => ` ${addrHex(b.address)} ${b.enabled === false ? "(disabled)" : ""}${b.condition ? ` if ${b.condition}` : ""}`); return ok(`${bps.length} breakpoint${bps.length === 1 ? "" : "s"}:\n${lines.join("\n")}`); } default: throw new Error(`Unknown tool: ${name}`); } }); } - src/tools.ts:397-403 (helper)Helper functions used by the ppsspp_read8 handler: fmtHex formats a number as 'DEC (0xHEX)' and addrHex formats as zero-padded '0xXXXXXXXX' hex.
function fmtHex(n: unknown): string { if (typeof n !== "number") return String(n); return `${n} (0x${n.toString(16).toUpperCase()})`; } function addrHex(n: number): string { return `0x${n.toString(16).toUpperCase().padStart(8, "0")}`; }