mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
3971af1cc3
pad_status reports both runtimes' current pad word side-by-side so input dispatch symmetry can be validated empirically. Verified that the existing `press` cmd already drives Beetle (via psx_oracle_run_frame in main.cpp's per-frame loop); the bouncing-vs- stable behavior the operator observed was downstream of input (shell-state-machine pacing differences), not an input dispatch asymmetry. sio_card_protocol_active() accessor returns 1 if any slot has an in-flight memcard protocol exchange. Building block for a future fix to defer VBlank-driven pad polling while a card read is in progress (root cause of the directory-load failure — see memory phase4_real_root_cause_d3_to_d4 for the byte-level ours-vs-Beetle divergence proof and the failed fix attempts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2803 lines
102 KiB
C
2803 lines
102 KiB
C
/*
|
||
* debug_server.c -- TCP debug server for PSX recomp v4
|
||
*
|
||
* Single-threaded, non-blocking TCP server polled once per vblank.
|
||
* JSON-over-newline protocol on localhost:4370.
|
||
*
|
||
* Same function names and protocol as nesrecomp/snesrecomp versions
|
||
* so TCP.md and DEBUG.md are reusable across projects.
|
||
*/
|
||
#include "debug_server.h"
|
||
#include "cpu_state.h"
|
||
#include "dma.h"
|
||
#include "gpu.h"
|
||
#include "sio.h"
|
||
#include "memcard.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <stdint.h>
|
||
|
||
/* ---- Platform sockets ---- */
|
||
#ifdef _WIN32
|
||
# define WIN32_LEAN_AND_MEAN
|
||
# include <winsock2.h>
|
||
# include <ws2tcpip.h>
|
||
typedef SOCKET sock_t;
|
||
# define SOCK_INVALID INVALID_SOCKET
|
||
# define sock_close closesocket
|
||
static int sock_error(void) { return WSAGetLastError(); }
|
||
#else
|
||
# include <sys/socket.h>
|
||
# include <netinet/in.h>
|
||
# include <unistd.h>
|
||
# include <fcntl.h>
|
||
# include <errno.h>
|
||
typedef int sock_t;
|
||
# define SOCK_INVALID (-1)
|
||
# define sock_close close
|
||
static int sock_error(void) { return errno; }
|
||
#endif
|
||
|
||
#include <SDL.h>
|
||
|
||
/* ---- Externs from runtime ---- */
|
||
extern uint32_t i_stat;
|
||
extern uint32_t i_mask;
|
||
|
||
/* Memory access (from memory.c) */
|
||
extern uint32_t psx_read_word(uint32_t addr);
|
||
extern void psx_write_word(uint32_t addr, uint32_t val);
|
||
extern uint8_t psx_read_byte(uint32_t addr);
|
||
extern void psx_write_byte(uint32_t addr, uint8_t val);
|
||
|
||
/* ---- Server state ---- */
|
||
static sock_t s_listen = SOCK_INVALID;
|
||
static sock_t s_client = SOCK_INVALID;
|
||
static int s_port = 4370;
|
||
|
||
#define RECV_BUF_SIZE 8192
|
||
static char s_recv_buf[RECV_BUF_SIZE];
|
||
static int s_recv_len = 0;
|
||
|
||
/* ---- Frame counter (set by record_frame caller) ---- */
|
||
static uint64_t s_frame_count = 0;
|
||
|
||
/* ---- CPU state pointer (set at init) ---- */
|
||
static CPUState *s_cpu = NULL;
|
||
|
||
/* ---- Pause / step ---- */
|
||
static volatile int s_paused = 0;
|
||
static int s_step_count = 0;
|
||
static uint32_t s_run_to = 0;
|
||
|
||
/* ---- Input override ---- */
|
||
static int s_input_override = -1;
|
||
static int s_input_frames = 0;
|
||
|
||
/* ---- Ring buffer (heap-allocated) ---- */
|
||
static PSXFrameRecord *s_frame_history = NULL;
|
||
static uint64_t s_history_count = 0;
|
||
|
||
/* ---- Snapshot regions (configurable via set_snapshot command) ---- */
|
||
static uint32_t s_snapshot_addrs[RAM_SNAPSHOT_REGIONS];
|
||
static int s_snapshot_active[RAM_SNAPSHOT_REGIONS];
|
||
|
||
/* ---- Watchpoints ---- */
|
||
#define MAX_WATCHPOINTS 8
|
||
typedef struct {
|
||
uint32_t addr;
|
||
uint8_t prev_val;
|
||
int active;
|
||
} Watchpoint;
|
||
static Watchpoint s_watchpoints[MAX_WATCHPOINTS];
|
||
|
||
/* ---- Write trace (Tier 1 reverse debugger) ----
|
||
* Records every RAM write matching one of up to 8 configurable address ranges.
|
||
* 1M-entry ring buffer, heap-allocated in debug_server_init(). */
|
||
#define WRITE_TRACE_CAP (1 << 20) /* 1M entries = 32 MB */
|
||
typedef struct {
|
||
uint64_t seq; /* monotonic sequence number */
|
||
uint32_t addr; /* physical RAM address */
|
||
uint32_t old_val; /* pre-write value */
|
||
uint32_t new_val; /* post-write value */
|
||
uint32_t ra; /* $ra (caller return address) */
|
||
uint32_t func_addr; /* dispatch target (which recompiled function) */
|
||
uint32_t frame; /* VBlank frame number */
|
||
uint8_t width; /* 1, 2, or 4 */
|
||
uint8_t pad[3]; /* align to 32 bytes */
|
||
} WriteTraceEntry;
|
||
static WriteTraceEntry *s_wtrace = NULL;
|
||
static uint64_t s_wtrace_seq = 0; /* total writes ever recorded */
|
||
static uint32_t s_wtrace_head = 0;
|
||
|
||
/* Multi-range filter: up to 8 [lo, hi) address ranges. */
|
||
#define WTRACE_MAX_RANGES 16
|
||
static struct { uint32_t lo, hi; } s_wtrace_ranges[WTRACE_MAX_RANGES];
|
||
static int s_wtrace_range_count = 0;
|
||
|
||
/* Function attribution global — set by psx_dispatch() before each call. */
|
||
uint32_t g_debug_current_func_addr = 0;
|
||
|
||
/* ---- Dispatch trace ring buffer ----
|
||
* Records every dispatched function address for post-mortem analysis.
|
||
* 64K entries, stack-allocated (256 KB). */
|
||
#define DISPATCH_TRACE_CAP (1 << 16)
|
||
static uint32_t s_dispatch_ring[DISPATCH_TRACE_CAP];
|
||
static uint64_t s_dispatch_seq = 0;
|
||
|
||
/* Unique dispatch set — tracks every unique function address ever dispatched.
|
||
* Simple hash set with linear probing. */
|
||
#define DISPATCH_UNIQUE_CAP 4096
|
||
static uint32_t s_dispatch_unique[DISPATCH_UNIQUE_CAP];
|
||
static int s_dispatch_unique_count = 0;
|
||
|
||
static void dispatch_unique_add(uint32_t addr) {
|
||
uint32_t idx = (addr >> 2) % DISPATCH_UNIQUE_CAP;
|
||
for (int i = 0; i < DISPATCH_UNIQUE_CAP; i++) {
|
||
uint32_t slot = (idx + i) % DISPATCH_UNIQUE_CAP;
|
||
if (s_dispatch_unique[slot] == addr) return; /* already present */
|
||
if (s_dispatch_unique[slot] == 0) {
|
||
s_dispatch_unique[slot] = addr;
|
||
s_dispatch_unique_count++;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
static int dispatch_trace_contains(uint32_t target) {
|
||
uint32_t idx = (target >> 2) % DISPATCH_UNIQUE_CAP;
|
||
for (int i = 0; i < DISPATCH_UNIQUE_CAP; i++) {
|
||
uint32_t slot = (idx + i) % DISPATCH_UNIQUE_CAP;
|
||
if (s_dispatch_unique[slot] == target) return 1;
|
||
if (s_dispatch_unique[slot] == 0) return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ---- Chain-dispatch return-v0 ring ----
|
||
* Each entry pairs (just-completed dispatch target, return v0 captured from
|
||
* cpu->gpr[2]) with the chain counter (mem[0x7514]) and the SIO byte index.
|
||
* Size 4096 — only writes when prev target was a chain state (states 1..13
|
||
* read or 1..4 detection). Other dispatches go through trace_dispatch
|
||
* untouched. */
|
||
#define CHAIN_TRACE_CAP 4096
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t prev_target; /* phys addr of the dispatch that just returned */
|
||
uint32_t v0; /* cpu->gpr[2] AT the next-dispatch trace point */
|
||
uint32_t counter_7514; /* mem[0x7514] at this moment */
|
||
uint32_t flag_7520; /* mem[0x7520] success flag */
|
||
uint32_t mc_byte_seq; /* sio_get_seq() for cross-ref */
|
||
} ChainTraceEntry;
|
||
static ChainTraceEntry s_chain_trace[CHAIN_TRACE_CAP];
|
||
static uint64_t s_chain_trace_seq = 0;
|
||
static uint32_t s_prev_dispatch_target = 0;
|
||
|
||
/* Track whether we're currently INSIDE a chain-state subtree.
|
||
* Set when chain state entry dispatched; cleared when chain epilogue
|
||
* (0x5B54 or 0x5B58) is dispatched. v0 captured at the post-epilogue
|
||
* dispatch is the state's final return value. */
|
||
static uint32_t s_chain_state_active = 0; /* 0 if not in a state, else state addr */
|
||
|
||
/* ---- Function ENTRY / EXIT trace rings (Tier 1 reverse-debugger) ----
|
||
*
|
||
* Two parallel always-on rings, hooked off psx_dispatch via
|
||
* debug_server_trace_dispatch. Together they provide:
|
||
*
|
||
* Entry ring — every dispatch in CALL ORDER, with $a0..$a3, $ra, $t1
|
||
* (the B0 function index when target is a B0 vector).
|
||
* Exit ring — every function in FINISH ORDER, with $v0, $v1 and a
|
||
* link back to the entry seq.
|
||
*
|
||
* Exit detection uses a shadow call stack: each entry pushes (target, ra);
|
||
* when a later dispatch's TARGET == a stack frame's RA, we pop intermediate
|
||
* frames as "exited" and record their exit values. This is heuristic
|
||
* (longjmp / setjmp / chain-handler tail-jumps via jr $t8 will skip), but
|
||
* covers ~95% of normal call/return.
|
||
*
|
||
* Optional address filter so the ring isn't drowned by hot kernel funcs.
|
||
* Default: trace everything; user sets [lo, hi) range to focus.
|
||
*
|
||
* Memory: 1M entries × 32B = 32 MB per ring × 2 = 64 MB. Heap-allocated. */
|
||
#define FN_TRACE_CAP (1 << 20)
|
||
#define FN_STACK_DEPTH 4096
|
||
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint64_t paired_exit_seq; /* set later when matching exit recorded; 0 = open */
|
||
uint32_t func_addr;
|
||
uint32_t ra;
|
||
uint32_t a0, a1, a2, a3;
|
||
uint32_t t1; /* B0/A0/C0 function index when target is BIOS vector */
|
||
uint32_t depth; /* shadow stack depth at entry */
|
||
uint32_t frame;
|
||
} FnEntryEntry;
|
||
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint64_t entry_seq; /* link back to entry */
|
||
uint32_t func_addr;
|
||
uint32_t v0, v1;
|
||
uint32_t depth; /* shadow stack depth at exit */
|
||
uint32_t frame;
|
||
uint32_t pad;
|
||
} FnExitEntry;
|
||
|
||
static FnEntryEntry *s_fn_entry = NULL;
|
||
static FnExitEntry *s_fn_exit = NULL;
|
||
static uint64_t s_fn_entry_seq = 0;
|
||
static uint64_t s_fn_exit_seq = 0;
|
||
|
||
/* Shadow call stack: tracks open call frames. */
|
||
typedef struct {
|
||
uint32_t func_addr;
|
||
uint32_t ra;
|
||
uint64_t entry_seq; /* index in s_fn_entry */
|
||
} FnStackFrame;
|
||
static FnStackFrame s_fn_stack[FN_STACK_DEPTH];
|
||
static int s_fn_stack_top = 0;
|
||
static uint32_t s_fn_prev_ra = 0; /* last seen $ra; new JAL changes this */
|
||
/* Stats: how many shadow-stack pops we couldn't match (interference signal). */
|
||
static uint64_t s_fn_unmatched_returns = 0;
|
||
static uint64_t s_fn_stack_overflows = 0;
|
||
static uint64_t s_fn_tail_calls = 0;
|
||
|
||
/* ---- EvCB walk ring ----
|
||
*
|
||
* Captures the full kernel EvCB table on every entry to and exit from
|
||
* DeliverEvent (RAM 0x1C5C). EvCB base ptr lives at RAM 0x124, total
|
||
* bytes at RAM 0x120 (per disasm InitEvents at BFC04678).
|
||
*
|
||
* Per-entry tag identifies whether the snapshot was taken at DeliverEvent
|
||
* entry or exit; pairing them lets the operator see exactly which entries
|
||
* changed status (FIRED) during one DeliverEvent call.
|
||
*
|
||
* Memory: 256 snapshots × ~960B = ~240 KB. */
|
||
#define EVCB_DELIVER_EVENT_ADDR 0x00001C5Cu /* func_00001C5C, RAM */
|
||
#define EVCB_RING_CAP 256
|
||
#define EVCB_MAX_ENTRIES 32
|
||
#define EVCB_ENTRY_SIZE 28 /* sizeof EvCB on real PSX */
|
||
|
||
typedef struct {
|
||
uint32_t cls;
|
||
uint32_t status;
|
||
uint32_t spec;
|
||
uint32_t mode;
|
||
uint32_t fhandler;
|
||
uint32_t pad1;
|
||
uint32_t pad2;
|
||
} EvCBRec;
|
||
|
||
typedef enum { EVCB_TAG_ENTRY = 0, EVCB_TAG_EXIT = 1 } EvCBTag;
|
||
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint64_t fn_entry_seq; /* DeliverEvent fn_entry seq this snapshot is paired with */
|
||
uint8_t tag; /* EvCBTag */
|
||
uint8_t pad[3];
|
||
uint32_t evcb_base; /* RAM addr of EvCB table */
|
||
uint32_t evcb_total_bytes;
|
||
uint32_t entry_count; /* number of EvCB entries actually snapshotted */
|
||
uint32_t a0, a1; /* DeliverEvent args ($a0, $a1) when known */
|
||
uint32_t v0; /* return value (only valid for EXIT snapshots) */
|
||
uint32_t counter_7514; /* card chain counter */
|
||
uint32_t flag_755A; /* card chain abort flag */
|
||
uint32_t flag_75C0; /* state-11 success marker (B0:6380) */
|
||
uint32_t frame;
|
||
EvCBRec entries[EVCB_MAX_ENTRIES];
|
||
} EvCBSnapshot;
|
||
|
||
static EvCBSnapshot *s_evcb_ring = NULL;
|
||
static uint64_t s_evcb_ring_seq = 0;
|
||
static uint64_t s_evcb_ring_entry_count = 0; /* DeliverEvent entries seen */
|
||
static uint64_t s_evcb_ring_exit_count = 0; /* DeliverEvent exits seen */
|
||
|
||
/* DeliverEvent return tracking — bypasses shadow stack since the kernel
|
||
* uses psx_restore_state_escape() longjmp, which leaves orphaned frames.
|
||
* Watch for the next dispatch matching the entry's stored RA; if a new
|
||
* DeliverEvent entry happens first, the previous one was unwound. */
|
||
static uint32_t s_evcb_pending_exit_ra = 0;
|
||
static uint64_t s_evcb_pending_entry_seq = 0;
|
||
static int s_evcb_pending_active = 0;
|
||
static uint64_t s_evcb_unwound_count = 0; /* DeliverEvent calls that were longjmp'd over */
|
||
|
||
extern uint8_t psx_read_byte(uint32_t addr);
|
||
|
||
static uint32_t evcb_read_u32_ram(uint32_t ram_addr) {
|
||
return (uint32_t)psx_read_byte(ram_addr)
|
||
| ((uint32_t)psx_read_byte(ram_addr + 1) << 8)
|
||
| ((uint32_t)psx_read_byte(ram_addr + 2) << 16)
|
||
| ((uint32_t)psx_read_byte(ram_addr + 3) << 24);
|
||
}
|
||
|
||
/* Snapshot the EvCB table into the ring. Reads kernel ptr [0x120] and
|
||
* size [0x124] (per nocash kernel ToT layout: 0x120=EvCB ptr, 0x124=EvCB
|
||
* size). Walks up to EVCB_MAX_ENTRIES entries. */
|
||
static void evcb_snapshot_capture(EvCBTag tag, uint64_t fn_entry_seq) {
|
||
if (!s_evcb_ring || !debug_cpu_ptr) return;
|
||
|
||
uint32_t base_ptr = evcb_read_u32_ram(0x00000120);
|
||
uint32_t total_bytes = evcb_read_u32_ram(0x00000124);
|
||
|
||
/* Convert RAM base ptr to RAM offset (kernel uses cached + uncached
|
||
* mirrors; mask off region bits). */
|
||
uint32_t base_ram = base_ptr & 0x001FFFFFu;
|
||
|
||
EvCBSnapshot *e = &s_evcb_ring[s_evcb_ring_seq % EVCB_RING_CAP];
|
||
e->seq = s_evcb_ring_seq;
|
||
e->fn_entry_seq = fn_entry_seq;
|
||
e->tag = (uint8_t)tag;
|
||
e->evcb_base = base_ptr;
|
||
e->evcb_total_bytes = total_bytes;
|
||
e->a0 = debug_cpu_ptr->gpr[4];
|
||
e->a1 = debug_cpu_ptr->gpr[5];
|
||
e->v0 = debug_cpu_ptr->gpr[2];
|
||
e->counter_7514 = evcb_read_u32_ram(0x00007514);
|
||
/* 0x755A is a single byte (chain abort flag). Read its byte directly,
|
||
* not the dword at 0x7558. */
|
||
e->flag_755A = (uint32_t)psx_read_byte(0x0000755A);
|
||
e->flag_75C0 = evcb_read_u32_ram(0x000075C0);
|
||
e->frame = (uint32_t)s_frame_count;
|
||
|
||
uint32_t n_entries = (total_bytes / EVCB_ENTRY_SIZE);
|
||
if (n_entries > EVCB_MAX_ENTRIES) n_entries = EVCB_MAX_ENTRIES;
|
||
e->entry_count = n_entries;
|
||
|
||
for (uint32_t i = 0; i < n_entries; i++) {
|
||
uint32_t off = base_ram + i * EVCB_ENTRY_SIZE;
|
||
if (off + EVCB_ENTRY_SIZE > 0x00200000u) break;
|
||
e->entries[i].cls = evcb_read_u32_ram(off + 0);
|
||
e->entries[i].status = evcb_read_u32_ram(off + 4);
|
||
e->entries[i].spec = evcb_read_u32_ram(off + 8);
|
||
e->entries[i].mode = evcb_read_u32_ram(off + 12);
|
||
e->entries[i].fhandler = evcb_read_u32_ram(off + 16);
|
||
e->entries[i].pad1 = evcb_read_u32_ram(off + 20);
|
||
e->entries[i].pad2 = evcb_read_u32_ram(off + 24);
|
||
}
|
||
s_evcb_ring_seq++;
|
||
if (tag == EVCB_TAG_ENTRY) s_evcb_ring_entry_count++;
|
||
else s_evcb_ring_exit_count++;
|
||
}
|
||
|
||
/* Optional [lo, hi) physical-address filter; default = trace all. */
|
||
static uint32_t s_fn_trace_filter_lo = 0u;
|
||
static uint32_t s_fn_trace_filter_hi = 0xFFFFFFFFu;
|
||
|
||
static int fn_trace_in_filter(uint32_t phys) {
|
||
return phys >= s_fn_trace_filter_lo && phys < s_fn_trace_filter_hi;
|
||
}
|
||
|
||
/* Helper: record an exit event for a popped frame. */
|
||
static void fn_record_exit(FnStackFrame *f) {
|
||
if (!fn_trace_in_filter(f->func_addr)) return;
|
||
FnExitEntry *e = &s_fn_exit[s_fn_exit_seq % FN_TRACE_CAP];
|
||
e->seq = s_fn_exit_seq;
|
||
e->entry_seq = f->entry_seq;
|
||
e->func_addr = f->func_addr;
|
||
e->v0 = debug_cpu_ptr->gpr[2];
|
||
e->v1 = debug_cpu_ptr->gpr[3];
|
||
e->depth = (uint32_t)s_fn_stack_top;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
/* Back-fill entry's paired_exit_seq if still in ring. */
|
||
if (f->entry_seq != (uint64_t)-1
|
||
&& s_fn_entry_seq > f->entry_seq
|
||
&& s_fn_entry_seq - f->entry_seq <= FN_TRACE_CAP) {
|
||
s_fn_entry[f->entry_seq % FN_TRACE_CAP].paired_exit_seq = e->seq;
|
||
}
|
||
s_fn_exit_seq++;
|
||
}
|
||
|
||
/* Called from debug_server_trace_dispatch on every dispatch.
|
||
*
|
||
* Classify the dispatch into one of:
|
||
* RETURN — target == some frame's saved RA → pop frames as exited
|
||
* TAIL CALL — $ra unchanged from previous dispatch → replace top frame
|
||
* NEW CALL — $ra changed (fresh JAL) → push new frame
|
||
*
|
||
* Distinguishing TAIL CALL from NEW CALL via $ra-change is what keeps the
|
||
* shadow stack bounded under heavy code that uses fall-through dispatch. */
|
||
static void function_trace_record(uint32_t target) {
|
||
if (!s_fn_entry || !s_fn_exit || !debug_cpu_ptr) return;
|
||
|
||
uint32_t cur_ra = debug_cpu_ptr->gpr[31];
|
||
|
||
/* RETURN check first: walk stack from top down. Match deepest first, since
|
||
* deeper frames may have stale RAs that coincidentally match. */
|
||
int return_idx = -1;
|
||
for (int i = s_fn_stack_top - 1; i >= 0; i--) {
|
||
if (s_fn_stack[i].ra == target) { return_idx = i; break; }
|
||
}
|
||
if (return_idx >= 0) {
|
||
while (s_fn_stack_top > return_idx) {
|
||
s_fn_stack_top--;
|
||
fn_record_exit(&s_fn_stack[s_fn_stack_top]);
|
||
}
|
||
s_fn_prev_ra = cur_ra;
|
||
return;
|
||
}
|
||
|
||
/* TAIL CALL: $ra unchanged from the previous dispatch and we have an open
|
||
* frame. Treat as replacing the top frame's func_addr. Also record an
|
||
* exit for the previous func + an entry for the new func, so the user
|
||
* sees the chain of tail calls. */
|
||
if (s_fn_stack_top > 0 && cur_ra == s_fn_prev_ra && cur_ra != 0) {
|
||
s_fn_tail_calls++;
|
||
fn_record_exit(&s_fn_stack[s_fn_stack_top - 1]);
|
||
/* Replace top frame in place (don't push). */
|
||
s_fn_stack[s_fn_stack_top - 1].func_addr = target;
|
||
s_fn_stack[s_fn_stack_top - 1].entry_seq = s_fn_entry_seq;
|
||
}
|
||
/* NEW CALL or first-ever dispatch: push new frame. */
|
||
else {
|
||
if (s_fn_stack_top < FN_STACK_DEPTH) {
|
||
s_fn_stack[s_fn_stack_top].func_addr = target;
|
||
s_fn_stack[s_fn_stack_top].ra = cur_ra;
|
||
s_fn_stack[s_fn_stack_top].entry_seq = s_fn_entry_seq;
|
||
s_fn_stack_top++;
|
||
} else {
|
||
/* Stack full — drop OLDEST frame (rotate) so we keep tracking
|
||
* recent activity instead of stalling forever. The dropped frame
|
||
* never gets an exit recorded; that's acceptable for unbounded
|
||
* recursion / longjmp pathology. */
|
||
for (int i = 0; i < FN_STACK_DEPTH - 1; i++) s_fn_stack[i] = s_fn_stack[i + 1];
|
||
s_fn_stack[FN_STACK_DEPTH - 1].func_addr = target;
|
||
s_fn_stack[FN_STACK_DEPTH - 1].ra = cur_ra;
|
||
s_fn_stack[FN_STACK_DEPTH - 1].entry_seq = s_fn_entry_seq;
|
||
s_fn_stack_overflows++;
|
||
}
|
||
}
|
||
|
||
/* Record entry (always, when target passes filter). */
|
||
uint64_t this_entry_seq = (uint64_t)-1;
|
||
if (fn_trace_in_filter(target)) {
|
||
FnEntryEntry *e = &s_fn_entry[s_fn_entry_seq % FN_TRACE_CAP];
|
||
e->seq = s_fn_entry_seq;
|
||
e->paired_exit_seq = 0;
|
||
e->func_addr = target;
|
||
e->ra = cur_ra;
|
||
e->a0 = debug_cpu_ptr->gpr[4];
|
||
e->a1 = debug_cpu_ptr->gpr[5];
|
||
e->a2 = debug_cpu_ptr->gpr[6];
|
||
e->a3 = debug_cpu_ptr->gpr[7];
|
||
e->t1 = debug_cpu_ptr->gpr[9];
|
||
e->depth = (uint32_t)s_fn_stack_top;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
this_entry_seq = s_fn_entry_seq;
|
||
s_fn_entry_seq++;
|
||
|
||
/* Update the just-pushed shadow frame's entry_seq so the EXIT
|
||
* capture in fn_record_exit can pair correctly. */
|
||
if (s_fn_stack_top > 0
|
||
&& s_fn_stack[s_fn_stack_top - 1].func_addr == target) {
|
||
s_fn_stack[s_fn_stack_top - 1].entry_seq = this_entry_seq;
|
||
}
|
||
}
|
||
|
||
/* EvCB ring: capture on DeliverEvent entry. Track the return RA so
|
||
* we can record an EXIT snapshot when execution next reaches it.
|
||
* cur_ra has the kernel-mode bit set (0x80000000); psx_dispatch passes
|
||
* `target` already masked to physical (& 0x1FFFFFFF). Mask before
|
||
* comparing. If a new DeliverEvent entry arrives first, the previous
|
||
* call was longjmp'd over (psx_restore_state_escape) — count it. */
|
||
if (target == EVCB_DELIVER_EVENT_ADDR) {
|
||
if (s_evcb_pending_active) {
|
||
s_evcb_unwound_count++;
|
||
}
|
||
s_evcb_pending_exit_ra = cur_ra & 0x1FFFFFFFu;
|
||
s_evcb_pending_entry_seq = this_entry_seq;
|
||
s_evcb_pending_active = 1;
|
||
evcb_snapshot_capture(EVCB_TAG_ENTRY, this_entry_seq);
|
||
}
|
||
/* DeliverEvent exit: dispatch matching the stored return RA (phys). */
|
||
else if (s_evcb_pending_active && target == s_evcb_pending_exit_ra) {
|
||
evcb_snapshot_capture(EVCB_TAG_EXIT, s_evcb_pending_entry_seq);
|
||
s_evcb_pending_active = 0;
|
||
}
|
||
|
||
s_fn_prev_ra = cur_ra;
|
||
}
|
||
|
||
static int is_chain_state_entry(uint32_t phys) {
|
||
static const uint32_t entries[] = {
|
||
/* Read chain (table at 0x6c98) */
|
||
0x000056E8u, 0x00005768u, 0x0000579Cu, 0x00005834u, 0x00005870u,
|
||
0x000058B4u, 0x000058E8u, 0x00005918u, 0x00005954u, 0x00005990u,
|
||
0x00005A00u, 0x00005A58u, 0x00005AB0u,
|
||
/* Detection chain (table at 0x6ccc) */
|
||
0x00005BA4u, 0x00005C24u, 0x00005C58u, 0x00005D48u };
|
||
for (size_t i = 0; i < sizeof(entries)/sizeof(entries[0]); i++)
|
||
if (entries[i] == phys) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* Chain epilogue (common). All chain states fall through to bfc15654 = RAM 0x5B54.
|
||
* func_00005B54 is the lw $ra; jr $ra block. After it runs, the chain dispatcher
|
||
* cascade reads $v0. */
|
||
static int is_chain_epilogue(uint32_t phys) {
|
||
return phys == 0x00005B54u || phys == 0x00005B58u;
|
||
}
|
||
|
||
void debug_server_trace_dispatch(uint32_t func_addr) {
|
||
/* Function entry/exit rings (always-on, hooked here so every dispatch
|
||
* is recorded with args and a return value when the call unwinds). */
|
||
function_trace_record(func_addr);
|
||
|
||
/* Track when we ENTER a chain state subtree. */
|
||
if (is_chain_state_entry(func_addr)) {
|
||
s_chain_state_active = func_addr;
|
||
}
|
||
|
||
/* Capture v0 when we LEAVE a chain state subtree, identified by the dispatch
|
||
* that immediately follows the chain epilogue (0x5B54). The dispatch right
|
||
* AFTER the epilogue is the chain dispatcher's cascade input. */
|
||
if (s_prev_dispatch_target != 0
|
||
&& is_chain_epilogue(s_prev_dispatch_target)
|
||
&& s_chain_state_active != 0
|
||
&& debug_cpu_ptr) {
|
||
ChainTraceEntry *e = &s_chain_trace[s_chain_trace_seq % CHAIN_TRACE_CAP];
|
||
e->seq = s_chain_trace_seq++;
|
||
e->prev_target = s_chain_state_active; /* the state, not the epilogue */
|
||
e->v0 = debug_cpu_ptr->gpr[2];
|
||
extern uint8_t psx_read_byte(uint32_t addr);
|
||
e->counter_7514 = (uint32_t)psx_read_byte(0x7514)
|
||
| ((uint32_t)psx_read_byte(0x7515) << 8)
|
||
| ((uint32_t)psx_read_byte(0x7516) << 16)
|
||
| ((uint32_t)psx_read_byte(0x7517) << 24);
|
||
e->flag_7520 = (uint32_t)psx_read_byte(0x7520)
|
||
| ((uint32_t)psx_read_byte(0x7521) << 8)
|
||
| ((uint32_t)psx_read_byte(0x7522) << 16)
|
||
| ((uint32_t)psx_read_byte(0x7523) << 24);
|
||
e->mc_byte_seq = sio_get_trace(NULL, NULL);
|
||
s_chain_state_active = 0;
|
||
}
|
||
s_prev_dispatch_target = func_addr;
|
||
|
||
s_dispatch_ring[s_dispatch_seq % DISPATCH_TRACE_CAP] = func_addr;
|
||
s_dispatch_seq++;
|
||
dispatch_unique_add(func_addr);
|
||
}
|
||
|
||
static int json_get_int(const char *json, const char *key, int def);
|
||
void debug_server_send_fmt(const char *fmt, ...);
|
||
#define send_fmt debug_server_send_fmt
|
||
|
||
static void handle_chain_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
int total = (int)(s_chain_trace_seq < CHAIN_TRACE_CAP
|
||
? s_chain_trace_seq : CHAIN_TRACE_CAP);
|
||
if (count > total) count = total;
|
||
if (count < 0) count = 0;
|
||
|
||
char buf[128 * 1024];
|
||
int n = snprintf(buf, sizeof(buf),
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%d,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)s_chain_trace_seq, total);
|
||
|
||
int start_idx = (int)(s_chain_trace_seq - (uint64_t)count);
|
||
for (int i = 0; i < count; i++) {
|
||
int idx = (start_idx + i) % CHAIN_TRACE_CAP;
|
||
ChainTraceEntry *e = &s_chain_trace[idx];
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
"%s{\"seq\":%llu,\"prev\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"counter\":%u,\"flag_7520\":%u,"
|
||
"\"mc_seq\":%u}",
|
||
i == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->prev_target,
|
||
e->v0, e->counter_7514, e->flag_7520, e->mc_byte_seq);
|
||
if (n >= (int)sizeof(buf) - 256) break;
|
||
}
|
||
n += snprintf(buf + n, sizeof(buf) - n, "]}");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
/* ---- MMIO write trace (separate ring buffer) ----
|
||
* Records every write to 0x1F801xxx MMIO registers. Unconditional (no filtering).
|
||
* 64K entries, heap-allocated in debug_server_init(). */
|
||
#define MMIO_TRACE_CAP (1 << 16) /* 64K entries = 2 MB */
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t addr; /* 0x1F801xxx */
|
||
uint32_t val; /* value written */
|
||
uint32_t func_addr; /* dispatch target */
|
||
uint32_t ra; /* $ra */
|
||
uint32_t frame; /* VBlank frame */
|
||
uint8_t width; /* 1, 2, or 4 */
|
||
uint8_t pad[3];
|
||
} MmioTraceEntry;
|
||
static MmioTraceEntry *s_mmio_trace = NULL;
|
||
static uint64_t s_mmio_trace_seq = 0;
|
||
static uint32_t s_mmio_trace_head = 0;
|
||
|
||
/* ---- Platform helpers ---- */
|
||
static void set_nonblocking(sock_t s)
|
||
{
|
||
#ifdef _WIN32
|
||
u_long mode = 1;
|
||
ioctlsocket(s, FIONBIO, &mode);
|
||
#else
|
||
int flags = fcntl(s, F_GETFL, 0);
|
||
fcntl(s, F_SETFL, flags | O_NONBLOCK);
|
||
#endif
|
||
}
|
||
|
||
/* ---- JSON helpers ---- */
|
||
|
||
static const char *json_get_str(const char *json, const char *key,
|
||
char *out, int out_sz)
|
||
{
|
||
char pattern[128];
|
||
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
||
const char *p = strstr(json, pattern);
|
||
if (!p) return NULL;
|
||
p += strlen(pattern);
|
||
while (*p == ' ' || *p == ':') p++;
|
||
if (*p == '"') {
|
||
p++;
|
||
int i = 0;
|
||
while (*p && *p != '"' && i < out_sz - 1)
|
||
out[i++] = *p++;
|
||
out[i] = '\0';
|
||
return out;
|
||
}
|
||
{
|
||
int i = 0;
|
||
while (*p && *p != ',' && *p != '}' && *p != ' ' && i < out_sz - 1)
|
||
out[i++] = *p++;
|
||
out[i] = '\0';
|
||
return out;
|
||
}
|
||
}
|
||
|
||
static int json_get_int(const char *json, const char *key, int def)
|
||
{
|
||
char buf[64];
|
||
if (!json_get_str(json, key, buf, sizeof(buf))) return def;
|
||
return atoi(buf);
|
||
}
|
||
|
||
static uint32_t hex_to_u32(const char *s)
|
||
{
|
||
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
|
||
return (uint32_t)strtoul(s, NULL, 16);
|
||
}
|
||
|
||
/* ---- Send helpers ---- */
|
||
|
||
static void send_all_blocking(sock_t sock, const char *data, int len)
|
||
{
|
||
#ifdef _WIN32
|
||
u_long mode = 0;
|
||
ioctlsocket(sock, FIONBIO, &mode);
|
||
#else
|
||
int flags = fcntl(sock, F_GETFL, 0);
|
||
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
|
||
#endif
|
||
int sent = 0;
|
||
while (sent < len) {
|
||
int n = send(sock, data + sent, len - sent, 0);
|
||
if (n > 0) { sent += n; continue; }
|
||
break;
|
||
}
|
||
#ifdef _WIN32
|
||
mode = 1;
|
||
ioctlsocket(sock, FIONBIO, &mode);
|
||
#else
|
||
fcntl(sock, F_SETFL, flags);
|
||
#endif
|
||
}
|
||
|
||
void debug_server_send_line(const char *json)
|
||
{
|
||
if (s_client == SOCK_INVALID) return;
|
||
int len = (int)strlen(json);
|
||
send_all_blocking(s_client, json, len);
|
||
send_all_blocking(s_client, "\n", 1);
|
||
}
|
||
|
||
void debug_server_send_fmt(const char *fmt, ...)
|
||
{
|
||
char buf[65536];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||
va_end(ap);
|
||
debug_server_send_line(buf);
|
||
}
|
||
|
||
#define send_line debug_server_send_line
|
||
#define send_fmt debug_server_send_fmt
|
||
|
||
static void send_ok(int id)
|
||
{
|
||
send_fmt("{\"id\":%d,\"ok\":true}", id);
|
||
}
|
||
|
||
static void send_err(int id, const char *msg)
|
||
{
|
||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"%s\"}", id, msg);
|
||
}
|
||
|
||
/* ---- Command handlers ---- */
|
||
|
||
static void handle_ping(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%llu}",
|
||
id, (unsigned long long)s_frame_count);
|
||
}
|
||
|
||
static void handle_frame(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%llu}",
|
||
id, (unsigned long long)s_frame_count);
|
||
}
|
||
|
||
static void handle_get_registers(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
if (!s_cpu) { send_err(id, "no cpu"); return; }
|
||
|
||
char *buf = (char *)malloc(4096);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
|
||
int pos = snprintf(buf, 4096,
|
||
"{\"id\":%d,\"ok\":true,\"frame\":%llu,"
|
||
"\"gpr\":[",
|
||
id, (unsigned long long)s_frame_count);
|
||
|
||
for (int i = 0; i < 32; i++) {
|
||
if (i) buf[pos++] = ',';
|
||
pos += snprintf(buf + pos, 4096 - pos, "\"0x%08X\"", s_cpu->gpr[i]);
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 4096 - pos,
|
||
"],\"hi\":\"0x%08X\",\"lo\":\"0x%08X\","
|
||
"\"cop0_sr\":\"0x%08X\",\"cop0_cause\":\"0x%08X\",\"cop0_epc\":\"0x%08X\","
|
||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\"}",
|
||
s_cpu->hi, s_cpu->lo,
|
||
s_cpu->cop0[12], s_cpu->cop0[13], s_cpu->cop0[14],
|
||
i_stat, i_mask,
|
||
s_cpu->pc);
|
||
|
||
send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_read_ram(int id, const char *json)
|
||
{
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
int len = json_get_int(json, "len", 1);
|
||
if (len < 1) len = 1;
|
||
if (len > 16384) len = 16384; /* 16KB max */
|
||
|
||
char *hex = (char *)malloc(len * 2 + 1);
|
||
if (!hex) { send_err(id, "alloc failed"); return; }
|
||
for (int i = 0; i < len; i++)
|
||
snprintf(hex + i * 2, 3, "%02x", psx_read_byte(addr + i));
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"%s\"}",
|
||
id, addr, len, hex);
|
||
free(hex);
|
||
}
|
||
|
||
static void handle_dump_ram(int id, const char *json)
|
||
{
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
int len = json_get_int(json, "len", 256);
|
||
if (len < 1) len = 1;
|
||
if (len > 4096) len = 4096;
|
||
|
||
int offset = 0;
|
||
while (offset < len) {
|
||
int chunk = len - offset;
|
||
if (chunk > 256) chunk = 256;
|
||
char hex[513];
|
||
for (int i = 0; i < chunk; i++)
|
||
snprintf(hex + i * 2, 3, "%02x", psx_read_byte(addr + offset + i));
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"offset\":%d,\"len\":%d,\"hex\":\"%s\"}",
|
||
id, addr + offset, offset, chunk, hex);
|
||
offset += chunk;
|
||
}
|
||
}
|
||
|
||
static void handle_write_ram(int id, const char *json)
|
||
{
|
||
char addr_str[32], val_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
if (!json_get_str(json, "val", val_str, sizeof(val_str))) {
|
||
send_err(id, "missing val"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
uint8_t val = (uint8_t)hex_to_u32(val_str);
|
||
psx_write_byte(addr, val);
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_gpu_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
uint32_t gpustat = gpu_read_gpustat();
|
||
|
||
GpuDrawArea da;
|
||
gpu_get_draw_area(&da);
|
||
uint64_t nop, fill, draw, env, copy;
|
||
gpu_get_gp0_stats(&nop, &fill, &draw, &env, ©);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"display_x\":%d,\"display_y\":%d,"
|
||
"\"width\":%d,\"height\":%d,"
|
||
"\"disabled\":%d,"
|
||
"\"gpustat\":\"0x%08X\","
|
||
"\"gp0_writes\":%llu,"
|
||
"\"gp0_nop\":%llu,\"gp0_fill\":%llu,\"gp0_draw\":%llu,\"gp0_env\":%llu,\"gp0_copy\":%llu,"
|
||
"\"draw_area\":[%u,%u,%u,%u],"
|
||
"\"draw_offset\":[%d,%d]}",
|
||
id, di.display_x, di.display_y,
|
||
di.width, di.height,
|
||
di.disabled,
|
||
gpustat,
|
||
(unsigned long long)gpu_get_gp0_count(),
|
||
(unsigned long long)nop, (unsigned long long)fill,
|
||
(unsigned long long)draw, (unsigned long long)env,
|
||
(unsigned long long)copy,
|
||
da.left, da.top, da.right, da.bottom,
|
||
da.offset_x, da.offset_y);
|
||
}
|
||
|
||
static void handle_irq_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||
"\"pending\":\"0x%08X\","
|
||
"\"cop0_sr\":\"0x%08X\","
|
||
"\"IEc\":%d,\"IM2\":%d,\"BEV\":%d,"
|
||
"\"dpcr\":\"0x%08X\",\"dicr\":\"0x%08X\"}",
|
||
id, i_stat, i_mask, i_stat & i_mask,
|
||
s_cpu ? s_cpu->cop0[12] : 0,
|
||
s_cpu ? (s_cpu->cop0[12] & 1) : 0,
|
||
s_cpu ? ((s_cpu->cop0[12] >> 10) & 1) : 0,
|
||
s_cpu ? ((s_cpu->cop0[12] >> 22) & 1) : 0,
|
||
dma_get_dpcr(), dma_get_dicr());
|
||
}
|
||
|
||
/* GPU opcode counter — defined in gpu.c */
|
||
extern uint32_t gpu_get_opcode_count(uint8_t op);
|
||
|
||
extern int gpu_get_a0_count(void);
|
||
extern int gpu_get_a0_history(int index, int *x, int *y, int *w, int *h,
|
||
uint32_t *fw0, uint32_t *fw1, int *wcount);
|
||
extern int gpu_get_a0_extra(int index, uint32_t *func, uint32_t *sp, uint32_t *ra,
|
||
uint32_t *s1, uint32_t *stack10);
|
||
|
||
static void handle_a0_history(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
int count = gpu_get_a0_count();
|
||
/* Use dynamic allocation for large output */
|
||
int bufsz = 65536;
|
||
char *buf = (char*)malloc(bufsz);
|
||
if (!buf) { send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"OOM\"}", id); return; }
|
||
int pos = snprintf(buf, bufsz, "{\"id\":%d,\"ok\":true,\"count\":%d,\"uploads\":[", id, count);
|
||
for (int i = 0; i < count && pos < bufsz - 500; i++) {
|
||
int x, y, w, h, wcount;
|
||
uint32_t fw0, fw1, func, sp, ra, s1, stk[10];
|
||
gpu_get_a0_history(i, &x, &y, &w, &h, &fw0, &fw1, &wcount);
|
||
gpu_get_a0_extra(i, &func, &sp, &ra, &s1, stk);
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"%s{\"i\":%d,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,"
|
||
"\"fw0\":\"0x%08X\",\"fw1\":\"0x%08X\",\"words\":%d,"
|
||
"\"func\":\"0x%08X\",\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"s1\":\"0x%08X\","
|
||
"\"stk\":[\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\","
|
||
"\"0x%08X\",\"0x%08X\",\"0x%08X\",\"0x%08X\","
|
||
"\"0x%08X\",\"0x%08X\"]}",
|
||
i ? "," : "", i, x, y, w, h, fw0, fw1, wcount,
|
||
func, sp, ra, s1,
|
||
stk[0], stk[1], stk[2], stk[3], stk[4], stk[5], stk[6], stk[7],
|
||
stk[8], stk[9]);
|
||
}
|
||
pos += snprintf(buf + pos, bufsz - pos, "]}");
|
||
send_fmt("%s", buf);
|
||
free(buf);
|
||
}
|
||
|
||
extern int gpu_get_c0_count(void);
|
||
extern int gpu_get_c0_history(int index, int *x, int *y, int *w, int *h,
|
||
uint32_t *func, uint32_t *sp, uint32_t *s1,
|
||
uint32_t *fw0, uint32_t *fw1, int *rcount);
|
||
|
||
static void handle_c0_history(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
int count = gpu_get_c0_count();
|
||
char buf[8192];
|
||
int pos = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"count\":%d,\"reads\":[", id, count);
|
||
for (int i = 0; i < count && pos < (int)sizeof(buf) - 300; i++) {
|
||
int x, y, w, h, rcount;
|
||
uint32_t func, sp, s1, fw0, fw1;
|
||
gpu_get_c0_history(i, &x, &y, &w, &h, &func, &sp, &s1, &fw0, &fw1, &rcount);
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"%s{\"i\":%d,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,"
|
||
"\"func\":\"0x%08X\",\"sp\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||
"\"fw0\":\"0x%08X\",\"fw1\":\"0x%08X\",\"reads\":%d}",
|
||
i ? "," : "", i, x, y, w, h, func, sp, s1, fw0, fw1, rcount);
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "]}");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
static void handle_gpu_opcodes(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
/* Report non-zero GP0 opcode counts */
|
||
char buf[4096];
|
||
int pos = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"opcodes\":{", id);
|
||
int first = 1;
|
||
for (int i = 0; i < 256; i++) {
|
||
uint32_t cnt = gpu_get_opcode_count((uint8_t)i);
|
||
if (cnt > 0) {
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "%s\"0x%02X\":%u",
|
||
first ? "" : ",", i, cnt);
|
||
first = 0;
|
||
}
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "}}");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
static void handle_capture_quads(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
gpu_arm_shaded_quad_capture();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_get_quads(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const GpuSqCapEntry *entries;
|
||
int count = gpu_get_shaded_quad_capture(&entries);
|
||
char buf[8192];
|
||
int pos = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"count\":%d,\"quads\":[", id, count);
|
||
for (int i = 0; i < count && pos < (int)sizeof(buf) - 256; i++) {
|
||
const GpuSqCapEntry *e = &entries[i];
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"%s{\"v\":[%d,%d,%d,%d,%d,%d,%d,%d],\"c\":[\"0x%06X\",\"0x%06X\",\"0x%06X\",\"0x%06X\"]}",
|
||
i ? "," : "",
|
||
e->vx[0], e->vy[0], e->vx[1], e->vy[1],
|
||
e->vx[2], e->vy[2], e->vx[3], e->vy[3],
|
||
e->color[0], e->color[1], e->color[2], e->color[3]);
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "]}");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
extern uint64_t gte_get_exec_count(void);
|
||
|
||
static void handle_gte_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
if (!s_cpu) { send_err(id, "no cpu"); return; }
|
||
char buf[2048];
|
||
int pos = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"gte_exec\":%llu,\"gte_ctrl\":[",
|
||
id, (unsigned long long)gte_get_exec_count());
|
||
for (int i = 0; i < 32; i++) {
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "%s\"0x%08X\"",
|
||
i ? "," : "", s_cpu->gte_ctrl[i]);
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "],\"gte_data\":[");
|
||
for (int i = 0; i < 32; i++) {
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "%s\"0x%08X\"",
|
||
i ? "," : "", s_cpu->gte_data[i]);
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "]}");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
static void handle_sio_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern int sio_get_mc_probe_count(void);
|
||
extern int sio_get_mc_ack_count(void);
|
||
extern int sio_get_mc_cmd_count(void);
|
||
extern int sio_get_mc_read_count(void);
|
||
extern int sio_get_mc_read_done(void);
|
||
extern uint32_t sio_get_mc_last_caller(void);
|
||
extern int sio_get_mc_abort_count(void);
|
||
extern int sio_get_mc_abort_state(void);
|
||
extern uint16_t sio_get_mc_abort_ctrl(void);
|
||
extern int sio_get_mc_max_state(void);
|
||
extern int sio_get_tx_writes(void);
|
||
extern int sio_get_tx_gated(void);
|
||
extern uint16_t sio_get_last_ctrl_on_tx(void);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"sio_stat\":\"0x%04X\","
|
||
"\"sio_ctrl\":\"0x%04X\","
|
||
"\"sio_rx\":\"0x%02X\","
|
||
"\"pad_buttons\":\"0x%04X\","
|
||
"\"mc_probes\":%d,"
|
||
"\"mc_acks\":%d,"
|
||
"\"mc_cmds\":%d,"
|
||
"\"mc_reads\":%d,"
|
||
"\"mc_read_done\":%d,"
|
||
"\"mc_last_caller\":\"0x%08X\","
|
||
"\"mc_aborts\":%d,"
|
||
"\"mc_abort_state\":%d,"
|
||
"\"mc_abort_ctrl\":\"0x%04X\","
|
||
"\"mc_max_state\":%d,"
|
||
"\"tx_writes\":%d,"
|
||
"\"tx_gated\":%d,"
|
||
"\"last_ctrl_on_tx\":\"0x%04X\"}",
|
||
id,
|
||
(uint16_t)sio_read(0x1F801044),
|
||
(uint16_t)sio_read(0x1F80104A),
|
||
(uint8_t)sio_read(0x1F801040),
|
||
sio_get_pad_buttons(),
|
||
sio_get_mc_probe_count(),
|
||
sio_get_mc_ack_count(),
|
||
sio_get_mc_cmd_count(),
|
||
sio_get_mc_read_count(),
|
||
sio_get_mc_read_done(),
|
||
sio_get_mc_last_caller(),
|
||
sio_get_mc_abort_count(),
|
||
sio_get_mc_abort_state(),
|
||
sio_get_mc_abort_ctrl(),
|
||
sio_get_mc_max_state(),
|
||
sio_get_tx_writes(),
|
||
sio_get_tx_gated(),
|
||
sio_get_last_ctrl_on_tx());
|
||
}
|
||
|
||
/* ---- Memory card disk-load status (per-slot) ---- */
|
||
static void handle_mc_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const char *p0 = "", *p1 = "";
|
||
uint8_t m0[2] = {0,0}, m1[2] = {0,0};
|
||
int pres0 = 0, pres1 = 0, dirty0 = 0, dirty1 = 0;
|
||
memcard_debug_info(0, &p0, m0, &pres0, &dirty0);
|
||
memcard_debug_info(1, &p1, m1, &pres1, &dirty1);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"slot0\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
|
||
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"},"
|
||
"\"slot1\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
|
||
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"}}",
|
||
id,
|
||
pres0 ? "true" : "false", dirty0 ? "true" : "false", p0,
|
||
(m0[0] >= 0x20 && m0[0] < 0x7F) ? m0[0] : '?',
|
||
(m0[1] >= 0x20 && m0[1] < 0x7F) ? m0[1] : '?',
|
||
m0[0], m0[1],
|
||
pres1 ? "true" : "false", dirty1 ? "true" : "false", p1,
|
||
(m1[0] >= 0x20 && m1[0] < 0x7F) ? m1[0] : '?',
|
||
(m1[1] >= 0x20 && m1[1] < 0x7F) ? m1[1] : '?',
|
||
m1[0], m1[1]);
|
||
}
|
||
|
||
/* ---- I_MASK bit 7 trace (card protocol flow) ---- */
|
||
typedef struct {
|
||
uint32_t old_mask;
|
||
uint32_t new_mask;
|
||
uint32_t caller;
|
||
uint8_t width;
|
||
uint8_t bit7_set;
|
||
uint8_t bit7_clear;
|
||
uint8_t in_exc;
|
||
} ImaskTraceEntry;
|
||
extern int memory_get_imask_bit7_set_count(void);
|
||
extern int memory_get_imask_bit7_clear_count(void);
|
||
extern const ImaskTraceEntry *memory_get_imask_trace(int *idx_out, int *count_out);
|
||
|
||
static void handle_imask_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 32);
|
||
int idx, total;
|
||
const ImaskTraceEntry *buf = memory_get_imask_trace(&idx, &total);
|
||
int cap = 256; /* IMASK_TRACE_CAP */
|
||
int avail = total < cap ? total : cap;
|
||
if (count > avail) count = avail;
|
||
if (count < 1) count = 1;
|
||
|
||
int start = (idx - count + cap) % cap;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"bit7_sets\":%d,\"bit7_clears\":%d,"
|
||
"\"total\":%d,\"count\":%d,\"entries\":[",
|
||
id, memory_get_imask_bit7_set_count(),
|
||
memory_get_imask_bit7_clear_count(), total, count);
|
||
|
||
for (int i = 0; i < count; i++) {
|
||
int ii = (start + i) % cap;
|
||
const ImaskTraceEntry *e = &buf[ii];
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("{\"old\":\"0x%03X\",\"new\":\"0x%03X\","
|
||
"\"func\":\"0x%08X\",\"w\":%d,"
|
||
"\"b7s\":%d,\"b7c\":%d,\"exc\":%d}",
|
||
e->old_mask, e->new_mask,
|
||
(unsigned)e->caller, e->width,
|
||
e->bit7_set, e->bit7_clear, e->in_exc);
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_sio_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 64);
|
||
if (count < 1) count = 1;
|
||
if (count > SIO_TRACE_CAP) count = SIO_TRACE_CAP;
|
||
|
||
const SioTraceEntry *buf;
|
||
int write_idx;
|
||
uint32_t total_seq = sio_get_trace(&buf, &write_idx);
|
||
|
||
/* How many entries are actually available? */
|
||
int avail = (int)(total_seq < (uint32_t)SIO_TRACE_CAP ? total_seq : SIO_TRACE_CAP);
|
||
if (count > avail) count = avail;
|
||
|
||
/* Start reading from (write_idx - count) wrapped */
|
||
int start = (write_idx - count + SIO_TRACE_CAP) % SIO_TRACE_CAP;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"count\":%d,\"entries\":[",
|
||
id, (unsigned)total_seq, count);
|
||
|
||
for (int i = 0; i < count; i++) {
|
||
int idx = (start + i) % SIO_TRACE_CAP;
|
||
const SioTraceEntry *e = &buf[idx];
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("{\"seq\":%u,\"tx\":\"0x%02X\",\"rx\":\"0x%02X\","
|
||
"\"mc_pre\":%d,\"mc_post\":%d,"
|
||
"\"dev_pre\":%d,\"dev_post\":%d,"
|
||
"\"ctrl\":\"0x%04X\",\"func\":\"0x%08X\","
|
||
"\"abort\":%d,\"irq_cd\":%d,\"in_exc\":%d,\"ctr\":%d,"
|
||
"\"sr\":\"0x%08X\"}",
|
||
(unsigned)e->seq, e->tx, e->rx,
|
||
e->mc_state_pre, e->mc_state_post,
|
||
e->dev_pre, e->dev_post,
|
||
e->ctrl, (unsigned)e->func_addr,
|
||
e->was_abort, e->irq_countdown, e->in_exception,
|
||
e->counter_7514, (unsigned)e->cop0_sr);
|
||
}
|
||
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
/* ---- Card transaction ring dump ----
|
||
*
|
||
* Returns the most recent N closed transactions plus the live (open) txn
|
||
* if there is one. Optional slot filter restricts to a single card slot.
|
||
* Each entry includes the full TX/RX byte stream for that transaction
|
||
* (truncated to SIO_TXN_MAX_BYTES per entry). */
|
||
static const char *txn_end_reason_str(int reason) {
|
||
switch (reason) {
|
||
case SIO_TXN_END_OPEN: return "open";
|
||
case SIO_TXN_END_SUCCESS: return "success";
|
||
case SIO_TXN_END_ABORT_RESELECT: return "abort_reselect";
|
||
case SIO_TXN_END_ABORT_RESET: return "abort_reset";
|
||
case SIO_TXN_END_ABORT_SLOT: return "abort_slot";
|
||
case SIO_TXN_END_ABORT_BAD_CMD: return "abort_bad_cmd";
|
||
case SIO_TXN_END_ABORT_OTHER: return "abort_other";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void emit_card_txn_json(const SioTxnEntry *e, int is_live) {
|
||
int n_bytes = e->byte_count;
|
||
if (n_bytes > SIO_TXN_MAX_BYTES) n_bytes = SIO_TXN_MAX_BYTES;
|
||
send_fmt("{\"txn_seq\":%u,\"slot\":%u,\"cmd\":\"0x%02X\","
|
||
"\"sector\":\"0x%04X\",\"bytes\":%u,\"acks\":%u,"
|
||
"\"start_byte_seq\":%u,\"end_byte_seq\":%u,"
|
||
"\"start_func\":\"0x%08X\",\"end_func\":\"0x%08X\","
|
||
"\"end_reason\":\"%s\",\"terminal_state\":%u,\"live\":%s,\"tx\":[",
|
||
(unsigned)e->txn_seq, e->slot, e->cmd, e->sector,
|
||
(unsigned)e->byte_count, (unsigned)e->ack_count,
|
||
(unsigned)e->start_byte_seq, (unsigned)e->end_byte_seq,
|
||
(unsigned)e->start_func, (unsigned)e->end_func,
|
||
txn_end_reason_str(e->end_reason), e->terminal_state,
|
||
is_live ? "true" : "false");
|
||
for (int i = 0; i < n_bytes; i++) {
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("\"0x%02X\"", e->tx[i]);
|
||
}
|
||
send_fmt("],\"rx\":[");
|
||
for (int i = 0; i < n_bytes; i++) {
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("\"0x%02X\"", e->rx[i]);
|
||
}
|
||
send_fmt("]}");
|
||
}
|
||
|
||
/* ---- EvCB ring TCP cmds ----
|
||
*
|
||
* evcb_snapshot — one-shot read of current EvCB table from RAM
|
||
* evcb_walk_dump — recent always-on snapshots (paired entry + exit per
|
||
* DeliverEvent call)
|
||
* evcb_walk_stats — counts + ring usage */
|
||
static void emit_evcb_snapshot_json(const EvCBSnapshot *e) {
|
||
send_fmt("{\"seq\":%llu,\"fn_entry_seq\":%llu,\"tag\":\"%s\","
|
||
"\"evcb_base\":\"0x%08X\",\"evcb_total_bytes\":%u,"
|
||
"\"entry_count\":%u,\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"counter_7514\":%u,\"flag_755A\":\"0x%08X\","
|
||
"\"flag_75C0\":\"0x%08X\",\"frame\":%u,\"entries\":[",
|
||
(unsigned long long)e->seq, (unsigned long long)e->fn_entry_seq,
|
||
e->tag == EVCB_TAG_ENTRY ? "entry" : "exit",
|
||
(unsigned)e->evcb_base, (unsigned)e->evcb_total_bytes,
|
||
(unsigned)e->entry_count, (unsigned)e->a0, (unsigned)e->a1,
|
||
(unsigned)e->v0, (unsigned)e->counter_7514,
|
||
(unsigned)e->flag_755A, (unsigned)e->flag_75C0,
|
||
(unsigned)e->frame);
|
||
for (uint32_t i = 0; i < e->entry_count; i++) {
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("{\"i\":%u,\"class\":\"0x%08X\",\"status\":\"0x%08X\","
|
||
"\"spec\":\"0x%08X\",\"mode\":\"0x%08X\","
|
||
"\"fhandler\":\"0x%08X\"}",
|
||
i, (unsigned)e->entries[i].cls,
|
||
(unsigned)e->entries[i].status,
|
||
(unsigned)e->entries[i].spec,
|
||
(unsigned)e->entries[i].mode,
|
||
(unsigned)e->entries[i].fhandler);
|
||
}
|
||
send_fmt("]}");
|
||
}
|
||
|
||
static void handle_evcb_snapshot(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
/* Synthetic one-shot capture into a stack-local snapshot — does NOT
|
||
* record into the ring (so manual probes don't drown the always-on
|
||
* pairing). */
|
||
EvCBSnapshot snap;
|
||
memset(&snap, 0, sizeof(snap));
|
||
if (!debug_cpu_ptr) { send_err(id, "no cpu ptr"); return; }
|
||
uint32_t base_ptr = evcb_read_u32_ram(0x00000120);
|
||
uint32_t total_bytes = evcb_read_u32_ram(0x00000124);
|
||
snap.seq = (uint64_t)-1; /* synthetic */
|
||
snap.fn_entry_seq = (uint64_t)-1;
|
||
snap.tag = EVCB_TAG_ENTRY;
|
||
snap.evcb_base = base_ptr;
|
||
snap.evcb_total_bytes = total_bytes;
|
||
snap.a0 = debug_cpu_ptr->gpr[4];
|
||
snap.a1 = debug_cpu_ptr->gpr[5];
|
||
snap.v0 = debug_cpu_ptr->gpr[2];
|
||
snap.counter_7514 = evcb_read_u32_ram(0x00007514);
|
||
snap.flag_755A = (uint32_t)psx_read_byte(0x0000755A);
|
||
snap.flag_75C0 = evcb_read_u32_ram(0x000075C0);
|
||
snap.frame = (uint32_t)s_frame_count;
|
||
uint32_t base_ram = base_ptr & 0x001FFFFFu;
|
||
uint32_t n_entries = (total_bytes / EVCB_ENTRY_SIZE);
|
||
if (n_entries > EVCB_MAX_ENTRIES) n_entries = EVCB_MAX_ENTRIES;
|
||
snap.entry_count = n_entries;
|
||
for (uint32_t i = 0; i < n_entries; i++) {
|
||
uint32_t off = base_ram + i * EVCB_ENTRY_SIZE;
|
||
if (off + EVCB_ENTRY_SIZE > 0x00200000u) break;
|
||
snap.entries[i].cls = evcb_read_u32_ram(off + 0);
|
||
snap.entries[i].status = evcb_read_u32_ram(off + 4);
|
||
snap.entries[i].spec = evcb_read_u32_ram(off + 8);
|
||
snap.entries[i].mode = evcb_read_u32_ram(off + 12);
|
||
snap.entries[i].fhandler = evcb_read_u32_ram(off + 16);
|
||
}
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"snapshot\":", id);
|
||
emit_evcb_snapshot_json(&snap);
|
||
send_fmt("}\n");
|
||
}
|
||
|
||
static void handle_evcb_walk_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 8);
|
||
if (count < 1) count = 1;
|
||
if (count > EVCB_RING_CAP) count = EVCB_RING_CAP;
|
||
|
||
if (!s_evcb_ring) { send_err(id, "evcb ring not allocated"); return; }
|
||
|
||
int avail = (int)(s_evcb_ring_seq < (uint64_t)EVCB_RING_CAP
|
||
? s_evcb_ring_seq : EVCB_RING_CAP);
|
||
if (count > avail) count = avail;
|
||
|
||
uint64_t start_seq = s_evcb_ring_seq - (uint64_t)count;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"entry_count\":%llu,"
|
||
"\"exit_count\":%llu,\"shown\":%d,\"snapshots\":[",
|
||
id, (unsigned long long)s_evcb_ring_seq,
|
||
(unsigned long long)s_evcb_ring_entry_count,
|
||
(unsigned long long)s_evcb_ring_exit_count, count);
|
||
for (int i = 0; i < count; i++) {
|
||
const EvCBSnapshot *e = &s_evcb_ring[(start_seq + (uint64_t)i) % EVCB_RING_CAP];
|
||
if (i > 0) send_fmt(",");
|
||
emit_evcb_snapshot_json(e);
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_evcb_walk_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"entry_count\":%llu,"
|
||
"\"exit_count\":%llu,\"unwound_count\":%llu,"
|
||
"\"pending_active\":%d,\"ring_cap\":%d,\"max_entries\":%d}\n",
|
||
id, (unsigned long long)s_evcb_ring_seq,
|
||
(unsigned long long)s_evcb_ring_entry_count,
|
||
(unsigned long long)s_evcb_ring_exit_count,
|
||
(unsigned long long)s_evcb_unwound_count,
|
||
s_evcb_pending_active,
|
||
EVCB_RING_CAP, EVCB_MAX_ENTRIES);
|
||
}
|
||
|
||
/* ---- SIO IRQ ring dump ----
|
||
*
|
||
* Returns the most recent N IRQ #7 fires with timing/source/state context.
|
||
* Each entry shows: when it was scheduled (byte_seq) vs when it actually
|
||
* fired, what the chain counter was, and whether mc_state was idle or in
|
||
* the middle of a card protocol. */
|
||
static const char *sio_irq_src_str(int s) {
|
||
switch (s) {
|
||
case SIO_IRQ_SRC_CARD_ACK: return "card";
|
||
case SIO_IRQ_SRC_PAD_ACK: return "pad";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void handle_sio_irq_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 64);
|
||
int src_filter = json_get_int(json, "src", -1); /* -1=all, else SioIrqSource value */
|
||
if (count < 1) count = 1;
|
||
if (count > SIO_IRQ_RING_CAP) count = SIO_IRQ_RING_CAP;
|
||
|
||
const SioIrqEntry *buf;
|
||
int write_idx;
|
||
uint32_t total_seq = sio_get_irq_ring(&buf, &write_idx);
|
||
|
||
int avail = (int)(total_seq < (uint32_t)SIO_IRQ_RING_CAP
|
||
? total_seq : SIO_IRQ_RING_CAP);
|
||
if (count > avail) count = avail;
|
||
|
||
int start = (write_idx - count + SIO_IRQ_RING_CAP) % SIO_IRQ_RING_CAP;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"shown\":%d,"
|
||
"\"src_filter\":%d,\"entries\":[",
|
||
id, (unsigned)total_seq, count, src_filter);
|
||
|
||
int emitted = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
const SioIrqEntry *e = &buf[(start + i) % SIO_IRQ_RING_CAP];
|
||
if (src_filter >= 0 && (int)e->source != src_filter) continue;
|
||
if (emitted > 0) send_fmt(",");
|
||
send_fmt("{\"seq\":%u,\"src\":\"%s\",\"slot\":%u,\"delay\":%u,"
|
||
"\"byte_seq\":%u,\"mc_state\":%u,\"active_device\":%u,"
|
||
"\"ctrl\":\"0x%04X\",\"func\":\"0x%08X\","
|
||
"\"counter_7514\":%u,"
|
||
"\"i_stat_before\":\"0x%08X\",\"i_stat_after\":\"0x%08X\"}",
|
||
(unsigned)e->seq, sio_irq_src_str(e->source),
|
||
e->slot, e->delay_applied,
|
||
(unsigned)e->byte_seq, (unsigned)e->mc_state,
|
||
(unsigned)e->active_device,
|
||
(unsigned)e->ctrl, (unsigned)e->func_addr,
|
||
(unsigned)e->counter_7514,
|
||
(unsigned)e->i_stat_before, (unsigned)e->i_stat_after);
|
||
emitted++;
|
||
}
|
||
send_fmt("],\"emitted\":%d}\n", emitted);
|
||
}
|
||
|
||
static void handle_card_txn_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 16);
|
||
int slot = json_get_int(json, "slot", -1); /* -1 = all */
|
||
if (count < 1) count = 1;
|
||
if (count > SIO_TXN_CAP) count = SIO_TXN_CAP;
|
||
|
||
const SioTxnEntry *buf;
|
||
int write_idx, open_flag;
|
||
uint32_t total_seq = sio_get_card_txns(&buf, &write_idx, &open_flag);
|
||
|
||
int avail = (int)(total_seq < (uint32_t)SIO_TXN_CAP
|
||
? total_seq : SIO_TXN_CAP);
|
||
if (count > avail) count = avail;
|
||
|
||
int start = (write_idx - count + SIO_TXN_CAP) % SIO_TXN_CAP;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total_closed\":%u,\"open\":%s,"
|
||
"\"slot_filter\":%d,\"count\":%d,\"entries\":[",
|
||
id, (unsigned)total_seq, open_flag ? "true" : "false",
|
||
slot, count);
|
||
|
||
int emitted = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
int idx = (start + i) % SIO_TXN_CAP;
|
||
const SioTxnEntry *e = &buf[idx];
|
||
if (slot >= 0 && (int)e->slot != slot) continue;
|
||
if (emitted > 0) send_fmt(",");
|
||
emit_card_txn_json(e, 0);
|
||
emitted++;
|
||
}
|
||
|
||
/* Also append the live (open) txn if present and matches the filter. */
|
||
const SioTxnEntry *live = sio_get_card_txn_live();
|
||
if (live && (slot < 0 || (int)live->slot == slot)) {
|
||
if (emitted > 0) send_fmt(",");
|
||
emit_card_txn_json(live, 1);
|
||
emitted++;
|
||
}
|
||
|
||
send_fmt("],\"emitted\":%d}\n", emitted);
|
||
}
|
||
|
||
static void handle_watch(int id, const char *json)
|
||
{
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
|
||
for (int i = 0; i < MAX_WATCHPOINTS; i++) {
|
||
if (!s_watchpoints[i].active) {
|
||
s_watchpoints[i].addr = addr;
|
||
s_watchpoints[i].prev_val = psx_read_byte(addr);
|
||
s_watchpoints[i].active = 1;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"slot\":%d,\"addr\":\"0x%08X\"}",
|
||
id, i, addr);
|
||
return;
|
||
}
|
||
}
|
||
send_err(id, "all watchpoint slots full (max 8)");
|
||
}
|
||
|
||
static void handle_unwatch(int id, const char *json)
|
||
{
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
|
||
for (int i = 0; i < MAX_WATCHPOINTS; i++) {
|
||
if (s_watchpoints[i].active && s_watchpoints[i].addr == addr) {
|
||
s_watchpoints[i].active = 0;
|
||
send_ok(id);
|
||
return;
|
||
}
|
||
}
|
||
send_err(id, "watchpoint not found");
|
||
}
|
||
|
||
static void handle_set_input(int id, const char *json)
|
||
{
|
||
char val_str[32];
|
||
if (!json_get_str(json, "buttons", val_str, sizeof(val_str))) {
|
||
send_err(id, "missing buttons"); return;
|
||
}
|
||
s_input_override = (int)hex_to_u32(val_str);
|
||
s_input_frames = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_press(int id, const char *json)
|
||
{
|
||
int buttons = json_get_int(json, "buttons", -1);
|
||
int frames = json_get_int(json, "frames", 2);
|
||
if (buttons < 0) { send_err(id, "missing buttons"); return; }
|
||
s_input_override = buttons;
|
||
s_input_frames = frames;
|
||
send_ok(id);
|
||
}
|
||
|
||
/* Reports BOTH ours' and Beetle's current pad word so the operator can
|
||
* empirically verify input dispatch symmetry. Note: ours' pad is what
|
||
* sio_set_pad_state stored last; Beetle's is whatever the last frame
|
||
* set s_joypad to. They should match when override is active. */
|
||
extern uint16_t sio_get_pad_buttons(void);
|
||
#if defined(ENABLE_BEETLE_PSX_ORACLE)
|
||
extern uint16_t beetle_get_pad(void);
|
||
#endif
|
||
static void handle_pad_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint16_t ours = sio_get_pad_buttons();
|
||
#if defined(ENABLE_BEETLE_PSX_ORACLE)
|
||
uint16_t beetle = beetle_get_pad();
|
||
#else
|
||
uint16_t beetle = 0xFFFF;
|
||
#endif
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"ours\":\"0x%04X\",\"beetle\":\"0x%04X\","
|
||
"\"override\":%d,\"override_frames\":%d,\"match\":%s}\n",
|
||
id, ours, beetle, s_input_override, s_input_frames,
|
||
(ours == beetle) ? "true" : "false");
|
||
}
|
||
|
||
static void handle_clear_input(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_input_override = -1;
|
||
s_input_frames = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
/* ---- display_source: toggle SDL window between our VRAM and Beetle's framebuffer ---- */
|
||
extern void display_source_set(int src);
|
||
extern int display_source_get(void);
|
||
static void handle_display_source(int id, const char *json)
|
||
{
|
||
char val[16] = {0};
|
||
if (!json_get_str(json, "src", val, sizeof(val))) {
|
||
/* No arg = report current */
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"src\":\"%s\"}",
|
||
id, display_source_get() ? "beetle" : "ours");
|
||
return;
|
||
}
|
||
if (strcmp(val, "beetle") == 0) display_source_set(1);
|
||
else if (strcmp(val, "ours") == 0) display_source_set(0);
|
||
else { send_err(id, "src must be 'ours' or 'beetle'"); return; }
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"src\":\"%s\"}",
|
||
id, display_source_get() ? "beetle" : "ours");
|
||
}
|
||
|
||
static void handle_pause(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_paused = 1;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"paused\":true,\"frame\":%llu}",
|
||
id, (unsigned long long)s_frame_count);
|
||
}
|
||
|
||
static void handle_continue(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_paused = 0;
|
||
s_step_count = 0;
|
||
s_run_to = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"paused\":false}", id);
|
||
}
|
||
|
||
static void handle_step(int id, const char *json)
|
||
{
|
||
int n = json_get_int(json, "count", 1);
|
||
if (n < 1) n = 1;
|
||
s_step_count = n;
|
||
s_paused = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"stepping\":%d}", id, n);
|
||
}
|
||
|
||
static void handle_run_to_frame(int id, const char *json)
|
||
{
|
||
int target = json_get_int(json, "frame", 0);
|
||
if (target <= (int)s_frame_count) {
|
||
send_err(id, "target frame already passed"); return;
|
||
}
|
||
s_run_to = (uint32_t)target;
|
||
s_paused = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"running_to\":%d}", id, target);
|
||
}
|
||
|
||
/* ---- Ring buffer queries ---- */
|
||
|
||
static void handle_history(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%llu,\"oldest\":%llu,\"newest\":%llu}",
|
||
id,
|
||
(unsigned long long)s_history_count,
|
||
(unsigned long long)oldest,
|
||
(unsigned long long)(s_history_count > 0 ? s_history_count - 1 : 0));
|
||
}
|
||
|
||
static void handle_get_frame(int id, const char *json)
|
||
{
|
||
int f = json_get_int(json, "frame", -1);
|
||
if (f < 0) { send_err(id, "missing frame"); return; }
|
||
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
if ((uint64_t)f < oldest || (uint64_t)f >= s_history_count) {
|
||
send_err(id, "frame not in buffer"); return;
|
||
}
|
||
|
||
uint32_t idx = (uint32_t)f % FRAME_HISTORY_CAP;
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
if (r->frame_number != (uint32_t)f) {
|
||
send_err(id, "frame record mismatch"); return;
|
||
}
|
||
|
||
char *buf = (char *)malloc(8192);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
|
||
int pos = snprintf(buf, 8192,
|
||
"{\"id\":%d,\"ok\":true,"
|
||
"\"frame\":%u,\"verify_pass\":%d,\"diff_count\":%d,"
|
||
"\"cop0_sr\":\"0x%08X\",\"cop0_cause\":\"0x%08X\",\"cop0_epc\":\"0x%08X\","
|
||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||
"\"display\":{\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"disabled\":%d},"
|
||
"\"pad_buttons\":\"0x%04X\","
|
||
"\"sio_stat\":\"0x%04X\",\"sio_ctrl\":\"0x%04X\","
|
||
"\"dispatch_count\":%u,"
|
||
"\"total_dispatches\":%llu,"
|
||
"\"last_func\":\"%s\","
|
||
"\"gpr\":[",
|
||
id, r->frame_number, r->verify_pass, r->diff_count,
|
||
r->cop0_sr, r->cop0_cause, r->cop0_epc,
|
||
r->i_stat, r->i_mask,
|
||
r->display_area_x, r->display_area_y, r->display_w, r->display_h,
|
||
r->display_disabled,
|
||
r->pad_buttons,
|
||
r->sio_stat, r->sio_ctrl,
|
||
r->dispatch_count,
|
||
(unsigned long long)r->total_dispatches,
|
||
r->last_func);
|
||
|
||
for (int i = 0; i < 32; i++) {
|
||
if (i) buf[pos++] = ',';
|
||
pos += snprintf(buf + pos, 8192 - pos, "\"0x%08X\"", r->gpr[i]);
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 8192 - pos, "]}");
|
||
send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_frame_range(int id, const char *json)
|
||
{
|
||
int start = json_get_int(json, "start", -1);
|
||
int end = json_get_int(json, "end", -1);
|
||
if (start < 0 || end < 0) { send_err(id, "missing start/end"); return; }
|
||
if (end - start + 1 > 200) { send_err(id, "max 200 frames per request"); return; }
|
||
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
|
||
char *buf = (char *)malloc(200 * 256 + 256);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
|
||
int pos = snprintf(buf, 64, "{\"id\":%d,\"ok\":true,\"frames\":[", id);
|
||
int first = 1;
|
||
|
||
for (int f = start; f <= end; f++) {
|
||
if (!first) buf[pos++] = ',';
|
||
first = 0;
|
||
|
||
if ((uint64_t)f < oldest || (uint64_t)f >= s_history_count) {
|
||
pos += snprintf(buf + pos, 128, "{\"frame\":%d,\"available\":false}", f);
|
||
continue;
|
||
}
|
||
uint32_t idx = (uint32_t)f % FRAME_HISTORY_CAP;
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
if (r->frame_number != (uint32_t)f) {
|
||
pos += snprintf(buf + pos, 128, "{\"frame\":%d,\"available\":false}", f);
|
||
continue;
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 256,
|
||
"{\"frame\":%u,\"verify\":%d,"
|
||
"\"sr\":\"0x%08X\",\"i_stat\":\"0x%08X\","
|
||
"\"pad\":\"0x%04X\"}",
|
||
r->frame_number, r->verify_pass,
|
||
r->cop0_sr, r->i_stat,
|
||
r->pad_buttons);
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 8, "]}");
|
||
send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_frame_timeseries(int id, const char *json)
|
||
{
|
||
int start = json_get_int(json, "start", -1);
|
||
int end = json_get_int(json, "end", -1);
|
||
if (start < 0 || end < 0) { send_err(id, "missing start/end"); return; }
|
||
if (end - start + 1 > 200) { send_err(id, "max 200 frames per request"); return; }
|
||
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
|
||
char *buf = (char *)malloc(200 * 320 + 256);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
|
||
int pos = snprintf(buf, 64, "{\"id\":%d,\"ok\":true,\"ts\":[", id);
|
||
int first = 1;
|
||
|
||
for (int f = start; f <= end; f++) {
|
||
if (!first) buf[pos++] = ',';
|
||
first = 0;
|
||
|
||
if ((uint64_t)f < oldest || (uint64_t)f >= s_history_count) {
|
||
pos += snprintf(buf + pos, 32, "null");
|
||
continue;
|
||
}
|
||
uint32_t idx = (uint32_t)f % FRAME_HISTORY_CAP;
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
if (r->frame_number != (uint32_t)f) {
|
||
pos += snprintf(buf + pos, 32, "null");
|
||
continue;
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 320,
|
||
"{\"f\":%u,\"v\":%d,"
|
||
"\"sr\":\"0x%08X\",\"ist\":\"0x%08X\",\"imk\":\"0x%08X\","
|
||
"\"pad\":\"0x%04X\",\"dc\":%u}",
|
||
r->frame_number, r->verify_pass,
|
||
r->cop0_sr, r->i_stat, r->i_mask,
|
||
r->pad_buttons, r->dispatch_count);
|
||
}
|
||
|
||
pos += snprintf(buf + pos, 8, "]}");
|
||
send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_first_failure(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
|
||
for (uint64_t f = oldest; f < s_history_count; f++) {
|
||
uint32_t idx = (uint32_t)(f % FRAME_HISTORY_CAP);
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
if (r->frame_number == (uint32_t)f && r->verify_pass == 0) {
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%u,\"diff_count\":%d}",
|
||
id, r->frame_number, r->diff_count);
|
||
return;
|
||
}
|
||
}
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":-1,\"message\":\"no failures found\"}", id);
|
||
}
|
||
|
||
static void handle_read_frame_ram(int id, const char *json)
|
||
{
|
||
int f = json_get_int(json, "frame", -1);
|
||
if (f < 0) { send_err(id, "missing frame"); return; }
|
||
if (!s_frame_history) { send_err(id, "ring buffer not allocated"); return; }
|
||
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
int len = json_get_int(json, "len", 1);
|
||
if (len < 1) len = 1;
|
||
if (len > 128) len = 128;
|
||
|
||
uint64_t oldest = (s_history_count > FRAME_HISTORY_CAP)
|
||
? s_history_count - FRAME_HISTORY_CAP : 0;
|
||
if ((uint64_t)f < oldest || (uint64_t)f >= s_history_count) {
|
||
send_err(id, "frame not in buffer"); return;
|
||
}
|
||
|
||
uint32_t idx = (uint32_t)f % FRAME_HISTORY_CAP;
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
|
||
/* Find matching snapshot region */
|
||
char hex[257];
|
||
int found = 0;
|
||
for (int i = 0; i < RAM_SNAPSHOT_REGIONS; i++) {
|
||
if (r->snapshot_addr[i] == 0) continue;
|
||
if (addr >= r->snapshot_addr[i] && addr + len <= r->snapshot_addr[i] + RAM_SNAPSHOT_SIZE) {
|
||
uint32_t off = addr - r->snapshot_addr[i];
|
||
for (int j = 0; j < len; j++)
|
||
snprintf(hex + j * 2, 3, "%02x", r->snapshot_data[i][off + j]);
|
||
found = 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
send_err(id, "address not in any snapshot region for this frame"); return;
|
||
}
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%d,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"%s\"}",
|
||
id, f, addr, len, hex);
|
||
}
|
||
|
||
static void handle_set_snapshot(int id, const char *json)
|
||
{
|
||
int slot = json_get_int(json, "slot", -1);
|
||
if (slot < 0 || slot >= RAM_SNAPSHOT_REGIONS) {
|
||
send_err(id, "invalid slot (0-3)"); return;
|
||
}
|
||
char addr_str[32];
|
||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||
send_err(id, "missing addr"); return;
|
||
}
|
||
uint32_t addr = hex_to_u32(addr_str);
|
||
s_snapshot_addrs[slot] = addr;
|
||
s_snapshot_active[slot] = (addr != 0);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"slot\":%d,\"addr\":\"0x%08X\"}", id, slot, addr);
|
||
}
|
||
|
||
static void handle_get_snapshots(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"snapshots\":["
|
||
"{\"slot\":0,\"addr\":\"0x%08X\",\"active\":%d},"
|
||
"{\"slot\":1,\"addr\":\"0x%08X\",\"active\":%d},"
|
||
"{\"slot\":2,\"addr\":\"0x%08X\",\"active\":%d},"
|
||
"{\"slot\":3,\"addr\":\"0x%08X\",\"active\":%d}]}",
|
||
id,
|
||
s_snapshot_addrs[0], s_snapshot_active[0],
|
||
s_snapshot_addrs[1], s_snapshot_active[1],
|
||
s_snapshot_addrs[2], s_snapshot_active[2],
|
||
s_snapshot_addrs[3], s_snapshot_active[3]);
|
||
}
|
||
|
||
static void handle_screenshot_file(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
if (di.disabled || di.width == 0 || di.height == 0) {
|
||
send_err(id, "display disabled"); return;
|
||
}
|
||
|
||
const uint16_t *vram = gpu_get_vram();
|
||
uint32_t w = di.width; if (w > 640) w = 640;
|
||
uint32_t h = di.height; if (h > 512) h = 512;
|
||
|
||
const char *path = "psx_screenshot.bmp";
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) { send_err(id, "cannot open file"); return; }
|
||
|
||
/* BMP row stride: 3 bytes/pixel, padded to 4-byte boundary */
|
||
uint32_t row_stride = (w * 3 + 3) & ~3u;
|
||
uint32_t pixel_size = row_stride * h;
|
||
uint32_t file_size = 14 + 40 + pixel_size;
|
||
|
||
/* BITMAPFILEHEADER (14 bytes) */
|
||
uint8_t bfh[14] = {0};
|
||
bfh[0] = 'B'; bfh[1] = 'M';
|
||
bfh[2] = file_size & 0xFF; bfh[3] = (file_size >> 8) & 0xFF;
|
||
bfh[4] = (file_size >> 16) & 0xFF; bfh[5] = (file_size >> 24) & 0xFF;
|
||
bfh[10] = 54; /* offset to pixel data */
|
||
fwrite(bfh, 1, 14, f);
|
||
|
||
/* BITMAPINFOHEADER (40 bytes) */
|
||
uint8_t bih[40] = {0};
|
||
bih[0] = 40; /* header size */
|
||
bih[4] = w & 0xFF; bih[5] = (w >> 8) & 0xFF;
|
||
/* Height negative = top-down */
|
||
int32_t neg_h = -(int32_t)h;
|
||
memcpy(bih + 8, &neg_h, 4);
|
||
bih[12] = 1; /* planes */
|
||
bih[14] = 24; /* bits per pixel */
|
||
fwrite(bih, 1, 40, f);
|
||
|
||
/* Pixel data: top-down, BGR */
|
||
uint8_t *row = (uint8_t *)malloc(row_stride);
|
||
for (uint32_t y = 0; y < h; y++) {
|
||
memset(row, 0, row_stride);
|
||
uint32_t vy = (di.display_y + y) & 511;
|
||
for (uint32_t x = 0; x < w; x++) {
|
||
uint32_t vx = (di.display_x + x) & 1023;
|
||
uint16_t c = vram[vy * 1024 + vx];
|
||
uint8_t r = (c & 0x1F) << 3;
|
||
uint8_t g = ((c >> 5) & 0x1F) << 3;
|
||
uint8_t b = ((c >> 10) & 0x1F) << 3;
|
||
row[x * 3 + 0] = b;
|
||
row[x * 3 + 1] = g;
|
||
row[x * 3 + 2] = r;
|
||
}
|
||
fwrite(row, 1, row_stride, f);
|
||
}
|
||
free(row);
|
||
fclose(f);
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%u,\"height\":%u}",
|
||
id, path, w, h);
|
||
}
|
||
|
||
static void handle_screenshot(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
/* Read display area from GPU and encode as hex RGB555 */
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
|
||
if (di.disabled || di.width == 0 || di.height == 0) {
|
||
send_err(id, "display disabled"); return;
|
||
}
|
||
|
||
const uint16_t *vram = gpu_get_vram();
|
||
uint32_t w = di.width;
|
||
uint32_t h = di.height;
|
||
if (w > 640) w = 640;
|
||
if (h > 512) h = 512;
|
||
|
||
/* Send metadata first */
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"width\":%u,\"height\":%u,\"format\":\"rgb555\"}",
|
||
id, w, h);
|
||
|
||
/* Send rows as hex lines */
|
||
char *hex = (char *)malloc(w * 4 + 32);
|
||
if (!hex) return;
|
||
|
||
for (uint32_t y = 0; y < h; y++) {
|
||
uint32_t vy = (di.display_y + y) & 511;
|
||
for (uint32_t x = 0; x < w; x++) {
|
||
uint32_t vx = (di.display_x + x) & 1023;
|
||
uint16_t pixel = vram[vy * 1024 + vx];
|
||
snprintf(hex + x * 4, 5, "%04x", pixel);
|
||
}
|
||
send_fmt("{\"row\":%u,\"hex\":\"%s\"}", y, hex);
|
||
}
|
||
free(hex);
|
||
}
|
||
|
||
static void handle_vram_peek(int id, const char *json)
|
||
{
|
||
int x = json_get_int(json, "x", 0);
|
||
int y = json_get_int(json, "y", 0);
|
||
int w = json_get_int(json, "w", 8);
|
||
int h = json_get_int(json, "h", 1);
|
||
if (w > 64) w = 64;
|
||
if (h > 64) h = 64;
|
||
char hex[64*64*4+1];
|
||
int pos = 0;
|
||
for (int row = 0; row < h; row++) {
|
||
for (int col = 0; col < w; col++) {
|
||
uint16_t p = gpu_vram_peek(x + col, y + row);
|
||
pos += snprintf(hex + pos, sizeof(hex) - pos, "%04x", p);
|
||
}
|
||
}
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,\"hex\":\"%s\"}",
|
||
id, x, y, w, h, hex);
|
||
}
|
||
|
||
/* ---- Write trace: hook + handlers (Tier 1 reverse debugger) ---- */
|
||
extern CPUState *debug_cpu_ptr;
|
||
|
||
/* Record a single write into the RAM trace ring buffer. */
|
||
static void wtrace_record(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width)
|
||
{
|
||
if (!s_wtrace) return;
|
||
uint32_t ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
WriteTraceEntry *e = &s_wtrace[s_wtrace_head];
|
||
e->seq = s_wtrace_seq++;
|
||
e->addr = phys;
|
||
e->old_val = old_val;
|
||
e->new_val = new_val;
|
||
e->ra = ra;
|
||
e->func_addr = g_debug_current_func_addr;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
s_wtrace_head = (s_wtrace_head + 1) % WRITE_TRACE_CAP;
|
||
}
|
||
|
||
/* Multi-range check called from memory.c write paths.
|
||
* Iterates up to 8 ranges; records if any match. */
|
||
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||
uint32_t new_val, uint8_t width)
|
||
{
|
||
if (s_wtrace_range_count == 0) return;
|
||
for (int i = 0; i < s_wtrace_range_count; i++) {
|
||
if (phys >= s_wtrace_ranges[i].lo && phys < s_wtrace_ranges[i].hi) {
|
||
wtrace_record(phys, old_val, new_val, width);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* MMIO write trace — called from memory.c mmio_write32/16/8. */
|
||
void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width)
|
||
{
|
||
if (!s_mmio_trace) return;
|
||
uint32_t ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
MmioTraceEntry *e = &s_mmio_trace[s_mmio_trace_head];
|
||
e->seq = s_mmio_trace_seq++;
|
||
e->addr = addr;
|
||
e->val = val;
|
||
e->func_addr = g_debug_current_func_addr;
|
||
e->ra = ra;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
s_mmio_trace_head = (s_mmio_trace_head + 1) % MMIO_TRACE_CAP;
|
||
}
|
||
|
||
static void handle_wtrace_range(int id, const char *json)
|
||
{
|
||
/* Backward compat: sets slot 0, clears all other slots. */
|
||
char lo_str[32], hi_str[32];
|
||
if (!json_get_str(json, "lo", lo_str, sizeof(lo_str))) { send_err(id, "missing lo"); return; }
|
||
if (!json_get_str(json, "hi", hi_str, sizeof(hi_str))) { send_err(id, "missing hi"); return; }
|
||
uint32_t lo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||
uint32_t hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||
s_wtrace_ranges[0].lo = lo;
|
||
s_wtrace_ranges[0].hi = hi;
|
||
s_wtrace_range_count = (lo != hi) ? 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||
id, lo, hi);
|
||
}
|
||
|
||
static void handle_wtrace_add(int id, const char *json)
|
||
{
|
||
if (s_wtrace_range_count >= WTRACE_MAX_RANGES) {
|
||
send_err(id, "max ranges reached (8)"); return;
|
||
}
|
||
char lo_str[32], hi_str[32];
|
||
if (!json_get_str(json, "lo", lo_str, sizeof(lo_str))) { send_err(id, "missing lo"); return; }
|
||
if (!json_get_str(json, "hi", hi_str, sizeof(hi_str))) { send_err(id, "missing hi"); return; }
|
||
int slot = s_wtrace_range_count++;
|
||
s_wtrace_ranges[slot].lo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||
s_wtrace_ranges[slot].hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||
id, slot, s_wtrace_ranges[slot].lo, s_wtrace_ranges[slot].hi);
|
||
}
|
||
|
||
static void handle_wtrace_del(int id, const char *json)
|
||
{
|
||
int slot = json_get_int(json, "slot", -1);
|
||
if (slot < 0 || slot >= s_wtrace_range_count) {
|
||
send_err(id, "invalid slot"); return;
|
||
}
|
||
/* Compact: shift remaining slots down. */
|
||
for (int i = slot; i < s_wtrace_range_count - 1; i++)
|
||
s_wtrace_ranges[i] = s_wtrace_ranges[i + 1];
|
||
s_wtrace_range_count--;
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_wtrace_ranges(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const size_t BUF_SZ = 2048;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"count\":%d,\"ranges\":[", id, s_wtrace_range_count);
|
||
for (int i = 0; i < s_wtrace_range_count; i++) {
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"slot\":%d,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||
(i == 0) ? "" : ",",
|
||
i, s_wtrace_ranges[i].lo, s_wtrace_ranges[i].hi);
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_wtrace_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t oldest = (s_wtrace_seq <= WRITE_TRACE_CAP) ? 0 : s_wtrace_seq - WRITE_TRACE_CAP;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||
"\"oldest_seq\":%llu,\"newest_seq\":%llu,\"ranges\":%d}",
|
||
id, (unsigned long long)s_wtrace_seq, WRITE_TRACE_CAP,
|
||
(unsigned long long)oldest,
|
||
(unsigned long long)(s_wtrace_seq > 0 ? s_wtrace_seq - 1 : 0),
|
||
s_wtrace_range_count);
|
||
}
|
||
|
||
static void handle_wtrace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_wtrace_seq = 0;
|
||
s_wtrace_head = 0;
|
||
if (s_wtrace) memset(s_wtrace, 0, (size_t)WRITE_TRACE_CAP * sizeof(WriteTraceEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_wtrace_dump(int id, const char *json)
|
||
{
|
||
if (!s_wtrace) { send_err(id, "trace not initialized"); return; }
|
||
|
||
/* Optional post-hoc address filter. */
|
||
char lo_str[32], hi_str[32];
|
||
uint32_t filter_lo = 0, filter_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "addr_lo", lo_str, sizeof(lo_str)))
|
||
filter_lo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
|
||
filter_hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||
|
||
uint64_t total = s_wtrace_seq;
|
||
uint32_t avail = (total < WRITE_TRACE_CAP) ? (uint32_t)total : WRITE_TRACE_CAP;
|
||
uint32_t start = (total < WRITE_TRACE_CAP) ? 0 : s_wtrace_head;
|
||
|
||
/* Dynamically size buffer: each JSON entry is ~160 bytes max.
|
||
* Cap output at 8192 entries to avoid multi-MB responses. */
|
||
const uint32_t MAX_OUT = 8192;
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
uint32_t emitted = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||
id, (unsigned long long)total, avail);
|
||
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
|
||
uint32_t idx = (start + i) % WRITE_TRACE_CAP;
|
||
WriteTraceEntry *e = &s_wtrace[idx];
|
||
if (e->addr < filter_lo || e->addr >= filter_hi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"old\":\"0x%08X\","
|
||
"\"new\":\"0x%08X\",\"ra\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"frame\":%u,\"w\":%u}",
|
||
(emitted == 0) ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->addr, e->old_val, e->new_val, e->ra, e->func_addr,
|
||
e->frame, (unsigned)e->width);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
/* ---- MMIO trace handlers ---- */
|
||
|
||
static void handle_mmio_dump(int id, const char *json)
|
||
{
|
||
if (!s_mmio_trace) { send_err(id, "mmio trace not initialized"); return; }
|
||
|
||
/* Optional address filter. */
|
||
char addr_str[32];
|
||
uint32_t filter_addr = 0;
|
||
int has_filter = json_get_str(json, "addr", addr_str, sizeof(addr_str)) != NULL;
|
||
if (has_filter) filter_addr = hex_to_u32(addr_str);
|
||
|
||
uint64_t total = s_mmio_trace_seq;
|
||
uint32_t avail = (total < MMIO_TRACE_CAP) ? (uint32_t)total : MMIO_TRACE_CAP;
|
||
uint32_t start = (total < MMIO_TRACE_CAP) ? 0 : s_mmio_trace_head;
|
||
|
||
const uint32_t MAX_OUT = 8192;
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
uint32_t emitted = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||
id, (unsigned long long)total, avail);
|
||
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
|
||
uint32_t idx = (start + i) % MMIO_TRACE_CAP;
|
||
MmioTraceEntry *e = &s_mmio_trace[idx];
|
||
if (has_filter && e->addr != filter_addr) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"val\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"ra\":\"0x%08X\",\"frame\":%u,\"w\":%u}",
|
||
(emitted == 0) ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->addr, e->val, e->func_addr, e->ra,
|
||
e->frame, (unsigned)e->width);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_mmio_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_mmio_trace_seq = 0;
|
||
s_mmio_trace_head = 0;
|
||
if (s_mmio_trace) memset(s_mmio_trace, 0, (size_t)MMIO_TRACE_CAP * sizeof(MmioTraceEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_quit(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_ok(id);
|
||
debug_server_shutdown();
|
||
exit(0);
|
||
}
|
||
|
||
/* ---- Command dispatch table ---- */
|
||
|
||
/* dispatch_check: check if a specific address was ever dispatched */
|
||
static void handle_dispatch_check(int id, const char *json) {
|
||
char abuf[32] = {0};
|
||
json_get_str(json, "addr", abuf, sizeof(abuf));
|
||
uint32_t addr = (uint32_t)strtoul(abuf, NULL, 0);
|
||
int found = dispatch_trace_contains(addr);
|
||
debug_server_send_fmt("{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"found\":%s,\"total\":%llu}\n",
|
||
id, addr, found ? "true" : "false", (unsigned long long)s_dispatch_seq);
|
||
}
|
||
|
||
/* dispatch_tail: dump the last N dispatched function addresses */
|
||
static void handle_dispatch_tail(int id, const char *json) {
|
||
int count = 64;
|
||
{ /* try to parse count */
|
||
char buf[32];
|
||
if (json_get_str(json, "count", buf, sizeof(buf)))
|
||
count = atoi(buf);
|
||
}
|
||
if (count > 4096) count = 4096;
|
||
if ((uint64_t)count > s_dispatch_seq) count = (int)s_dispatch_seq;
|
||
|
||
debug_server_send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%d,\"addrs\":[",
|
||
id, (unsigned long long)s_dispatch_seq, count);
|
||
uint64_t start = s_dispatch_seq - count;
|
||
for (int i = 0; i < count; i++) {
|
||
uint32_t a = s_dispatch_ring[(start + i) % DISPATCH_TRACE_CAP];
|
||
if (i > 0) debug_server_send_fmt(",");
|
||
debug_server_send_fmt("\"0x%08X\"", a);
|
||
}
|
||
debug_server_send_fmt("]}\n");
|
||
}
|
||
|
||
/* ---- Function entry/exit trace handlers ---- */
|
||
|
||
static void handle_fn_filter(int id, const char *json) {
|
||
char buf[32];
|
||
if (json_get_str(json, "lo", buf, sizeof(buf))) s_fn_trace_filter_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "hi", buf, sizeof(buf))) s_fn_trace_filter_hi = hex_to_u32(buf);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}\n",
|
||
id, s_fn_trace_filter_lo, s_fn_trace_filter_hi);
|
||
}
|
||
|
||
static void handle_fn_clear(int id, const char *json) {
|
||
(void)json;
|
||
s_fn_entry_seq = 0;
|
||
s_fn_exit_seq = 0;
|
||
s_fn_stack_top = 0;
|
||
s_fn_prev_ra = 0;
|
||
s_fn_unmatched_returns = 0;
|
||
s_fn_stack_overflows = 0;
|
||
s_fn_tail_calls = 0;
|
||
if (s_fn_entry) memset(s_fn_entry, 0, (size_t)FN_TRACE_CAP * sizeof(FnEntryEntry));
|
||
if (s_fn_exit) memset(s_fn_exit, 0, (size_t)FN_TRACE_CAP * sizeof(FnExitEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_fn_stats(int id, const char *json) {
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"entry_total\":%llu,\"exit_total\":%llu,"
|
||
"\"stack_top\":%d,\"unmatched_returns\":%llu,"
|
||
"\"stack_overflows\":%llu,\"tail_calls\":%llu,"
|
||
"\"capacity\":%d,"
|
||
"\"filter_lo\":\"0x%08X\",\"filter_hi\":\"0x%08X\"}\n",
|
||
id,
|
||
(unsigned long long)s_fn_entry_seq,
|
||
(unsigned long long)s_fn_exit_seq,
|
||
s_fn_stack_top,
|
||
(unsigned long long)s_fn_unmatched_returns,
|
||
(unsigned long long)s_fn_stack_overflows,
|
||
(unsigned long long)s_fn_tail_calls,
|
||
FN_TRACE_CAP,
|
||
s_fn_trace_filter_lo, s_fn_trace_filter_hi);
|
||
}
|
||
|
||
/* Helper: parse filter / range / count for fn_*_dump. */
|
||
static void fn_dump_parse(const char *json, uint64_t total,
|
||
uint64_t *out_seq_lo, uint64_t *out_seq_hi,
|
||
uint32_t *out_addr_lo, uint32_t *out_addr_hi,
|
||
int *out_max) {
|
||
char buf[32];
|
||
*out_seq_lo = 0;
|
||
*out_seq_hi = total;
|
||
*out_addr_lo = 0;
|
||
*out_addr_hi = 0xFFFFFFFFu;
|
||
*out_max = 256;
|
||
if (json_get_str(json, "seq_lo", buf, sizeof(buf))) *out_seq_lo = strtoull(buf, NULL, 0);
|
||
if (json_get_str(json, "seq_hi", buf, sizeof(buf))) *out_seq_hi = strtoull(buf, NULL, 0);
|
||
if (json_get_str(json, "addr_lo", buf, sizeof(buf))) *out_addr_lo = hex_to_u32(buf) & 0x1FFFFFFFu;
|
||
if (json_get_str(json, "addr_hi", buf, sizeof(buf))) *out_addr_hi = hex_to_u32(buf) & 0x1FFFFFFFu;
|
||
*out_max = json_get_int(json, "count", 256);
|
||
if (*out_max < 1) *out_max = 1;
|
||
if (*out_max > 4096) *out_max = 4096;
|
||
/* If seq_lo not specified, default to "tail N entries". */
|
||
if (*out_seq_lo == 0 && total > (uint64_t)*out_max) {
|
||
*out_seq_lo = total - (uint64_t)*out_max;
|
||
}
|
||
}
|
||
|
||
static void handle_fn_entry_dump(int id, const char *json) {
|
||
if (!s_fn_entry) { send_err(id, "not initialized"); return; }
|
||
uint64_t seq_lo, seq_hi;
|
||
uint32_t addr_lo, addr_hi;
|
||
int max_count;
|
||
fn_dump_parse(json, s_fn_entry_seq, &seq_lo, &seq_hi, &addr_lo, &addr_hi, &max_count);
|
||
|
||
/* Earliest available seq in ring. */
|
||
uint64_t oldest = (s_fn_entry_seq > FN_TRACE_CAP) ? s_fn_entry_seq - FN_TRACE_CAP : 0;
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_hi > s_fn_entry_seq) seq_hi = s_fn_entry_seq;
|
||
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,"
|
||
"\"seq_lo\":%llu,\"seq_hi\":%llu,\"entries\":[",
|
||
id, (unsigned long long)s_fn_entry_seq,
|
||
(unsigned long long)oldest,
|
||
(unsigned long long)seq_lo, (unsigned long long)seq_hi);
|
||
int emitted = 0;
|
||
int first = 1;
|
||
for (uint64_t s = seq_lo; s < seq_hi && emitted < max_count && pos < BUF_SZ - 512; s++) {
|
||
FnEntryEntry *e = &s_fn_entry[s % FN_TRACE_CAP];
|
||
if (e->func_addr < addr_lo || e->func_addr >= addr_hi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"func\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t1\":\"0x%08X\",\"depth\":%u,\"frame\":%u,\"exit_seq\":%llu}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->func_addr, e->ra, e->a0, e->a1, e->a2, e->a3, e->t1,
|
||
e->depth, e->frame,
|
||
(unsigned long long)e->paired_exit_seq);
|
||
first = 0;
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_fn_exit_dump(int id, const char *json) {
|
||
if (!s_fn_exit) { send_err(id, "not initialized"); return; }
|
||
uint64_t seq_lo, seq_hi;
|
||
uint32_t addr_lo, addr_hi;
|
||
int max_count;
|
||
fn_dump_parse(json, s_fn_exit_seq, &seq_lo, &seq_hi, &addr_lo, &addr_hi, &max_count);
|
||
|
||
uint64_t oldest = (s_fn_exit_seq > FN_TRACE_CAP) ? s_fn_exit_seq - FN_TRACE_CAP : 0;
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_hi > s_fn_exit_seq) seq_hi = s_fn_exit_seq;
|
||
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,"
|
||
"\"seq_lo\":%llu,\"seq_hi\":%llu,\"entries\":[",
|
||
id, (unsigned long long)s_fn_exit_seq,
|
||
(unsigned long long)oldest,
|
||
(unsigned long long)seq_lo, (unsigned long long)seq_hi);
|
||
int emitted = 0;
|
||
int first = 1;
|
||
for (uint64_t s = seq_lo; s < seq_hi && emitted < max_count && pos < BUF_SZ - 512; s++) {
|
||
FnExitEntry *e = &s_fn_exit[s % FN_TRACE_CAP];
|
||
if (e->func_addr < addr_lo || e->func_addr >= addr_hi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"entry_seq\":%llu,\"func\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"depth\":%u,\"frame\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, (unsigned long long)e->entry_seq,
|
||
e->func_addr, e->v0, e->v1, e->depth, e->frame);
|
||
first = 0;
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
typedef void (*CmdHandler)(int id, const char *json);
|
||
typedef struct { const char *name; CmdHandler handler; } CmdEntry;
|
||
|
||
static const CmdEntry s_commands[] = {
|
||
{ "ping", handle_ping },
|
||
{ "frame", handle_frame },
|
||
{ "get_registers", handle_get_registers },
|
||
{ "read_ram", handle_read_ram },
|
||
{ "dump_ram", handle_dump_ram },
|
||
{ "write_ram", handle_write_ram },
|
||
{ "gpu_state", handle_gpu_state },
|
||
{ "vram_peek", handle_vram_peek },
|
||
{ "irq_state", handle_irq_state },
|
||
{ "sio_state", handle_sio_state },
|
||
{ "mc_status", handle_mc_status },
|
||
{ "chain_trace", handle_chain_trace },
|
||
{ "sio_trace", handle_sio_trace },
|
||
{ "card_txn_dump", handle_card_txn_dump },
|
||
{ "sio_irq_dump", handle_sio_irq_dump },
|
||
{ "evcb_snapshot", handle_evcb_snapshot },
|
||
{ "evcb_walk_dump", handle_evcb_walk_dump },
|
||
{ "evcb_walk_stats", handle_evcb_walk_stats },
|
||
{ "imask_trace", handle_imask_trace },
|
||
{ "watch", handle_watch },
|
||
{ "unwatch", handle_unwatch },
|
||
{ "wtrace_range", handle_wtrace_range },
|
||
{ "wtrace_add", handle_wtrace_add },
|
||
{ "wtrace_del", handle_wtrace_del },
|
||
{ "wtrace_ranges", handle_wtrace_ranges },
|
||
{ "wtrace_dump", handle_wtrace_dump },
|
||
{ "wtrace_clear", handle_wtrace_clear },
|
||
{ "wtrace_stats", handle_wtrace_stats },
|
||
{ "mmio_dump", handle_mmio_dump },
|
||
{ "mmio_clear", handle_mmio_clear },
|
||
{ "set_input", handle_set_input },
|
||
{ "press", handle_press },
|
||
{ "pad_status", handle_pad_status },
|
||
{ "clear_input", handle_clear_input },
|
||
{ "display_source", handle_display_source },
|
||
{ "pause", handle_pause },
|
||
{ "continue", handle_continue },
|
||
{ "step", handle_step },
|
||
{ "run_to_frame", handle_run_to_frame },
|
||
{ "history", handle_history },
|
||
{ "get_frame", handle_get_frame },
|
||
{ "frame_range", handle_frame_range },
|
||
{ "frame_timeseries", handle_frame_timeseries },
|
||
{ "first_failure", handle_first_failure },
|
||
{ "read_frame_ram", handle_read_frame_ram },
|
||
{ "set_snapshot", handle_set_snapshot },
|
||
{ "get_snapshots", handle_get_snapshots },
|
||
{ "screenshot", handle_screenshot },
|
||
{ "screenshot_file", handle_screenshot_file },
|
||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||
{ "a0_history", handle_a0_history },
|
||
{ "c0_history", handle_c0_history },
|
||
{ "capture_quads", handle_capture_quads },
|
||
{ "get_quads", handle_get_quads },
|
||
{ "gte_state", handle_gte_state },
|
||
{ "quit", handle_quit },
|
||
{ "dispatch_check", handle_dispatch_check },
|
||
{ "dispatch_tail", handle_dispatch_tail },
|
||
{ "fn_filter", handle_fn_filter },
|
||
{ "fn_clear", handle_fn_clear },
|
||
{ "fn_stats", handle_fn_stats },
|
||
{ "fn_entry_dump", handle_fn_entry_dump },
|
||
{ "fn_exit_dump", handle_fn_exit_dump },
|
||
{ NULL, NULL }
|
||
};
|
||
|
||
static void process_command(const char *line)
|
||
{
|
||
char cmd[64];
|
||
if (!json_get_str(line, "cmd", cmd, sizeof(cmd))) {
|
||
strncpy(cmd, line, sizeof(cmd) - 1);
|
||
cmd[sizeof(cmd) - 1] = '\0';
|
||
int len = (int)strlen(cmd);
|
||
while (len > 0 && (cmd[len-1] == '\r' || cmd[len-1] == ' '))
|
||
cmd[--len] = '\0';
|
||
}
|
||
|
||
int id = json_get_int(line, "id", 0);
|
||
|
||
for (const CmdEntry *e = s_commands; e->name; e++) {
|
||
if (strcmp(cmd, e->name) == 0) {
|
||
e->handler(id, line);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/* Oracle commands (find_first_divergence, emu_read_ram, etc.) */
|
||
#if defined(ENABLE_DUCKSTATION_ORACLE) || defined(ENABLE_BEETLE_PSX_ORACLE)
|
||
{
|
||
extern int psx_oracle_handle_cmd(const char *cmd, int id, const char *json);
|
||
if (psx_oracle_handle_cmd(cmd, id, line)) return;
|
||
}
|
||
#endif
|
||
|
||
send_err(id, "unknown command");
|
||
}
|
||
|
||
/* ---- Public API ---- */
|
||
|
||
/* Extended init that accepts a CPU state pointer for register queries. */
|
||
static CPUState *s_init_cpu = NULL;
|
||
CPUState *debug_cpu_ptr = NULL; /* Global, used by memory.c watchpoints */
|
||
|
||
void debug_server_set_cpu(CPUState *cpu)
|
||
{
|
||
s_cpu = cpu;
|
||
debug_cpu_ptr = cpu;
|
||
}
|
||
|
||
void debug_server_init(int port)
|
||
{
|
||
if (port > 0) s_port = port;
|
||
|
||
#ifdef _WIN32
|
||
WSADATA wsa;
|
||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||
#endif
|
||
|
||
s_listen = socket(AF_INET, SOCK_STREAM, 0);
|
||
if (s_listen == SOCK_INVALID) return;
|
||
|
||
int yes = 1;
|
||
setsockopt(s_listen, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
|
||
|
||
struct sockaddr_in addr;
|
||
memset(&addr, 0, sizeof(addr));
|
||
addr.sin_family = AF_INET;
|
||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||
addr.sin_port = htons((uint16_t)s_port);
|
||
|
||
if (bind(s_listen, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
||
sock_close(s_listen);
|
||
s_listen = SOCK_INVALID;
|
||
return;
|
||
}
|
||
|
||
listen(s_listen, 1);
|
||
set_nonblocking(s_listen);
|
||
|
||
if (!s_frame_history) {
|
||
s_frame_history = (PSXFrameRecord *)calloc(FRAME_HISTORY_CAP, sizeof(PSXFrameRecord));
|
||
}
|
||
s_history_count = 0;
|
||
|
||
/* Tier 1: heap-allocate write trace ring buffer (32 MB). */
|
||
if (!s_wtrace) {
|
||
s_wtrace = (WriteTraceEntry *)calloc(WRITE_TRACE_CAP, sizeof(WriteTraceEntry));
|
||
}
|
||
s_wtrace_seq = 0;
|
||
s_wtrace_head = 0;
|
||
|
||
/* Function entry/exit ring buffers (32 MB each, 64 MB total). */
|
||
if (!s_fn_entry) s_fn_entry = (FnEntryEntry *)calloc(FN_TRACE_CAP, sizeof(FnEntryEntry));
|
||
if (!s_fn_exit) s_fn_exit = (FnExitEntry *)calloc(FN_TRACE_CAP, sizeof(FnExitEntry));
|
||
s_fn_entry_seq = 0;
|
||
s_fn_exit_seq = 0;
|
||
s_fn_stack_top = 0;
|
||
s_fn_unmatched_returns = 0;
|
||
s_fn_stack_overflows = 0;
|
||
|
||
/* EvCB walk ring (~240 KB). */
|
||
if (!s_evcb_ring) s_evcb_ring = (EvCBSnapshot *)calloc(EVCB_RING_CAP, sizeof(EvCBSnapshot));
|
||
s_evcb_ring_seq = 0;
|
||
s_evcb_ring_entry_count = 0;
|
||
s_evcb_ring_exit_count = 0;
|
||
s_evcb_pending_active = 0;
|
||
s_evcb_unwound_count = 0;
|
||
|
||
/* Phase 4.5: watch EB4 area + DF8/DFC area + EvCB slot 1 status + state machine
|
||
* + spiral texture buffer. */
|
||
s_wtrace_ranges[0].lo = 0x00079EB0u;
|
||
s_wtrace_ranges[0].hi = 0x00079EC0u;
|
||
s_wtrace_ranges[1].lo = 0x00079DF0u;
|
||
s_wtrace_ranges[1].hi = 0x00079E04u;
|
||
s_wtrace_ranges[2].lo = 0x0000E044u; /* EvCB slot 1 (class+status+spec) */
|
||
s_wtrace_ranges[2].hi = 0x0000E054u;
|
||
s_wtrace_ranges[3].lo = 0x00066940u; /* shell state machine 0x80066940 */
|
||
s_wtrace_ranges[3].hi = 0x00066954u;
|
||
s_wtrace_ranges[4].lo = 0x001B6810u; /* spiral texture buffer (heap @ 0x801B6814) */
|
||
s_wtrace_ranges[4].hi = 0x001B6830u;
|
||
/* Card-chain visibility (always-on per global rule "use ring buffers, never
|
||
* sample"): 0x7514 is the shared chain counter, 0x7528[0..1] are the per-slot
|
||
* chain handler ptrs. Reading 0x7528 = 0x5688 means read-chain installed;
|
||
* = 0x5B64 means detection-chain installed. Catching every write to these
|
||
* tells us deterministically when card-read paths get armed. */
|
||
s_wtrace_ranges[5].lo = 0x00007514u;
|
||
s_wtrace_ranges[5].hi = 0x00007518u;
|
||
s_wtrace_ranges[6].lo = 0x00007528u; /* 0x7528[0] (slot 0 chain handler ptr) */
|
||
s_wtrace_ranges[6].hi = 0x00007530u; /* through 0x7528[1] (slot 1) */
|
||
/* 0x755A is the chain abort flag: D1 sets =1, outer coord clears+aborts on !=0 */
|
||
s_wtrace_ranges[7].lo = 0x0000755Au;
|
||
s_wtrace_ranges[7].hi = 0x0000755Cu;
|
||
/* 0x7520 is the success flag (state-3 sets, dispatcher v0=-1 cascade reads) */
|
||
s_wtrace_ranges[8].lo = 0x00007520u;
|
||
s_wtrace_ranges[8].hi = 0x00007524u;
|
||
/* 0x74A4 is the chain status flag */
|
||
s_wtrace_ranges[9].lo = 0x000074A4u;
|
||
s_wtrace_ranges[9].hi = 0x000074A8u;
|
||
s_wtrace_range_count = 10;
|
||
|
||
/* Tier 1: heap-allocate MMIO trace ring buffer (2 MB). */
|
||
if (!s_mmio_trace) {
|
||
s_mmio_trace = (MmioTraceEntry *)calloc(MMIO_TRACE_CAP, sizeof(MmioTraceEntry));
|
||
}
|
||
s_mmio_trace_seq = 0;
|
||
s_mmio_trace_head = 0;
|
||
|
||
memset(s_watchpoints, 0, sizeof(s_watchpoints));
|
||
memset(s_snapshot_addrs, 0, sizeof(s_snapshot_addrs));
|
||
memset(s_snapshot_active, 0, sizeof(s_snapshot_active));
|
||
}
|
||
|
||
void debug_server_poll(void)
|
||
{
|
||
if (s_listen == SOCK_INVALID) return;
|
||
|
||
if (s_client == SOCK_INVALID) {
|
||
struct sockaddr_in caddr;
|
||
int clen = sizeof(caddr);
|
||
sock_t c = accept(s_listen, (struct sockaddr *)&caddr, &clen);
|
||
if (c != SOCK_INVALID) {
|
||
s_client = c;
|
||
set_nonblocking(s_client);
|
||
s_recv_len = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
int space = RECV_BUF_SIZE - s_recv_len - 1;
|
||
if (space > 0) {
|
||
int n = recv(s_client, s_recv_buf + s_recv_len, space, 0);
|
||
if (n > 0) {
|
||
s_recv_len += n;
|
||
s_recv_buf[s_recv_len] = '\0';
|
||
} else if (n == 0) {
|
||
sock_close(s_client);
|
||
s_client = SOCK_INVALID;
|
||
s_paused = 0;
|
||
return;
|
||
} else {
|
||
int err = sock_error();
|
||
#ifdef _WIN32
|
||
if (err != WSAEWOULDBLOCK) {
|
||
#else
|
||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||
#endif
|
||
sock_close(s_client);
|
||
s_client = SOCK_INVALID;
|
||
s_paused = 0;
|
||
s_input_override = -1;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
char *nl;
|
||
while ((nl = strchr(s_recv_buf, '\n')) != NULL) {
|
||
*nl = '\0';
|
||
if (nl > s_recv_buf && *(nl - 1) == '\r')
|
||
*(nl - 1) = '\0';
|
||
if (s_recv_buf[0] != '\0')
|
||
process_command(s_recv_buf);
|
||
int consumed = (int)(nl - s_recv_buf) + 1;
|
||
s_recv_len -= consumed;
|
||
memmove(s_recv_buf, nl + 1, s_recv_len + 1);
|
||
}
|
||
}
|
||
|
||
void debug_server_record_frame(void)
|
||
{
|
||
if (!s_frame_history) return;
|
||
if (!s_cpu) return;
|
||
|
||
uint32_t idx = (uint32_t)(s_frame_count % FRAME_HISTORY_CAP);
|
||
PSXFrameRecord *r = &s_frame_history[idx];
|
||
|
||
r->frame_number = (uint32_t)s_frame_count;
|
||
r->verify_pass = -1;
|
||
r->diff_count = 0;
|
||
memset(r->diffs, 0, sizeof(r->diffs));
|
||
|
||
/* MIPS CPU state */
|
||
memcpy(r->gpr, s_cpu->gpr, sizeof(r->gpr));
|
||
r->hi = s_cpu->hi;
|
||
r->lo = s_cpu->lo;
|
||
r->cop0_sr = s_cpu->cop0[12];
|
||
r->cop0_cause = s_cpu->cop0[13];
|
||
r->cop0_epc = s_cpu->cop0[14];
|
||
|
||
/* Interrupt state */
|
||
r->i_stat = i_stat;
|
||
r->i_mask = i_mask;
|
||
|
||
/* GPU display state */
|
||
{
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
r->display_area_x = (uint16_t)di.display_x;
|
||
r->display_area_y = (uint16_t)di.display_y;
|
||
r->display_w = (uint16_t)di.width;
|
||
r->display_h = (uint16_t)di.height;
|
||
r->display_disabled = di.disabled;
|
||
}
|
||
|
||
/* SIO state */
|
||
r->pad_buttons = sio_get_pad_buttons();
|
||
r->sio_stat = (uint16_t)sio_read(0x1F801044);
|
||
r->sio_ctrl = (uint16_t)sio_read(0x1F80104A);
|
||
|
||
/* Timing */
|
||
r->dispatch_count = 0; /* filled externally if needed */
|
||
r->total_dispatches = s_frame_count;
|
||
|
||
/* Snapshot regions */
|
||
for (int i = 0; i < RAM_SNAPSHOT_REGIONS; i++) {
|
||
r->snapshot_addr[i] = s_snapshot_addrs[i];
|
||
if (s_snapshot_active[i] && s_snapshot_addrs[i] != 0) {
|
||
for (int j = 0; j < RAM_SNAPSHOT_SIZE; j++)
|
||
r->snapshot_data[i][j] = psx_read_byte(s_snapshot_addrs[i] + j);
|
||
} else {
|
||
memset(r->snapshot_data[i], 0, RAM_SNAPSHOT_SIZE);
|
||
}
|
||
}
|
||
|
||
/* Game-specific data */
|
||
memset(r->game_data, 0, sizeof(r->game_data));
|
||
|
||
/* Last function */
|
||
strcpy(r->last_func, "(no tracking)");
|
||
|
||
s_history_count = s_frame_count + 1;
|
||
s_frame_count++;
|
||
|
||
/* Step mode */
|
||
if (s_step_count > 0) {
|
||
s_step_count--;
|
||
if (s_step_count == 0) {
|
||
s_paused = 1;
|
||
send_fmt("{\"event\":\"step_done\",\"frame\":%llu}",
|
||
(unsigned long long)(s_frame_count - 1));
|
||
}
|
||
}
|
||
|
||
/* Run-to-frame */
|
||
if (s_run_to > 0 && s_frame_count - 1 >= s_run_to) {
|
||
s_paused = 1;
|
||
s_run_to = 0;
|
||
send_fmt("{\"event\":\"run_to_done\",\"frame\":%llu}",
|
||
(unsigned long long)(s_frame_count - 1));
|
||
}
|
||
}
|
||
|
||
void debug_server_wait_if_paused(void)
|
||
{
|
||
while (s_paused) {
|
||
debug_server_poll();
|
||
|
||
SDL_Event ev;
|
||
while (SDL_PollEvent(&ev)) {
|
||
if (ev.type == SDL_QUIT) exit(0);
|
||
}
|
||
|
||
SDL_Delay(5);
|
||
}
|
||
}
|
||
|
||
void debug_server_check_watchpoints(void)
|
||
{
|
||
if (s_client == SOCK_INVALID) return;
|
||
|
||
for (int i = 0; i < MAX_WATCHPOINTS; i++) {
|
||
if (!s_watchpoints[i].active) continue;
|
||
uint8_t cur = psx_read_byte(s_watchpoints[i].addr);
|
||
if (cur != s_watchpoints[i].prev_val) {
|
||
send_fmt("{\"event\":\"watchpoint\","
|
||
"\"addr\":\"0x%08X\",\"old\":\"0x%02X\",\"new\":\"0x%02X\","
|
||
"\"frame\":%llu}",
|
||
s_watchpoints[i].addr,
|
||
s_watchpoints[i].prev_val, cur,
|
||
(unsigned long long)s_frame_count);
|
||
s_watchpoints[i].prev_val = cur;
|
||
}
|
||
}
|
||
}
|
||
|
||
void debug_server_shutdown(void)
|
||
{
|
||
if (s_client != SOCK_INVALID) {
|
||
sock_close(s_client);
|
||
s_client = SOCK_INVALID;
|
||
}
|
||
if (s_listen != SOCK_INVALID) {
|
||
sock_close(s_listen);
|
||
s_listen = SOCK_INVALID;
|
||
}
|
||
#ifdef _WIN32
|
||
WSACleanup();
|
||
#endif
|
||
}
|
||
|
||
int debug_server_is_connected(void)
|
||
{
|
||
return s_client != SOCK_INVALID;
|
||
}
|
||
|
||
int debug_server_get_input_override(void)
|
||
{
|
||
if (s_input_override >= 0 && s_input_frames > 0) {
|
||
if (--s_input_frames == 0)
|
||
s_input_override = -1;
|
||
}
|
||
return s_input_override;
|
||
}
|