mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
ca6f423edb
New debug server commands for the GL backend: gl_fbo_peek reads a rect of GPU-side VRAM (via the pack pass) without touching the CPU array, gl_vram_diff reports full-VRAM FBO-vs-CPU mismatch count/bbox/samples, and gl_coh_ring dumps the always-on coherency event ring (last n events, optional frame_min filter). crash_trace SEH dumps now report the faulting module + base + offset and scan the stack for executable return addresses, so dumps stay addr2line-able despite ASLR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9164 lines
366 KiB
C
9164 lines
366 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.
|
||
*/
|
||
/* Expose POSIX clock_gettime()/CLOCK_MONOTONIC (used by monotonic_ms) on
|
||
* glibc — must precede any system header. Harmless on Windows/macOS. */
|
||
#ifndef _POSIX_C_SOURCE
|
||
# define _POSIX_C_SOURCE 200809L
|
||
#endif
|
||
#include <time.h>
|
||
#include "debug_server.h"
|
||
#include "overlay_loader.h"
|
||
#include "overlay_capture.h"
|
||
#include "cpu_state.h"
|
||
#include "dma.h"
|
||
#include "gpu.h"
|
||
#include "cdrom.h"
|
||
#include "sio.h"
|
||
#include "memcard.h"
|
||
#include "spu.h"
|
||
#include "mdec.h"
|
||
#include "interrupts.h"
|
||
#include "psx_cycles.h"
|
||
#include "timers.h"
|
||
#include "dirty_ram_interp.h"
|
||
#include "card_read_summary.h"
|
||
#include "card_data_writes.h"
|
||
#include "crash_trace.h"
|
||
#include "gpu_gl_renderer.h"
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <stdint.h>
|
||
|
||
#ifndef DEFAULT_DEBUG_PORT
|
||
#error DEFAULT_DEBUG_PORT must be defined by the runtime target.
|
||
#endif
|
||
|
||
/* ---- 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 <sys/time.h> /* struct timeval — socket send/recv timeouts */
|
||
# 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 = DEFAULT_DEBUG_PORT;
|
||
|
||
#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) ---- */
|
||
/* Non-static so other instrumentation (e.g. dirty_ram_interp.c) can stamp
|
||
* ring-buffer entries with the current frame for cross-correlation. */
|
||
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;
|
||
|
||
/* ---- Dirty-RAM one-shot break ---- */
|
||
static volatile int s_dirty_break_active = 0;
|
||
static uint32_t s_dirty_break_lo = 0;
|
||
static uint32_t s_dirty_break_hi = 0;
|
||
static uint32_t s_dirty_break_target = 0;
|
||
static uint32_t s_dirty_break_ra = 0;
|
||
static uint32_t s_dirty_break_a0 = 0;
|
||
static uint32_t s_dirty_break_a1 = 0;
|
||
static uint32_t s_dirty_break_a2 = 0;
|
||
static uint32_t s_dirty_break_a3 = 0;
|
||
static uint32_t s_dirty_break_sp = 0;
|
||
static uint32_t s_dirty_break_frame = 0;
|
||
static uint64_t s_dirty_break_hits = 0;
|
||
|
||
/* ---- Input override ---- */
|
||
static int s_input_override = -1;
|
||
static int s_input_frames = 0;
|
||
|
||
/* ---- Frontend turbo override ---- */
|
||
static volatile int s_turbo_enabled = 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 the configurable address ranges.
|
||
* 1M-entry ring buffer, heap-allocated in debug_server_init(). */
|
||
/* Ring caps were sized for hour-long captures; that's ~700 MB if every
|
||
* ring is touched. Most diagnostic flows need seconds, not hours. Drop
|
||
* to 256K-entry caps unless an explicit need for more arises — at 60Hz
|
||
* and ~thousand events/frame that's still ~4 sec of coverage per ring,
|
||
* and total runtime memory stays under ~100 MB worst-case. */
|
||
#define WRITE_TRACE_CAP (1 << 18)
|
||
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 pc; /* g_debug_last_store_pc — exact PC of the SW/SH/SB */
|
||
uint32_t cpu_pc; /* CPUState::pc at the moment of the write */
|
||
uint32_t sp;
|
||
uint32_t v0;
|
||
uint32_t v1;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t t0;
|
||
uint32_t t1;
|
||
uint32_t frame; /* VBlank frame number */
|
||
uint8_t width; /* 1, 2, or 4 */
|
||
uint8_t pad[3]; /* align to 8 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;
|
||
|
||
/* Boot-pinned write trace. This is intentionally not a ring: it captures the
|
||
* first writes to a few high-value startup ranges and then stops, so late
|
||
* probes can answer "what initialized/reset this?" even after the normal trace
|
||
* has rolled. */
|
||
#define WRITE_TRACE_BOOT_CAP (1 << 18)
|
||
static WriteTraceEntry *s_wtrace_boot = NULL;
|
||
static uint64_t s_wtrace_boot_total = 0; /* matching writes ever seen */
|
||
static uint32_t s_wtrace_boot_count = 0; /* entries retained */
|
||
#define WTRACE_BOOT_MAX_RANGES 12
|
||
static struct { uint32_t lo, hi; } s_wtrace_boot_ranges[WTRACE_BOOT_MAX_RANGES];
|
||
static int s_wtrace_boot_range_count = 0;
|
||
|
||
/* Multi-range filter: up to 64 [lo, hi) address ranges. Boot defaults
|
||
* occupy ~15; investigative arms must always have headroom. */
|
||
#define WTRACE_MAX_RANGES 64
|
||
static struct { uint32_t lo, hi; } s_wtrace_ranges[WTRACE_MAX_RANGES];
|
||
static int s_wtrace_range_count = 0;
|
||
|
||
/* ---- wtrace_all ring (ALWAYS-ON; no filter; lean fields) ----
|
||
* Parity with psx-beetle's s_wtrace_all. Every recompiled-code write
|
||
* to RAM lands here unconditionally, so a probe that connects AFTER
|
||
* an event can query the write history without needing to have pre-armed
|
||
* a range. Lean record (no register window) keeps 4M entries at ~128 MB;
|
||
* this is intentionally large enough to retain Tomba's boot-to-OPTIONS
|
||
* window for post-hoc initialization questions. */
|
||
#define WRITE_TRACE_ALL_CAP (1 << 22)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t addr;
|
||
uint32_t new_val;
|
||
uint32_t pc;
|
||
uint32_t ra;
|
||
uint32_t frame;
|
||
uint8_t w;
|
||
uint8_t pad[3];
|
||
} WriteTraceAllEntry;
|
||
static WriteTraceAllEntry *s_wtrace_all = NULL;
|
||
static uint64_t s_wtrace_all_seq = 0;
|
||
static uint32_t s_wtrace_all_head = 0;
|
||
|
||
/* ---- wtrace_transition ring (ALWAYS-ON; selected ranges; value changes only)
|
||
* The catch-all write ring intentionally records every write, but hot paths can
|
||
* roll it before a boot-to-menu transition is diagnosed. This ring keeps a
|
||
* long-lived timeseries for high-value scheduler/render state by recording only
|
||
* writes whose value changed. */
|
||
#define WRITE_TRACE_TRANS_CAP (1 << 20)
|
||
#define WTRACE_TRANS_MAX_RANGES 16
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t addr;
|
||
uint32_t old_val;
|
||
uint32_t new_val;
|
||
uint32_t pc;
|
||
uint32_t func_addr;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t s0;
|
||
uint32_t s1;
|
||
uint32_t s2;
|
||
uint32_t s3;
|
||
uint32_t stk20;
|
||
uint32_t stk40;
|
||
uint32_t frame;
|
||
uint8_t width;
|
||
uint8_t pad[3];
|
||
} WriteTraceTransEntry;
|
||
static WriteTraceTransEntry *s_wtrace_trans = NULL;
|
||
static uint64_t s_wtrace_trans_seq = 0;
|
||
static uint32_t s_wtrace_trans_head = 0;
|
||
static struct { uint32_t lo, hi; } s_wtrace_trans_ranges[WTRACE_TRANS_MAX_RANGES];
|
||
static int s_wtrace_trans_range_count = 0;
|
||
|
||
/* Function attribution global — set by psx_dispatch() before each call. */
|
||
uint32_t g_debug_current_func_addr = 0;
|
||
|
||
/* Last store PC — set by recompiler emitter before every store instruction. */
|
||
uint32_t g_debug_last_store_pc = 0;
|
||
|
||
/* Static dispatch hit counter — incremented by generated dispatch code on
|
||
* every binary-search hit (i.e. successfully dispatched to static C). */
|
||
uint64_t g_dispatch_static_hits = 0;
|
||
|
||
/* ---- SIO write PC tracer ring ----
|
||
* Captures (pc, addr, value, byte_seq, ctr) for every write to a SIO
|
||
* register, attributing the exact writing instruction. Used to find what
|
||
* code is putting bytes on the SIO bus when chain-dispatcher attribution
|
||
* (g_debug_current_func_addr) is too coarse.
|
||
*
|
||
* 1<<22 entries x 32 bytes = 128 MiB. This is intentionally much larger
|
||
* than the old 64K ring because BIOS pad/card polling can burn through
|
||
* tens of thousands of MMIO writes before a useful post-failure query. */
|
||
#define SIO_PC_TRACE_CAP (1 << 18)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t pc; /* g_debug_last_store_pc at the moment of write */
|
||
uint32_t func; /* g_debug_current_func_addr — outer frame */
|
||
uint32_t addr; /* full MMIO address written */
|
||
uint32_t value; /* value written (low 16/32 bits) */
|
||
uint32_t byte_seq; /* sio_get_seq() — cross-ref with sio_trace */
|
||
uint8_t width; /* 1=byte, 2=half, 4=word */
|
||
uint8_t pad[3];
|
||
} SioPcTraceEntry;
|
||
static SioPcTraceEntry s_sio_pc_trace[SIO_PC_TRACE_CAP];
|
||
static uint64_t s_sio_pc_trace_seq = 0;
|
||
|
||
/* Compact register sidecar for SIO_CTRL writes. The broad SIO PC ring keeps
|
||
* the long timeline; this smaller ring carries the CPU state needed to explain
|
||
* BIOS chain-driver branch decisions around SELECT resets. */
|
||
#define SIO_CTRL_REG_TRACE_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t pc;
|
||
uint32_t func;
|
||
uint32_t value;
|
||
uint32_t byte_seq;
|
||
uint32_t cpu_pc;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t v0;
|
||
uint32_t v1;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t sr;
|
||
uint32_t epc;
|
||
uint32_t istat;
|
||
uint32_t imask;
|
||
uint8_t width;
|
||
uint8_t in_exception;
|
||
uint8_t counter_7514;
|
||
uint8_t pad;
|
||
} SioCtrlRegTraceEntry;
|
||
static SioCtrlRegTraceEntry s_sio_ctrl_reg_trace[SIO_CTRL_REG_TRACE_CAP];
|
||
static uint64_t s_sio_ctrl_reg_trace_seq = 0;
|
||
|
||
/* RestoreState / exception longjmp trace. This is intentionally compact:
|
||
* the high-volume SIO/MMIO rings show what happened on the bus, while this
|
||
* ring shows whether exception nonlocal control flow skipped a callback's
|
||
* normal return-value cleanup. */
|
||
#define RESTORE_TRACE_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t kind;
|
||
uint32_t jmp_val;
|
||
uint32_t target_pc;
|
||
uint32_t cpu_pc;
|
||
uint32_t func;
|
||
uint32_t last_store_pc;
|
||
uint32_t byte_seq;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t v0;
|
||
uint32_t v1;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t s0;
|
||
uint32_t s1;
|
||
uint32_t sr;
|
||
uint32_t epc;
|
||
uint32_t istat;
|
||
uint32_t imask;
|
||
uint32_t frame;
|
||
uint8_t in_exception;
|
||
uint8_t pad[3];
|
||
} RestoreTraceEntry;
|
||
static RestoreTraceEntry s_restore_trace[RESTORE_TRACE_CAP];
|
||
static uint64_t s_restore_trace_seq = 0;
|
||
|
||
#define THREAD_TRACE_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t kind;
|
||
uint32_t current_tcb;
|
||
uint32_t target_tcb;
|
||
uint32_t current_state;
|
||
uint32_t target_state;
|
||
uint32_t current_tcb_ptr;
|
||
uint32_t target_pc;
|
||
uint32_t func;
|
||
uint32_t last_store_pc;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t s0;
|
||
uint32_t s1;
|
||
uint32_t s2;
|
||
uint32_t s3;
|
||
uint32_t sr;
|
||
uint32_t epc;
|
||
uint32_t saved_a0;
|
||
uint32_t saved_a1;
|
||
uint32_t saved_a2;
|
||
uint32_t saved_a3;
|
||
uint32_t saved_s0;
|
||
uint32_t saved_s1;
|
||
uint32_t saved_s2;
|
||
uint32_t saved_s3;
|
||
uint32_t saved_sp;
|
||
uint32_t saved_ra;
|
||
uint32_t saved_pc;
|
||
uint32_t saved_sr;
|
||
uint32_t task_ptr;
|
||
uint32_t task_state;
|
||
uint32_t task_mode;
|
||
uint32_t task_submode;
|
||
uint32_t istat;
|
||
uint32_t imask;
|
||
uint32_t frame;
|
||
uint8_t in_exception;
|
||
uint8_t pad[3];
|
||
} ThreadTraceEntry;
|
||
static ThreadTraceEntry s_thread_trace[THREAD_TRACE_CAP];
|
||
static uint64_t s_thread_trace_seq = 0;
|
||
|
||
#define SREG_TRACE_CAP (1 << 18)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t tcb;
|
||
uint32_t func;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t s0, s1, s2, s3, s4, s5, s6, s7;
|
||
uint32_t prev_s0, prev_s1, prev_s2, prev_s3;
|
||
uint32_t a0, a1, a2, a3;
|
||
uint32_t stack10, stack14, stack18, stack1c;
|
||
uint32_t stack20, stack28, stack40;
|
||
uint32_t task_ptr;
|
||
uint32_t task_state;
|
||
uint32_t task_mode;
|
||
uint32_t task_submode;
|
||
uint32_t frame;
|
||
uint8_t reason;
|
||
uint8_t pad[3];
|
||
} SregTraceEntry;
|
||
|
||
typedef struct {
|
||
uint32_t tcb;
|
||
uint32_t s[8];
|
||
int valid;
|
||
} SregLastEntry;
|
||
|
||
static SregTraceEntry s_sreg_trace[SREG_TRACE_CAP];
|
||
static uint64_t s_sreg_trace_seq = 0;
|
||
static SregLastEntry s_sreg_last[32];
|
||
|
||
#define PROBE_TRACE_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t pc;
|
||
uint32_t func;
|
||
uint32_t last_store_pc;
|
||
uint32_t byte_seq;
|
||
uint32_t ra;
|
||
uint32_t sp;
|
||
uint32_t v0;
|
||
uint32_t v1;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t sr;
|
||
uint32_t epc;
|
||
uint32_t istat;
|
||
uint32_t imask;
|
||
uint32_t frame;
|
||
uint8_t in_exception;
|
||
uint8_t pad[3];
|
||
} ProbeTraceEntry;
|
||
static ProbeTraceEntry s_probe_trace[PROBE_TRACE_CAP];
|
||
static uint64_t s_probe_trace_seq = 0;
|
||
|
||
void debug_server_log_probe(uint32_t pc, CPUState *cpu)
|
||
{
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)pc; (void)cpu;
|
||
return;
|
||
#else
|
||
if (!cpu) return;
|
||
ProbeTraceEntry *e = &s_probe_trace[s_probe_trace_seq % PROBE_TRACE_CAP];
|
||
e->seq = s_probe_trace_seq++;
|
||
e->pc = pc;
|
||
e->func = g_debug_current_func_addr;
|
||
e->last_store_pc = g_debug_last_store_pc;
|
||
e->byte_seq = sio_get_seq();
|
||
e->ra = cpu->gpr[31];
|
||
e->sp = cpu->gpr[29];
|
||
e->v0 = cpu->gpr[2];
|
||
e->v1 = cpu->gpr[3];
|
||
e->a0 = cpu->gpr[4];
|
||
e->a1 = cpu->gpr[5];
|
||
e->a2 = cpu->gpr[6];
|
||
e->a3 = cpu->gpr[7];
|
||
e->sr = cpu->cop0[12];
|
||
e->epc = cpu->cop0[14];
|
||
e->istat = i_stat;
|
||
e->imask = i_mask;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->in_exception = (uint8_t)psx_get_in_exception();
|
||
#endif /* PSX_NO_DEBUG_TOOLS */
|
||
}
|
||
|
||
void debug_server_log_restore_event(uint32_t kind, uint32_t target_pc, uint32_t jmp_val)
|
||
{
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)kind; (void)target_pc; (void)jmp_val;
|
||
return;
|
||
#endif
|
||
RestoreTraceEntry *e =
|
||
&s_restore_trace[s_restore_trace_seq % RESTORE_TRACE_CAP];
|
||
CPUState *cpu = s_cpu;
|
||
e->seq = s_restore_trace_seq++;
|
||
e->kind = kind;
|
||
e->jmp_val = jmp_val;
|
||
e->target_pc = target_pc;
|
||
e->cpu_pc = cpu ? cpu->pc : 0;
|
||
e->func = g_debug_current_func_addr;
|
||
e->last_store_pc = g_debug_last_store_pc;
|
||
e->byte_seq = sio_get_seq();
|
||
e->ra = cpu ? cpu->gpr[31] : 0;
|
||
e->sp = cpu ? cpu->gpr[29] : 0;
|
||
e->v0 = cpu ? cpu->gpr[2] : 0;
|
||
e->v1 = cpu ? cpu->gpr[3] : 0;
|
||
e->a0 = cpu ? cpu->gpr[4] : 0;
|
||
e->a1 = cpu ? cpu->gpr[5] : 0;
|
||
e->a2 = cpu ? cpu->gpr[6] : 0;
|
||
e->a3 = cpu ? cpu->gpr[7] : 0;
|
||
e->s0 = cpu ? cpu->gpr[16] : 0;
|
||
e->s1 = cpu ? cpu->gpr[17] : 0;
|
||
e->sr = cpu ? cpu->cop0[12] : 0;
|
||
e->epc = cpu ? cpu->cop0[14] : 0;
|
||
e->istat = i_stat;
|
||
e->imask = i_mask;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->in_exception = (uint8_t)psx_get_in_exception();
|
||
}
|
||
|
||
static uint32_t trace_read_word(CPUState *cpu, uint32_t addr)
|
||
{
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
if (phys > 0x001FFFFCu &&
|
||
(phys < 0x1F800000u || phys > 0x1F8003FCu)) {
|
||
return 0;
|
||
}
|
||
return cpu ? cpu->read_word(addr) : 0;
|
||
}
|
||
|
||
static uint32_t trace_read_half(CPUState *cpu, uint32_t addr)
|
||
{
|
||
uint32_t phys = addr & 0x1FFFFFFFu;
|
||
if (phys > 0x001FFFFEu &&
|
||
(phys < 0x1F800000u || phys > 0x1F8003FEu)) {
|
||
return 0;
|
||
}
|
||
return cpu ? cpu->read_half(addr) : 0;
|
||
}
|
||
|
||
static int sreg_trace_focus_func(uint32_t func)
|
||
{
|
||
switch (func) {
|
||
case 0x800171D4u:
|
||
case 0x8001A51Cu:
|
||
case 0x8001A670u:
|
||
case 0x8001A774u:
|
||
case 0x8001A954u:
|
||
case 0x8001CE80u:
|
||
case 0x8001DE24u:
|
||
case 0x80021C24u:
|
||
case 0x80021CC8u:
|
||
case 0x800222B8u:
|
||
case 0x800223E0u:
|
||
case 0x8005B40Cu:
|
||
return 1;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static SregLastEntry *sreg_last_slot(uint32_t tcb)
|
||
{
|
||
int free_idx = -1;
|
||
for (int i = 0; i < (int)(sizeof(s_sreg_last) / sizeof(s_sreg_last[0])); i++) {
|
||
if (s_sreg_last[i].valid && s_sreg_last[i].tcb == tcb) {
|
||
return &s_sreg_last[i];
|
||
}
|
||
if (!s_sreg_last[i].valid && free_idx < 0) {
|
||
free_idx = i;
|
||
}
|
||
}
|
||
if (free_idx < 0) {
|
||
free_idx = (int)(tcb % (uint32_t)(sizeof(s_sreg_last) / sizeof(s_sreg_last[0])));
|
||
}
|
||
memset(&s_sreg_last[free_idx], 0, sizeof(s_sreg_last[free_idx]));
|
||
s_sreg_last[free_idx].valid = 1;
|
||
s_sreg_last[free_idx].tcb = tcb;
|
||
return &s_sreg_last[free_idx];
|
||
}
|
||
|
||
static void sreg_trace_record(uint32_t func_addr)
|
||
{
|
||
CPUState *cpu = debug_cpu_ptr;
|
||
if (!cpu) return;
|
||
|
||
uint32_t tcb_ptr_addr = trace_read_word(cpu, 0x00000108u);
|
||
uint32_t tcb = tcb_ptr_addr ? trace_read_word(cpu, tcb_ptr_addr) : 0;
|
||
uint32_t tcb_phys = tcb & 0x1FFFFFFFu;
|
||
if (tcb == 0 || tcb_phys < 0x0000E000u || tcb_phys >= 0x0000F000u) {
|
||
return;
|
||
}
|
||
int focus_func = sreg_trace_focus_func(func_addr);
|
||
if (!focus_func) {
|
||
return;
|
||
}
|
||
uint32_t task_ptr = trace_read_word(cpu, 0x1F8001D4u);
|
||
uint32_t task_state = task_ptr ? trace_read_word(cpu, task_ptr) : 0;
|
||
uint32_t task_mode = task_ptr ? trace_read_half(cpu, task_ptr + 72u) : 0;
|
||
uint32_t task_submode = task_ptr ? trace_read_half(cpu, task_ptr + 74u) : 0;
|
||
|
||
SregLastEntry *last = sreg_last_slot(tcb);
|
||
uint32_t cur[8] = {
|
||
cpu->gpr[16], cpu->gpr[17], cpu->gpr[18], cpu->gpr[19],
|
||
cpu->gpr[20], cpu->gpr[21], cpu->gpr[22], cpu->gpr[23]
|
||
};
|
||
uint8_t reason = 0;
|
||
if (!last->valid) {
|
||
reason |= 1u;
|
||
}
|
||
for (int i = 0; i < 8; i++) {
|
||
if (last->s[i] != cur[i]) {
|
||
reason |= 2u;
|
||
break;
|
||
}
|
||
}
|
||
if (focus_func) {
|
||
reason |= 4u;
|
||
}
|
||
if (!reason) {
|
||
return;
|
||
}
|
||
|
||
SregTraceEntry *e = &s_sreg_trace[s_sreg_trace_seq % SREG_TRACE_CAP];
|
||
e->seq = s_sreg_trace_seq++;
|
||
e->tcb = tcb;
|
||
e->func = func_addr;
|
||
e->ra = cpu->gpr[31];
|
||
e->sp = cpu->gpr[29];
|
||
e->s0 = cur[0]; e->s1 = cur[1]; e->s2 = cur[2]; e->s3 = cur[3];
|
||
e->s4 = cur[4]; e->s5 = cur[5]; e->s6 = cur[6]; e->s7 = cur[7];
|
||
e->prev_s0 = last->s[0];
|
||
e->prev_s1 = last->s[1];
|
||
e->prev_s2 = last->s[2];
|
||
e->prev_s3 = last->s[3];
|
||
e->a0 = cpu->gpr[4];
|
||
e->a1 = cpu->gpr[5];
|
||
e->a2 = cpu->gpr[6];
|
||
e->a3 = cpu->gpr[7];
|
||
e->stack10 = trace_read_word(cpu, cpu->gpr[29] + 0x10u);
|
||
e->stack14 = trace_read_word(cpu, cpu->gpr[29] + 0x14u);
|
||
e->stack18 = trace_read_word(cpu, cpu->gpr[29] + 0x18u);
|
||
e->stack1c = trace_read_word(cpu, cpu->gpr[29] + 0x1Cu);
|
||
e->stack20 = trace_read_word(cpu, cpu->gpr[29] + 0x20u);
|
||
e->stack28 = trace_read_word(cpu, cpu->gpr[29] + 0x28u);
|
||
e->stack40 = trace_read_word(cpu, cpu->gpr[29] + 0x40u);
|
||
e->task_ptr = task_ptr;
|
||
e->task_state = task_state;
|
||
e->task_mode = task_mode;
|
||
e->task_submode = task_submode;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->reason = reason;
|
||
|
||
memcpy(last->s, cur, sizeof(cur));
|
||
}
|
||
|
||
void debug_server_log_thread_event(uint32_t kind, CPUState *cpu,
|
||
uint32_t current_tcb,
|
||
uint32_t target_tcb,
|
||
uint32_t target_pc)
|
||
{
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)kind; (void)cpu; (void)current_tcb; (void)target_tcb; (void)target_pc;
|
||
return;
|
||
#endif
|
||
if (!cpu) return;
|
||
ThreadTraceEntry *e =
|
||
&s_thread_trace[s_thread_trace_seq % THREAD_TRACE_CAP];
|
||
uint32_t tcb_ptr_addr = trace_read_word(cpu, 0x00000108u);
|
||
uint32_t save = target_tcb + 8u;
|
||
e->seq = s_thread_trace_seq++;
|
||
e->kind = kind;
|
||
e->current_tcb = current_tcb;
|
||
e->target_tcb = target_tcb;
|
||
e->current_state = current_tcb ? trace_read_word(cpu, current_tcb) : 0;
|
||
e->target_state = target_tcb ? trace_read_word(cpu, target_tcb) : 0;
|
||
e->current_tcb_ptr = tcb_ptr_addr ? trace_read_word(cpu, tcb_ptr_addr) : 0;
|
||
e->target_pc = target_pc;
|
||
e->func = g_debug_current_func_addr;
|
||
e->last_store_pc = g_debug_last_store_pc;
|
||
e->ra = cpu->gpr[31];
|
||
e->sp = cpu->gpr[29];
|
||
e->a0 = cpu->gpr[4];
|
||
e->a1 = cpu->gpr[5];
|
||
e->a2 = cpu->gpr[6];
|
||
e->a3 = cpu->gpr[7];
|
||
e->s0 = cpu->gpr[16];
|
||
e->s1 = cpu->gpr[17];
|
||
e->s2 = cpu->gpr[18];
|
||
e->s3 = cpu->gpr[19];
|
||
e->sr = cpu->cop0[12];
|
||
e->epc = cpu->cop0[14];
|
||
e->saved_a0 = target_tcb ? trace_read_word(cpu, save + 16u) : 0;
|
||
e->saved_a1 = target_tcb ? trace_read_word(cpu, save + 20u) : 0;
|
||
e->saved_a2 = target_tcb ? trace_read_word(cpu, save + 24u) : 0;
|
||
e->saved_a3 = target_tcb ? trace_read_word(cpu, save + 28u) : 0;
|
||
e->saved_s0 = target_tcb ? trace_read_word(cpu, save + 64u) : 0;
|
||
e->saved_s1 = target_tcb ? trace_read_word(cpu, save + 68u) : 0;
|
||
e->saved_s2 = target_tcb ? trace_read_word(cpu, save + 72u) : 0;
|
||
e->saved_s3 = target_tcb ? trace_read_word(cpu, save + 76u) : 0;
|
||
e->saved_sp = target_tcb ? trace_read_word(cpu, save + 116u) : 0;
|
||
e->saved_ra = target_tcb ? trace_read_word(cpu, save + 124u) : 0;
|
||
e->saved_pc = target_tcb ? trace_read_word(cpu, save + 128u) : 0;
|
||
e->saved_sr = target_tcb ? trace_read_word(cpu, save + 140u) : 0;
|
||
e->task_ptr = trace_read_word(cpu, 0x1F8001D4u);
|
||
e->task_state = e->task_ptr ? trace_read_word(cpu, e->task_ptr) : 0;
|
||
e->task_mode = e->task_ptr ? trace_read_half(cpu, e->task_ptr + 72u) : 0;
|
||
e->task_submode = e->task_ptr ? trace_read_half(cpu, e->task_ptr + 74u) : 0;
|
||
e->istat = i_stat;
|
||
e->imask = i_mask;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->in_exception = (uint8_t)psx_get_in_exception();
|
||
}
|
||
|
||
static void debug_server_log_sio_ctrl_regs(uint32_t value, uint8_t width,
|
||
uint32_t byte_seq) {
|
||
SioCtrlRegTraceEntry *e =
|
||
&s_sio_ctrl_reg_trace[s_sio_ctrl_reg_trace_seq % SIO_CTRL_REG_TRACE_CAP];
|
||
CPUState *cpu = s_cpu;
|
||
e->seq = s_sio_ctrl_reg_trace_seq++;
|
||
e->pc = g_debug_last_store_pc;
|
||
e->func = g_debug_current_func_addr;
|
||
e->value = value;
|
||
e->byte_seq = byte_seq;
|
||
e->cpu_pc = cpu ? cpu->pc : 0;
|
||
e->ra = cpu ? cpu->gpr[31] : 0;
|
||
e->sp = cpu ? cpu->gpr[29] : 0;
|
||
e->v0 = cpu ? cpu->gpr[2] : 0;
|
||
e->v1 = cpu ? cpu->gpr[3] : 0;
|
||
e->a0 = cpu ? cpu->gpr[4] : 0;
|
||
e->a1 = cpu ? cpu->gpr[5] : 0;
|
||
e->a2 = cpu ? cpu->gpr[6] : 0;
|
||
e->a3 = cpu ? cpu->gpr[7] : 0;
|
||
e->sr = cpu ? cpu->cop0[12] : 0;
|
||
e->epc = cpu ? cpu->cop0[14] : 0;
|
||
e->istat = i_stat;
|
||
e->imask = i_mask;
|
||
e->width = width;
|
||
e->in_exception = (uint8_t)psx_get_in_exception();
|
||
e->counter_7514 = psx_read_byte(0x7514);
|
||
}
|
||
|
||
void debug_server_log_sio_write(uint32_t addr, uint32_t value, uint8_t width) {
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)addr; (void)value; (void)width;
|
||
return;
|
||
#endif
|
||
SioPcTraceEntry *e = &s_sio_pc_trace[s_sio_pc_trace_seq % SIO_PC_TRACE_CAP];
|
||
uint32_t byte_seq = sio_get_seq();
|
||
e->seq = s_sio_pc_trace_seq++;
|
||
e->pc = g_debug_last_store_pc;
|
||
e->func = g_debug_current_func_addr;
|
||
e->addr = addr;
|
||
e->value = value;
|
||
e->byte_seq = byte_seq;
|
||
e->width = width;
|
||
if (addr == 0x1F80104Au)
|
||
debug_server_log_sio_ctrl_regs(value, width, byte_seq);
|
||
}
|
||
|
||
/* ---- 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;
|
||
|
||
/* ---- Unknown-dispatch ring buffer ----
|
||
* Always-on log of every psx_dispatch target that doesn't resolve to a
|
||
* generated function AND doesn't match any trampoline pattern in
|
||
* traps.c. Used to identify functions the recompiler missed.
|
||
* 64K entries × 32 bytes = 2 MB. Replaces the prior file-based log. */
|
||
#define UNKNOWN_DISPATCH_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t addr;
|
||
uint32_t phys;
|
||
uint32_t ra;
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t frame;
|
||
uint32_t pad;
|
||
} UnknownDispatchEntry;
|
||
static UnknownDispatchEntry s_unknown_ring[UNKNOWN_DISPATCH_CAP];
|
||
static uint64_t s_unknown_seq = 0;
|
||
|
||
/* Crash-trace accessor: returns entry at the given seq number (modulo cap).
|
||
* Layout matches crash_trace.c's UnknownDispatchEntry typedef. */
|
||
UnknownDispatchEntry crash_trace_unknown_get(uint64_t seq) {
|
||
return s_unknown_ring[seq & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||
}
|
||
uint64_t crash_trace_unknown_seq_get(void) { return s_unknown_seq; }
|
||
/* Per-target hit count — bounded set, ~N unique targets typically. */
|
||
#define UNKNOWN_UNIQUE_CAP 1024
|
||
typedef struct { uint32_t phys; uint64_t count; } UnknownUniqueEntry;
|
||
static UnknownUniqueEntry s_unknown_unique[UNKNOWN_UNIQUE_CAP];
|
||
static int s_unknown_unique_count = 0;
|
||
|
||
void psx_unknown_dispatch_record(uint32_t addr, uint32_t phys,
|
||
uint32_t ra, uint32_t a0, uint32_t a1)
|
||
{
|
||
uint64_t seq = s_unknown_seq++;
|
||
UnknownDispatchEntry *e = &s_unknown_ring[seq & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||
e->seq = seq;
|
||
e->addr = addr;
|
||
e->phys = phys;
|
||
e->ra = ra;
|
||
e->a0 = a0;
|
||
e->a1 = a1;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
/* Per-target count (linear probe). */
|
||
uint32_t idx = (phys >> 2) % UNKNOWN_UNIQUE_CAP;
|
||
for (int i = 0; i < UNKNOWN_UNIQUE_CAP; i++) {
|
||
uint32_t slot = (idx + i) % UNKNOWN_UNIQUE_CAP;
|
||
if (s_unknown_unique[slot].phys == phys) {
|
||
s_unknown_unique[slot].count++;
|
||
return;
|
||
}
|
||
if (s_unknown_unique[slot].phys == 0) {
|
||
s_unknown_unique[slot].phys = phys;
|
||
s_unknown_unique[slot].count = 1;
|
||
s_unknown_unique_count++;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Crash-trace accessors (used by crash_trace.c). The unknown-ring
|
||
* accessor is defined later in this file, after UnknownDispatchEntry. */
|
||
uint32_t crash_trace_dispatch_ring_get(int idx) {
|
||
return s_dispatch_ring[idx & (DISPATCH_TRACE_CAP - 1)];
|
||
}
|
||
uint64_t crash_trace_dispatch_seq_get(void) { return s_dispatch_seq; }
|
||
|
||
/* 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. */
|
||
/* 64K entries × ~24 B ≈ 1.5 MB; holds tens of minutes of chain transitions. */
|
||
#define CHAIN_TRACE_CAP (1 << 16)
|
||
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. */
|
||
/* Entry ring sized for ≥3 min of unrotated history at peak capture rate
|
||
* (~600k/sec under a wide direct-call filter): 1<<27 ≈ 134M entries × 56 B
|
||
* ≈ 7 GB. Lazily resident — only pages actually written use RAM, so the
|
||
* footprint grows from 0 toward the cap as the ring fills.
|
||
* Exit ring stays modest (1<<22, ~192 MB) — exits are auxiliary. */
|
||
#define FN_TRACE_CAP (1 << 18)
|
||
#define FN_EXIT_TRACE_CAP (1 << 18)
|
||
#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 s0, s1, s2, s3;
|
||
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;
|
||
|
||
/* Focused menu/render manager call ring. This is always-on, but deliberately
|
||
* narrow: it records the Tomba title-menu and render-object manager functions
|
||
* involved in NEW GAME / LOAD GAME / OPTIONS transitions. The generic function
|
||
* ring is either filtered or too hot; this ring preserves the boot-to-menu
|
||
* call history without one-shot arming. */
|
||
#define CALL_FOCUS_CAP (1 << 20)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t func_addr;
|
||
uint32_t ra;
|
||
uint32_t pc;
|
||
uint32_t frame;
|
||
uint32_t sp;
|
||
uint32_t v0, v1;
|
||
uint32_t a0, a1, a2, a3;
|
||
uint32_t t0, t1;
|
||
uint32_t s0, s1, s2, s3;
|
||
uint32_t stk10, stk14, stk18, stk20, stk40;
|
||
uint32_t obj;
|
||
uint32_t obj_10;
|
||
uint32_t obj_14;
|
||
uint32_t obj_18;
|
||
uint32_t obj_30;
|
||
uint32_t obj_30_0;
|
||
uint32_t obj_30_1;
|
||
uint32_t obj_34;
|
||
uint32_t obj_35;
|
||
uint32_t obj_36;
|
||
uint32_t obj_37;
|
||
uint32_t obj_38;
|
||
uint32_t obj_3c;
|
||
uint32_t obj_40;
|
||
uint32_t obj_44;
|
||
uint32_t obj_45;
|
||
uint32_t obj_46;
|
||
uint32_t obj_49;
|
||
uint32_t obj_4a;
|
||
uint32_t obj_50;
|
||
uint32_t obj_e0;
|
||
uint32_t obj_e3;
|
||
uint32_t obj_e4;
|
||
uint32_t obj_e5;
|
||
uint32_t obj_e6;
|
||
uint32_t obj_e8;
|
||
uint32_t obj_e9;
|
||
uint32_t obj_ea;
|
||
} CallFocusEntry;
|
||
static CallFocusEntry *s_call_focus = NULL;
|
||
static uint64_t s_call_focus_seq = 0;
|
||
|
||
/* Narrow card-manager trace. The generic function-entry ring is too hot
|
||
* during Tomba title/menu polling, so it rotates away the card-read setup
|
||
* before we can inspect a later hang. This ring records only the BIOS public
|
||
* card state machine and the low-level RAM card service boundary. */
|
||
#define CARD_MGR_TRACE_CAP 8192
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t func_addr;
|
||
uint32_t ra;
|
||
uint32_t pc;
|
||
uint32_t a0, a1, a2, a3;
|
||
uint32_t v0, t0, t1;
|
||
uint32_t frame;
|
||
uint32_t state_9f20;
|
||
uint32_t state_9f24;
|
||
uint32_t state_9f28;
|
||
uint32_t state_9f2c;
|
||
uint32_t state_9f30;
|
||
uint32_t state_9f34;
|
||
uint32_t state_7258;
|
||
uint32_t state_725c;
|
||
uint32_t state_7264;
|
||
uint32_t state_74bc;
|
||
uint32_t state_7500;
|
||
uint32_t state_7504;
|
||
uint32_t state_7508;
|
||
uint32_t state_750c;
|
||
uint32_t state_7510;
|
||
uint32_t state_7514;
|
||
uint32_t state_7518;
|
||
uint32_t state_751c;
|
||
uint32_t state_7520;
|
||
uint32_t state_7528;
|
||
uint32_t state_752c;
|
||
uint32_t state_7558;
|
||
uint32_t state_7568;
|
||
uint32_t state_756c;
|
||
uint8_t source; /* 0 = direct entry hook, 1 = dispatch hook */
|
||
uint8_t pad[3];
|
||
} CardMgrTraceEntry;
|
||
static CardMgrTraceEntry s_card_mgr_trace[CARD_MGR_TRACE_CAP];
|
||
static uint64_t s_card_mgr_trace_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 0x1B44). 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 0x00001B44u /* func_00001B44, RAM */
|
||
/* 4096 snapshots × 32 entries × ~28 B ≈ 3.7 MB — holds many minutes. */
|
||
#define EVCB_RING_CAP 4096
|
||
#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 for the fn_entry/fn_exit rings.
|
||
* Default disabled — recording every dispatch's args burns ~10 us per call
|
||
* (stack walk + 9-field write) and crushes wall-clock simulation rate.
|
||
* Enable via debug-server `fntrace_arm_filter` (sets lo/hi); investigators
|
||
* pick a tight range so the per-dispatch cost only fires for code under
|
||
* inspection. */
|
||
static int s_fn_trace_active = 0;
|
||
static uint32_t s_fn_trace_filter_lo = 0u;
|
||
static uint32_t s_fn_trace_filter_hi = 0xFFFFFFFFu;
|
||
static uint64_t s_fn_direct_seen = 0;
|
||
static uint64_t s_fn_direct_no_cpu = 0;
|
||
static uint64_t s_fn_direct_filtered = 0;
|
||
|
||
static int fn_trace_in_filter(uint32_t phys) {
|
||
if (!s_fn_trace_active) return 0;
|
||
return phys >= s_fn_trace_filter_lo && phys < s_fn_trace_filter_hi;
|
||
}
|
||
|
||
static void fn_trace_filter_from_env(const char *env_name) {
|
||
const char *spec = getenv(env_name);
|
||
if (!spec || !*spec) return;
|
||
|
||
char *end = NULL;
|
||
uint32_t lo = (uint32_t)strtoul(spec, &end, 0);
|
||
if (end == spec) return;
|
||
|
||
while (*end == ' ' || *end == '\t') end++;
|
||
if (*end != ':' && *end != '-' && *end != ',' && *end != ';') return;
|
||
end++;
|
||
|
||
while (*end == ' ' || *end == '\t') end++;
|
||
char *end2 = NULL;
|
||
uint32_t hi = (uint32_t)strtoul(end, &end2, 0);
|
||
if (end2 == end || hi <= lo) return;
|
||
|
||
s_fn_trace_filter_lo = lo;
|
||
s_fn_trace_filter_hi = hi;
|
||
s_fn_trace_active = 1;
|
||
}
|
||
|
||
static int card_mgr_trace_target(uint32_t phys) {
|
||
phys &= 0x1FFFFFFFu;
|
||
if (phys >= 0x1FC0B600u && phys < 0x1FC0C240u) return 1;
|
||
if (phys >= 0x00004900u && phys < 0x00006C80u) return 1;
|
||
if (phys == 0x00001B44u) return 1;
|
||
return 0;
|
||
}
|
||
|
||
static void card_mgr_trace_record(uint32_t func_addr, uint8_t source) {
|
||
if (!debug_cpu_ptr) return;
|
||
uint32_t phys = func_addr & 0x1FFFFFFFu;
|
||
if (!card_mgr_trace_target(phys)) return;
|
||
|
||
CardMgrTraceEntry *e = &s_card_mgr_trace[s_card_mgr_trace_seq % CARD_MGR_TRACE_CAP];
|
||
e->seq = s_card_mgr_trace_seq++;
|
||
e->func_addr = phys;
|
||
e->ra = debug_cpu_ptr->gpr[31];
|
||
e->pc = debug_cpu_ptr->pc;
|
||
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->v0 = debug_cpu_ptr->gpr[2];
|
||
e->t0 = debug_cpu_ptr->gpr[8];
|
||
e->t1 = debug_cpu_ptr->gpr[9];
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->state_9f20 = psx_read_word(0x00009F20u);
|
||
e->state_9f24 = psx_read_word(0x00009F24u);
|
||
e->state_9f28 = psx_read_word(0x00009F28u);
|
||
e->state_9f2c = psx_read_word(0x00009F2Cu);
|
||
e->state_9f30 = psx_read_word(0x00009F30u);
|
||
e->state_9f34 = psx_read_word(0x00009F34u);
|
||
e->state_7258 = psx_read_word(0x00007258u);
|
||
e->state_725c = psx_read_word(0x0000725Cu);
|
||
e->state_7264 = psx_read_word(0x00007264u);
|
||
e->state_74bc = psx_read_word(0x000074BCu);
|
||
e->state_7500 = psx_read_word(0x00007500u);
|
||
e->state_7504 = psx_read_word(0x00007504u);
|
||
e->state_7508 = psx_read_word(0x00007508u);
|
||
e->state_750c = psx_read_word(0x0000750Cu);
|
||
e->state_7510 = psx_read_word(0x00007510u);
|
||
e->state_7514 = psx_read_word(0x00007514u);
|
||
e->state_7518 = psx_read_word(0x00007518u);
|
||
e->state_751c = psx_read_word(0x0000751Cu);
|
||
e->state_7520 = psx_read_word(0x00007520u);
|
||
e->state_7528 = psx_read_word(0x00007528u);
|
||
e->state_752c = psx_read_word(0x0000752Cu);
|
||
e->state_7558 = psx_read_word(0x00007558u);
|
||
e->state_7568 = psx_read_word(0x00007568u);
|
||
e->state_756c = psx_read_word(0x0000756Cu);
|
||
e->source = source;
|
||
}
|
||
|
||
static int call_focus_target(uint32_t func)
|
||
{
|
||
switch (func) {
|
||
/* Title/menu state and input paths. */
|
||
case 0x80028638u:
|
||
case 0x80028728u:
|
||
case 0x80028794u:
|
||
case 0x800287F8u:
|
||
case 0x800288C4u:
|
||
case 0x80028A74u:
|
||
case 0x80028B34u:
|
||
case 0x80028CE4u:
|
||
case 0x80028D70u:
|
||
case 0x80028EF4u:
|
||
/* Render-object manager and parser paths. */
|
||
case 0x80068AA8u:
|
||
case 0x80068DDCu:
|
||
case 0x80068E5Cu:
|
||
case 0x800694FCu:
|
||
case 0x80069818u:
|
||
case 0x8006995Cu:
|
||
case 0x800699D0u:
|
||
case 0x80069AC8u:
|
||
case 0x80069B4Cu:
|
||
case 0x80069C98u:
|
||
case 0x80069CD0u:
|
||
case 0x8006A0C0u:
|
||
case 0x8006A128u:
|
||
case 0x8006A144u:
|
||
case 0x8006A378u:
|
||
case 0x8006A38Cu:
|
||
case 0x8006A3CCu:
|
||
case 0x8006AB4Cu:
|
||
case 0x8006ACA8u:
|
||
case 0x8006ACB8u:
|
||
case 0x8006AD74u:
|
||
case 0x8006AFF0u:
|
||
case 0x8006B028u:
|
||
case 0x8006B080u:
|
||
case 0x8006B154u:
|
||
case 0x8006B3B8u:
|
||
case 0x8006B494u:
|
||
return 1;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static uint32_t dbg_read_u16_phys(uint32_t phys)
|
||
{
|
||
return (uint32_t)psx_read_byte(phys)
|
||
| ((uint32_t)psx_read_byte(phys + 1u) << 8);
|
||
}
|
||
|
||
static uint32_t call_focus_object_ptr(uint32_t func, CPUState *cpu)
|
||
{
|
||
if (!cpu) return 0;
|
||
switch (func) {
|
||
case 0x80068AA8u:
|
||
case 0x80068DDCu:
|
||
case 0x80068E5Cu:
|
||
return 0x8009B3A0u + ((cpu->gpr[4] & 0x00F0u) ? 0xF0u : 0u);
|
||
case 0x8006A0C0u:
|
||
case 0x8006A128u:
|
||
case 0x8006A144u:
|
||
case 0x8006A378u:
|
||
case 0x8006A38Cu:
|
||
case 0x8006A3CCu:
|
||
case 0x8006AB4Cu:
|
||
case 0x8006ACA8u:
|
||
case 0x8006ACB8u:
|
||
case 0x8006AD74u:
|
||
case 0x8006B080u:
|
||
case 0x8006B154u:
|
||
case 0x8006B3B8u:
|
||
case 0x8006B494u:
|
||
case 0x800694FCu:
|
||
case 0x80069818u:
|
||
case 0x8006995Cu:
|
||
case 0x800699D0u:
|
||
case 0x80069AC8u:
|
||
case 0x80069B4Cu:
|
||
case 0x80069C98u:
|
||
case 0x80069CD0u:
|
||
return cpu->gpr[4];
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
static void call_focus_record(uint32_t func_addr)
|
||
{
|
||
CPUState *cpu = debug_cpu_ptr;
|
||
if (!s_call_focus || !cpu) return;
|
||
if (!call_focus_target(func_addr)) return;
|
||
|
||
CallFocusEntry *e = &s_call_focus[s_call_focus_seq % CALL_FOCUS_CAP];
|
||
e->seq = s_call_focus_seq++;
|
||
e->func_addr = func_addr;
|
||
e->ra = cpu->gpr[31];
|
||
e->pc = cpu->pc;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->sp = cpu->gpr[29];
|
||
e->v0 = cpu->gpr[2];
|
||
e->v1 = cpu->gpr[3];
|
||
e->a0 = cpu->gpr[4];
|
||
e->a1 = cpu->gpr[5];
|
||
e->a2 = cpu->gpr[6];
|
||
e->a3 = cpu->gpr[7];
|
||
e->t0 = cpu->gpr[8];
|
||
e->t1 = cpu->gpr[9];
|
||
e->s0 = cpu->gpr[16];
|
||
e->s1 = cpu->gpr[17];
|
||
e->s2 = cpu->gpr[18];
|
||
e->s3 = cpu->gpr[19];
|
||
e->stk10 = trace_read_word(cpu, cpu->gpr[29] + 0x10u);
|
||
e->stk14 = trace_read_word(cpu, cpu->gpr[29] + 0x14u);
|
||
e->stk18 = trace_read_word(cpu, cpu->gpr[29] + 0x18u);
|
||
e->stk20 = trace_read_word(cpu, cpu->gpr[29] + 0x20u);
|
||
e->stk40 = trace_read_word(cpu, cpu->gpr[29] + 0x40u);
|
||
e->obj = 0;
|
||
e->obj_10 = 0;
|
||
e->obj_14 = 0;
|
||
e->obj_18 = 0;
|
||
e->obj_30 = 0;
|
||
e->obj_30_0 = 0;
|
||
e->obj_30_1 = 0;
|
||
e->obj_34 = 0;
|
||
e->obj_35 = 0;
|
||
e->obj_36 = 0;
|
||
e->obj_37 = 0;
|
||
e->obj_38 = 0;
|
||
e->obj_3c = 0;
|
||
e->obj_40 = 0;
|
||
e->obj_44 = 0;
|
||
e->obj_45 = 0;
|
||
e->obj_46 = 0;
|
||
e->obj_49 = 0;
|
||
e->obj_4a = 0;
|
||
e->obj_50 = 0;
|
||
e->obj_e0 = 0;
|
||
e->obj_e3 = 0;
|
||
e->obj_e4 = 0;
|
||
e->obj_e5 = 0;
|
||
e->obj_e6 = 0;
|
||
e->obj_e8 = 0;
|
||
e->obj_e9 = 0;
|
||
e->obj_ea = 0;
|
||
|
||
uint32_t obj = call_focus_object_ptr(func_addr, cpu);
|
||
uint32_t phys = obj & 0x1FFFFFFFu;
|
||
if (phys >= 0x0009B300u && phys < 0x0009B700u) {
|
||
e->obj = obj;
|
||
e->obj_10 = psx_read_word(phys + 0x10u);
|
||
e->obj_14 = psx_read_word(phys + 0x14u);
|
||
e->obj_18 = psx_read_word(phys + 0x18u);
|
||
e->obj_30 = psx_read_word(phys + 0x30u);
|
||
uint32_t out_phys = e->obj_30 & 0x1FFFFFFFu;
|
||
if (out_phys < 0x00200000u - 1u) {
|
||
e->obj_30_0 = psx_read_byte(out_phys);
|
||
e->obj_30_1 = psx_read_byte(out_phys + 1u);
|
||
}
|
||
e->obj_34 = psx_read_byte(phys + 0x34u);
|
||
e->obj_35 = psx_read_byte(phys + 0x35u);
|
||
e->obj_36 = psx_read_byte(phys + 0x36u);
|
||
e->obj_37 = psx_read_byte(phys + 0x37u);
|
||
e->obj_38 = psx_read_byte(phys + 0x38u);
|
||
e->obj_3c = psx_read_word(phys + 0x3Cu);
|
||
e->obj_40 = psx_read_word(phys + 0x40u);
|
||
e->obj_44 = psx_read_byte(phys + 0x44u);
|
||
e->obj_45 = psx_read_byte(phys + 0x45u);
|
||
e->obj_46 = psx_read_byte(phys + 0x46u);
|
||
e->obj_49 = psx_read_byte(phys + 0x49u);
|
||
e->obj_4a = psx_read_byte(phys + 0x4Au);
|
||
e->obj_50 = psx_read_byte(phys + 0x50u);
|
||
e->obj_e0 = psx_read_byte(phys + 0xE0u);
|
||
e->obj_e3 = psx_read_byte(phys + 0xE3u);
|
||
e->obj_e4 = psx_read_byte(phys + 0xE4u);
|
||
e->obj_e5 = psx_read_byte(phys + 0xE5u);
|
||
e->obj_e6 = dbg_read_u16_phys(phys + 0xE6u);
|
||
e->obj_e8 = psx_read_byte(phys + 0xE8u);
|
||
e->obj_e9 = psx_read_byte(phys + 0xE9u);
|
||
e->obj_ea = psx_read_byte(phys + 0xEAu);
|
||
}
|
||
}
|
||
|
||
/* 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_EXIT_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_trace_active) return;
|
||
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->s0 = debug_cpu_ptr->gpr[16];
|
||
e->s1 = debug_cpu_ptr->gpr[17];
|
||
e->s2 = debug_cpu_ptr->gpr[18];
|
||
e->s3 = debug_cpu_ptr->gpr[19];
|
||
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;
|
||
}
|
||
|
||
/* Direct-call entry hook — emitted by the recompiler at the top of every
|
||
* generated function. Captures into the fn_entry ring without touching the
|
||
* shadow stack, so we see direct-jal call paths that never go through
|
||
* psx_dispatch (e.g. shell -> firstfile -> bu_read -> card_read). The shadow
|
||
* stack is owned by function_trace_record and is only meaningful for
|
||
* indirect dispatches; direct calls return through the native C stack. */
|
||
void debug_server_log_call_entry(uint32_t func_addr) {
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
/* Hottest call site in the binary — called at the top of every
|
||
* recompiled function. Early-return makes it a 1-instruction
|
||
* function-call cost; the compiler will likely PLT it through. */
|
||
(void)func_addr;
|
||
return;
|
||
#endif
|
||
s_fn_direct_seen++;
|
||
if (!debug_cpu_ptr) {
|
||
s_fn_direct_no_cpu++;
|
||
return;
|
||
}
|
||
card_mgr_trace_record(func_addr, 0);
|
||
sreg_trace_record(func_addr);
|
||
call_focus_record(func_addr);
|
||
if (!s_fn_entry) return;
|
||
if (!fn_trace_in_filter(func_addr)) return;
|
||
s_fn_direct_filtered++;
|
||
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 = func_addr;
|
||
e->ra = debug_cpu_ptr->gpr[31];
|
||
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->s0 = debug_cpu_ptr->gpr[16];
|
||
e->s1 = debug_cpu_ptr->gpr[17];
|
||
e->s2 = debug_cpu_ptr->gpr[18];
|
||
e->s3 = debug_cpu_ptr->gpr[19];
|
||
e->depth = (uint32_t)s_fn_stack_top;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
s_fn_entry_seq++;
|
||
}
|
||
|
||
/* Always-on A0/B0/C0 BIOS-call ring (ported from ape-fw for good-vs-bad
|
||
* event-delivery comparison). Recorded at the central dispatch chokepoint. */
|
||
#define BIOSCALL_RING_CAP (1 << 16)
|
||
typedef struct {
|
||
uint64_t seq; uint32_t table_base; uint32_t index; uint32_t func_ptr;
|
||
uint32_t a0, a1, a2, a3; uint32_t ra; uint32_t current_func; uint32_t frame;
|
||
uint8_t in_exception;
|
||
} BiosCallEntry;
|
||
static BiosCallEntry s_bioscall_ring[BIOSCALL_RING_CAP];
|
||
static uint64_t s_bioscall_seq = 0;
|
||
#define BIOSCALL_UNIQUE_CAP 2048
|
||
typedef struct { uint32_t table_base; uint32_t index; uint64_t count; } BiosCallUnique;
|
||
static BiosCallUnique s_bioscall_unique[BIOSCALL_UNIQUE_CAP];
|
||
static int s_bioscall_unique_count = 0;
|
||
void psx_bioscall_record(uint32_t table_base, uint32_t index, uint32_t func_ptr,
|
||
uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t ra)
|
||
{
|
||
uint64_t seq = s_bioscall_seq++;
|
||
BiosCallEntry *e = &s_bioscall_ring[seq & (BIOSCALL_RING_CAP - 1u)];
|
||
e->seq = seq; e->table_base = table_base; e->index = index; e->func_ptr = func_ptr;
|
||
e->a0 = a0; e->a1 = a1; e->a2 = a2; e->a3 = a3; e->ra = ra;
|
||
e->current_func = g_debug_current_func_addr; e->frame = (uint32_t)s_frame_count;
|
||
e->in_exception = (uint8_t)(psx_get_in_exception() ? 1u : 0u);
|
||
uint32_t h = ((table_base >> 2) ^ (index * 2654435761u)) % BIOSCALL_UNIQUE_CAP;
|
||
for (int i = 0; i < BIOSCALL_UNIQUE_CAP; i++) {
|
||
uint32_t slot = (h + i) % BIOSCALL_UNIQUE_CAP;
|
||
if (s_bioscall_unique[slot].count != 0 &&
|
||
s_bioscall_unique[slot].table_base == table_base &&
|
||
s_bioscall_unique[slot].index == index) { s_bioscall_unique[slot].count++; return; }
|
||
if (s_bioscall_unique[slot].count == 0) {
|
||
s_bioscall_unique[slot].table_base = table_base; s_bioscall_unique[slot].index = index;
|
||
s_bioscall_unique[slot].count = 1; s_bioscall_unique_count++; return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void debug_server_trace_dispatch(uint32_t func_addr) {
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)func_addr;
|
||
return;
|
||
#endif
|
||
card_mgr_trace_record(func_addr, 1);
|
||
|
||
{
|
||
uint32_t vphys = func_addr & 0x1FFFFFFFu;
|
||
if ((vphys == 0xA0u || vphys == 0xB0u || vphys == 0xC0u) && debug_cpu_ptr) {
|
||
psx_bioscall_record(vphys, debug_cpu_ptr->gpr[9], 0,
|
||
debug_cpu_ptr->gpr[4], debug_cpu_ptr->gpr[5],
|
||
debug_cpu_ptr->gpr[6], debug_cpu_ptr->gpr[7],
|
||
debug_cpu_ptr->gpr[31]);
|
||
}
|
||
}
|
||
|
||
/* 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);
|
||
static const char *json_get_str(const char *json, const char *key,
|
||
char *out, int out_sz);
|
||
static uint32_t hex_to_u32(const char *s);
|
||
static void handle_dirty_ram_stats(int id, const char *json);
|
||
static void handle_dirty_ram_unsupported(int id, const char *json);
|
||
static void send_err(int id, const char *msg);
|
||
static void send_ok(int id);
|
||
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);
|
||
}
|
||
|
||
/* ---- Dirty-RAM interpreter counters ---- */
|
||
#include "dirty_ram_interp.h"
|
||
static void handle_dirty_ram_stats(int id, const char *json)
|
||
{
|
||
extern uint64_t g_dirty_ram_blocks_run;
|
||
extern uint64_t g_dirty_ram_insns_run;
|
||
extern uint64_t g_dirty_ram_aborts;
|
||
extern uint64_t g_dirty_ram_guard_yields;
|
||
extern uint32_t dirty_ram_get_bitmap(void);
|
||
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||
extern uint32_t dirty_ram_get_bitmap_word_count(void);
|
||
(void)json;
|
||
|
||
char buf[16 * 1024];
|
||
int n = snprintf(buf, sizeof(buf),
|
||
"{\"id\":%d,\"ok\":true,\"blocks_run\":%llu,"
|
||
"\"insns_run\":%llu,\"aborts\":%llu,"
|
||
"\"guard_yields\":%llu,"
|
||
"\"dirty_bitmap\":\"0x%08X\",\"per_pc\":[",
|
||
id,
|
||
(unsigned long long)g_dirty_ram_blocks_run,
|
||
(unsigned long long)g_dirty_ram_insns_run,
|
||
(unsigned long long)g_dirty_ram_aborts,
|
||
(unsigned long long)g_dirty_ram_guard_yields,
|
||
(unsigned)dirty_ram_get_bitmap());
|
||
|
||
int first = 1;
|
||
for (int i = 0; i < DIRTY_RAM_PC_TABLE_SIZE; i++) {
|
||
DirtyRamPcEntry *e = &g_dirty_ram_pc_table[i];
|
||
if (e->pc == 0 || e->hits == 0) continue;
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
"%s{\"pc\":\"0x%08X\",\"hits\":%llu,\"insns\":%llu,"
|
||
"\"entries\":%llu}",
|
||
first ? "" : ",",
|
||
(unsigned)e->pc,
|
||
(unsigned long long)e->hits,
|
||
(unsigned long long)e->insns,
|
||
(unsigned long long)e->entry_hits);
|
||
first = 0;
|
||
if (n >= (int)sizeof(buf) - 128) break;
|
||
}
|
||
n += snprintf(buf + n, sizeof(buf) - n, "],\"dirty_bitmap_words\":[");
|
||
uint32_t word_count = dirty_ram_get_bitmap_word_count();
|
||
for (uint32_t i = 0; i < word_count; i++) {
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
"%s\"0x%08X\"",
|
||
i == 0 ? "" : ",",
|
||
(unsigned)dirty_ram_get_bitmap_word(i));
|
||
if (n >= (int)sizeof(buf) - 64) break;
|
||
}
|
||
n += snprintf(buf + n, sizeof(buf) - n, "]}\n");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
static void handle_dirty_ram_unsupported(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const char *reason = g_dirty_ram_last_unsupported_reason;
|
||
if (!reason) reason = "";
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"aborts\":%llu,"
|
||
"\"midblock\":%llu,\"entry\":\"0x%08X\","
|
||
"\"entry_ra\":\"0x%08X\",\"entry_sp\":\"0x%08X\","
|
||
"\"entry_insns\":%u,\"last_pc\":\"0x%08X\","
|
||
"\"last_insn\":\"0x%08X\",\"reason\":\"%s\"}\n",
|
||
id,
|
||
(unsigned long long)g_dirty_ram_aborts,
|
||
(unsigned long long)g_dirty_ram_unsupported_midblock,
|
||
(unsigned)g_dirty_ram_last_unsupported_entry,
|
||
(unsigned)g_dirty_ram_last_unsupported_entry_ra,
|
||
(unsigned)g_dirty_ram_last_unsupported_entry_sp,
|
||
(unsigned)g_dirty_ram_last_unsupported_insns,
|
||
(unsigned)g_dirty_ram_last_unsupported_pc,
|
||
(unsigned)g_dirty_ram_last_unsupported_insn,
|
||
reason);
|
||
}
|
||
|
||
/* ---- Dirty-RAM block-entry log: dump (target,ra,frame) tuples to find
|
||
* the caller of any RAM-installed stub. Optional target_lo/target_hi
|
||
* filters the response to a target-PC range; with no filter, dumps the
|
||
* most recent `count` entries (default 256, max DIRTY_RAM_BLOCK_LOG_CAP). */
|
||
static void handle_dirty_block_log(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > (int)DIRTY_RAM_BLOCK_LOG_CAP) count = DIRTY_RAM_BLOCK_LOG_CAP;
|
||
|
||
uint64_t total = g_dirty_ram_block_log_seq;
|
||
uint64_t avail = (total < DIRTY_RAM_BLOCK_LOG_CAP) ? total : DIRTY_RAM_BLOCK_LOG_CAP;
|
||
uint64_t scan_start = (total > avail) ? (total - avail) : 0;
|
||
|
||
/* Generous response buffer — 16K log entries * ~96 chars/entry < 2 MB. */
|
||
const size_t BUF_SZ = 4 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) {
|
||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
|
||
return;
|
||
}
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\",\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
target_lo, target_hi);
|
||
int emitted = 0;
|
||
/* Walk newest-first so callers naturally get the latest dispatches.
|
||
* Stop once we've emitted `count` matches or scanned the whole window. */
|
||
for (uint64_t i = 0; i < avail && emitted < count; i++) {
|
||
uint64_t seq = total - 1 - i;
|
||
DirtyRamBlockLogEntry *e =
|
||
&g_dirty_ram_block_log[seq & (DIRTY_RAM_BLOCK_LOG_CAP - 1u)];
|
||
if (e->target < target_lo || e->target >= target_hi) continue;
|
||
if (pos > BUF_SZ - 256) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"target\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
|
||
"\"frame\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->target, e->ra, e->a0, e->a1, e->a2, e->a3,
|
||
e->sp, e->t0, e->t1, e->t2, e->frame);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
static void handle_dirty_flow_log(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > (int)DIRTY_RAM_FLOW_LOG_CAP) count = DIRTY_RAM_FLOW_LOG_CAP;
|
||
|
||
uint64_t total = g_dirty_ram_flow_log_seq;
|
||
uint64_t avail = (total < DIRTY_RAM_FLOW_LOG_CAP) ? total : DIRTY_RAM_FLOW_LOG_CAP;
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) {
|
||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
|
||
return;
|
||
}
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\",\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
target_lo, target_hi);
|
||
int emitted = 0;
|
||
for (uint64_t i = 0; i < avail && emitted < count; i++) {
|
||
uint64_t seq = total - 1 - i;
|
||
DirtyRamFlowLogEntry *e =
|
||
&g_dirty_ram_flow_log[seq & (DIRTY_RAM_FLOW_LOG_CAP - 1u)];
|
||
if (e->target < target_lo || e->target >= target_hi) continue;
|
||
if (pos > BUF_SZ - 256) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"target\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"frame\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->pc, e->target,
|
||
e->ra, e->a0, e->a1, e->a2, e->a3, e->sp, e->frame);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
static void handle_dirty_insn_log(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "pc_lo", buf, sizeof(buf))) pc_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "pc_hi", buf, sizeof(buf))) pc_hi = hex_to_u32(buf);
|
||
int changed_only = json_get_int(json, "changed_only", 0);
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > (int)DIRTY_RAM_INSN_LOG_CAP) count = DIRTY_RAM_INSN_LOG_CAP;
|
||
|
||
uint64_t total = g_dirty_ram_insn_log_seq;
|
||
uint64_t avail = (total < DIRTY_RAM_INSN_LOG_CAP) ? total : DIRTY_RAM_INSN_LOG_CAP;
|
||
const size_t BUF_SZ = 16 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) {
|
||
send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"oom\"}\n", id);
|
||
return;
|
||
}
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"pc_lo\":\"0x%08X\",\"pc_hi\":\"0x%08X\",\"changed_only\":%d,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
pc_lo, pc_hi, changed_only ? 1 : 0);
|
||
int emitted = 0;
|
||
for (uint64_t i = 0; i < avail && emitted < count; i++) {
|
||
uint64_t seq = total - 1 - i;
|
||
DirtyRamInsnLogEntry *e =
|
||
&g_dirty_ram_insn_log[seq & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
|
||
if (e->pc < pc_lo || e->pc >= pc_hi) continue;
|
||
if (changed_only && e->before_s0 == e->after_s0) continue;
|
||
if (pos > BUF_SZ - 768) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
|
||
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
|
||
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
|
||
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||
"\"t2\":\"0x%08X\","
|
||
"\"current_tcb\":\"0x%08X\",\"task_ptr\":\"0x%08X\","
|
||
"\"task_mode\":\"0x%08X\",\"task_submode\":\"0x%08X\","
|
||
"\"frame\":%u,\"transferred\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->pc, e->insn, e->next_pc, e->target,
|
||
e->before_s0, e->after_s0, e->sp, e->ra,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||
e->t0, e->t1, e->t2,
|
||
e->current_tcb, e->task_ptr, e->task_mode,
|
||
e->task_submode, e->frame, (unsigned)e->transferred);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
/* dirty_block_dump_file: write the WHOLE live block-log window (oldest->newest)
|
||
* to a file. Inline dumps above ~2MB wedge the single-threaded server (seen
|
||
* twice); file dumps are the only safe path for full-ring extraction. */
|
||
static void handle_dirty_block_dump_file(int id, const char *json)
|
||
{
|
||
char path[512];
|
||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||
snprintf(path, sizeof(path), "dirty_block_log.json");
|
||
FILE *f = fopen(path, "w");
|
||
if (!f) { send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"open failed\"}\n", id); return; }
|
||
uint64_t total = g_dirty_ram_block_log_seq;
|
||
uint64_t avail = (total < DIRTY_RAM_BLOCK_LOG_CAP) ? total : DIRTY_RAM_BLOCK_LOG_CAP;
|
||
fputc('[', f);
|
||
int first = 1, count = 0;
|
||
for (uint64_t s = total - avail; s < total; s++) {
|
||
DirtyRamBlockLogEntry *e =
|
||
&g_dirty_ram_block_log[s & (DIRTY_RAM_BLOCK_LOG_CAP - 1u)];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"target\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
|
||
"\"sp\":\"0x%08X\",\"frame\":%u}",
|
||
first ? "" : ",\n",
|
||
(unsigned long long)e->seq, e->target, e->ra,
|
||
e->a0, e->a1, e->a2, e->a3, e->t0, e->t1, e->t2,
|
||
e->sp, e->frame);
|
||
first = 0; count++;
|
||
}
|
||
fputs("]\n", f);
|
||
fclose(f);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"file\":\"%s\",\"entries\":%d}\n",
|
||
id, path, count);
|
||
}
|
||
|
||
/* dirty_insn_dump_file: write the WHOLE live insn-log window (oldest->newest)
|
||
* as a JSON array to a file. The inline dirty_insn_log path serializes into a
|
||
* bounded TCP buffer and wedges the server on multi-MB dumps — file dumps have
|
||
* no size limit and leave the server responsive. */
|
||
static void handle_dirty_insn_dump_file(int id, const char *json)
|
||
{
|
||
char path[512];
|
||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||
snprintf(path, sizeof(path), "dirty_insn_log.json");
|
||
FILE *f = fopen(path, "w");
|
||
if (!f) { send_fmt("{\"id\":%d,\"ok\":false,\"error\":\"open failed\"}\n", id); return; }
|
||
uint64_t total = g_dirty_ram_insn_log_seq;
|
||
uint64_t avail = (total < DIRTY_RAM_INSN_LOG_CAP) ? total : DIRTY_RAM_INSN_LOG_CAP;
|
||
fputc('[', f);
|
||
int first = 1, count = 0;
|
||
for (uint64_t s = total - avail; s < total; s++) {
|
||
DirtyRamInsnLogEntry *e =
|
||
&g_dirty_ram_insn_log[s & (DIRTY_RAM_INSN_LOG_CAP - 1u)];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"insn\":\"0x%08X\","
|
||
"\"next_pc\":\"0x%08X\",\"target\":\"0x%08X\","
|
||
"\"before_s0\":\"0x%08X\",\"after_s0\":\"0x%08X\","
|
||
"\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\",\"t2\":\"0x%08X\","
|
||
"\"frame\":%u,\"transferred\":%u}",
|
||
first ? "" : ",\n",
|
||
(unsigned long long)e->seq,
|
||
e->pc, e->insn, e->next_pc, e->target,
|
||
e->before_s0, e->after_s0, e->sp, e->ra,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||
e->t0, e->t1, e->t2, e->frame, (unsigned)e->transferred);
|
||
first = 0; count++;
|
||
}
|
||
fputs("]\n", f);
|
||
fclose(f);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"file\":\"%s\",\"entries\":%d}\n",
|
||
id, path, count);
|
||
}
|
||
|
||
/* ---- fntrace: always-on psx_dispatch call ring ----
|
||
* Mirrors beetle_libretro.cpp's fntrace; covers every static-recomp +
|
||
* dirty-RAM dispatch on this side. Use to find indirect callers, walk
|
||
* argument-passing chains, and answer "who called X with what args"
|
||
* across processes (psx-runtime port 4370, psx-beetle port 4380). */
|
||
#include "fntrace.h"
|
||
|
||
static void handle_fntrace_arm(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
if (!json_get_str(json, "target", buf, sizeof(buf))) {
|
||
send_err(id, "missing target"); return;
|
||
}
|
||
uint32_t target = hex_to_u32(buf);
|
||
if (target == 0) { fntrace_arm_clear(); send_ok(id); return; }
|
||
fntrace_arm(target);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"target\":\"0x%08X\",\"armed\":%u}",
|
||
id, target, fntrace_arm_count());
|
||
}
|
||
|
||
static void handle_fntrace_arm_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
fntrace_arm_clear();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_fntrace_armed(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;
|
||
uint32_t n = fntrace_arm_count();
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"count\":%u,\"targets\":[", id, n);
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s\"0x%08X\"", (i == 0) ? "" : ",", fntrace_arm_get(i));
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_fntrace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
fntrace_clear();
|
||
send_ok(id);
|
||
}
|
||
|
||
/* fntrace_dump: paginated tail of the ring.
|
||
* Optional filters:
|
||
* - target_lo / target_hi: half-open virtual-address range filter on `target`
|
||
* - count: max entries to return (default 256, cap = ring size)
|
||
* Walks newest-first; emits up to `count` matches. When no filter is
|
||
* given, returns the most recent `count` entries verbatim. */
|
||
static void handle_fntrace_dump(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
uint32_t target_lo = 0, target_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "target_lo", buf, sizeof(buf))) target_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "target_hi", buf, sizeof(buf))) target_hi = hex_to_u32(buf);
|
||
uint64_t seq_lo = 0, seq_hi = 0;
|
||
int have_seq_window = 0;
|
||
if (json_get_str(json, "seq_lo", buf, sizeof(buf))) {
|
||
seq_lo = strtoull(buf, NULL, 0);
|
||
have_seq_window = 1;
|
||
}
|
||
if (json_get_str(json, "seq_hi", buf, sizeof(buf))) {
|
||
seq_hi = strtoull(buf, NULL, 0);
|
||
have_seq_window = 1;
|
||
}
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > (int)FNTRACE_RING_CAP) count = FNTRACE_RING_CAP;
|
||
|
||
uint64_t total = g_fntrace_seq;
|
||
uint64_t avail = (total < FNTRACE_RING_CAP) ? total : FNTRACE_RING_CAP;
|
||
uint64_t oldest = total - avail;
|
||
if (have_seq_window) {
|
||
if (seq_hi == 0 || seq_hi > total) seq_hi = total;
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_lo > seq_hi) seq_lo = seq_hi;
|
||
}
|
||
|
||
const size_t BUF_SZ = 4 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"seq_lo\":%llu,\"seq_hi\":%llu,"
|
||
"\"target_lo\":\"0x%08X\",\"target_hi\":\"0x%08X\","
|
||
"\"armed\":%u,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
(unsigned long long)(have_seq_window ? seq_lo : 0),
|
||
(unsigned long long)(have_seq_window ? seq_hi : 0),
|
||
target_lo, target_hi, fntrace_arm_count());
|
||
int emitted = 0;
|
||
uint64_t scan_count = have_seq_window ? (seq_hi - seq_lo) : avail;
|
||
for (uint64_t i = 0; i < scan_count && emitted < count; i++) {
|
||
uint64_t seq = have_seq_window ? (seq_lo + i) : (total - 1 - i);
|
||
FntraceEntry *e = &g_fntrace_ring[seq & (FNTRACE_RING_CAP - 1u)];
|
||
if (e->target < target_lo || e->target >= target_hi) continue;
|
||
if (pos > BUF_SZ - 256) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"target\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"s3\":\"0x%08X\",\"frame\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)seq,
|
||
e->target, e->ra, e->a0, e->a1, e->a2, e->a3,
|
||
e->s3, e->frame);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%d}\n", emitted);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
/* ---- unknown_dispatch_log: dump psx_unknown_dispatch hits ----
|
||
* Two modes:
|
||
* - default: per-target count summary (sorted by hit count)
|
||
* - tail=N: most recent N entries from the ring */
|
||
static void handle_bioscall_dump(int id, const char *json)
|
||
{
|
||
int tail = json_get_int(json, "tail", 0);
|
||
long want_index = json_get_int(json, "index", -1);
|
||
char tbuf[32] = {0}; uint32_t want_table = 0; int have_table = 0;
|
||
if (json_get_str(json, "table", tbuf, sizeof tbuf)) { want_table = (uint32_t)strtoul(tbuf, NULL, 0); have_table = 1; }
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ); if (!out) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
if (tail > 0) {
|
||
if (tail > (int)BIOSCALL_RING_CAP) tail = BIOSCALL_RING_CAP;
|
||
uint64_t total = s_bioscall_seq;
|
||
uint64_t avail = (total < BIOSCALL_RING_CAP) ? total : BIOSCALL_RING_CAP;
|
||
if ((uint64_t)tail > avail) tail = (int)avail;
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "{\"id\":%d,\"ok\":true,\"total\":%llu,\"tail\":%d,\"entries\":[", id, (unsigned long long)total, tail);
|
||
uint64_t start = total - (uint64_t)tail; int first = 1;
|
||
for (int i = 0; i < tail; i++) {
|
||
BiosCallEntry *e = &s_bioscall_ring[(start + i) & (BIOSCALL_RING_CAP - 1u)];
|
||
if (want_index >= 0 && (long)e->index != want_index) continue;
|
||
if (have_table && e->table_base != want_table) continue;
|
||
if (pos > BUF_SZ - 512) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "%s{\"seq\":%llu,\"table\":\"0x%08X\",\"index\":%u,\"func\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\",\"ra\":\"0x%08X\",\"current_func\":\"0x%08X\",\"in_exc\":%u,\"frame\":%u}",
|
||
first ? "" : ",", (unsigned long long)e->seq, e->table_base, e->index, e->func_ptr, e->a0, e->a1, e->a2, e->a3, e->ra, e->current_func, e->in_exception, e->frame);
|
||
first = 0;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(out); free(out); return;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "{\"id\":%d,\"ok\":true,\"total\":%llu,\"unique\":%d,\"counts\":[", id, (unsigned long long)s_bioscall_seq, s_bioscall_unique_count);
|
||
int first = 1;
|
||
for (int i = 0; i < BIOSCALL_UNIQUE_CAP; i++) {
|
||
if (s_bioscall_unique[i].count == 0) continue;
|
||
if (want_index >= 0 && (long)s_bioscall_unique[i].index != want_index) continue;
|
||
if (have_table && s_bioscall_unique[i].table_base != want_table) continue;
|
||
if (pos > BUF_SZ - 256) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "%s{\"table\":\"0x%08X\",\"index\":%u,\"count\":%llu}", first ? "" : ",", s_bioscall_unique[i].table_base, s_bioscall_unique[i].index, (unsigned long long)s_bioscall_unique[i].count);
|
||
first = 0;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(out); free(out);
|
||
}
|
||
|
||
static void handle_unknown_dispatch_log(int id, const char *json)
|
||
{
|
||
int tail = json_get_int(json, "tail", 0);
|
||
|
||
if (tail > 0) {
|
||
if (tail > (int)UNKNOWN_DISPATCH_CAP) tail = UNKNOWN_DISPATCH_CAP;
|
||
uint64_t total = s_unknown_seq;
|
||
uint64_t avail = (total < UNKNOWN_DISPATCH_CAP) ? total : UNKNOWN_DISPATCH_CAP;
|
||
if ((uint64_t)tail > avail) tail = (int)avail;
|
||
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"unique\":%d,"
|
||
"\"tail\":%d,\"entries\":[",
|
||
id, (unsigned long long)total, s_unknown_unique_count, tail);
|
||
uint64_t start = total - (uint64_t)tail;
|
||
for (int i = 0; i < tail; i++) {
|
||
UnknownDispatchEntry *e =
|
||
&s_unknown_ring[(start + i) & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||
if (pos > BUF_SZ - 256) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"phys\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"frame\":%u}",
|
||
i == 0 ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->addr, e->phys, e->ra, e->a0, e->a1, e->frame);
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
return;
|
||
}
|
||
|
||
/* Summary mode: per-phys hit count, sorted descending. */
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"unique\":%d,"
|
||
"\"summary\":[",
|
||
id, (unsigned long long)s_unknown_seq, s_unknown_unique_count);
|
||
/* Sort by count desc — small cap, simple selection. */
|
||
int emitted = 0;
|
||
for (int round = 0; round < UNKNOWN_UNIQUE_CAP && emitted < 200; round++) {
|
||
uint64_t best_count = 0;
|
||
int best_idx = -1;
|
||
for (int i = 0; i < UNKNOWN_UNIQUE_CAP; i++) {
|
||
if (s_unknown_unique[i].phys == 0) continue;
|
||
if (s_unknown_unique[i].count > best_count) {
|
||
best_count = s_unknown_unique[i].count;
|
||
best_idx = i;
|
||
}
|
||
}
|
||
if (best_idx < 0) break;
|
||
if (pos > BUF_SZ - 128) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"phys\":\"0x%08X\",\"count\":%llu}",
|
||
emitted == 0 ? "" : ",",
|
||
s_unknown_unique[best_idx].phys,
|
||
(unsigned long long)s_unknown_unique[best_idx].count);
|
||
s_unknown_unique[best_idx].count = 0; /* mark consumed */
|
||
emitted++;
|
||
}
|
||
/* Restore: re-walk the ring once to rebuild counts. Cheap since cap is 1024. */
|
||
/* (We mutated counts above to drive selection; rebuild by replaying the
|
||
* ring's most recent UNKNOWN_DISPATCH_CAP entries.) */
|
||
for (int i = 0; i < UNKNOWN_UNIQUE_CAP; i++) s_unknown_unique[i].phys = 0;
|
||
s_unknown_unique_count = 0;
|
||
uint64_t avail = (s_unknown_seq < UNKNOWN_DISPATCH_CAP) ? s_unknown_seq : UNKNOWN_DISPATCH_CAP;
|
||
uint64_t start = s_unknown_seq - avail;
|
||
for (uint64_t i = 0; i < avail; i++) {
|
||
UnknownDispatchEntry *e = &s_unknown_ring[(start + i) & (UNKNOWN_DISPATCH_CAP - 1u)];
|
||
uint32_t phys = e->phys;
|
||
uint32_t idx = (phys >> 2) % UNKNOWN_UNIQUE_CAP;
|
||
for (int k = 0; k < UNKNOWN_UNIQUE_CAP; k++) {
|
||
uint32_t slot = (idx + k) % UNKNOWN_UNIQUE_CAP;
|
||
if (s_unknown_unique[slot].phys == phys) {
|
||
s_unknown_unique[slot].count++;
|
||
break;
|
||
}
|
||
if (s_unknown_unique[slot].phys == 0) {
|
||
s_unknown_unique[slot].phys = phys;
|
||
s_unknown_unique[slot].count = 1;
|
||
s_unknown_unique_count++;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
/* ---- SIO write PC tracer dump ----
|
||
* Returns the most recent N entries from the SIO PC tracer ring.
|
||
* Optional addr_lo/addr_hi (hex-string) filter restricts to a single
|
||
* MMIO register range — typical use: filter for SIO_DATA (0x1F801040)
|
||
* or SIO_CTRL (0x1F80104A) writes only. */
|
||
static void handle_sio_pc_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
char addr_lo_buf[32], addr_hi_buf[32];
|
||
uint32_t addr_lo = 0, addr_hi = 0;
|
||
if (json_get_str(json, "addr_lo", addr_lo_buf, sizeof(addr_lo_buf)))
|
||
addr_lo = hex_to_u32(addr_lo_buf);
|
||
if (json_get_str(json, "addr_hi", addr_hi_buf, sizeof(addr_hi_buf)))
|
||
addr_hi = hex_to_u32(addr_hi_buf);
|
||
int filter = (addr_hi > addr_lo);
|
||
|
||
int total = (int)(s_sio_pc_trace_seq < SIO_PC_TRACE_CAP
|
||
? s_sio_pc_trace_seq : SIO_PC_TRACE_CAP);
|
||
if (count > total) count = total;
|
||
if (count < 0) count = 0;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%d,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)s_sio_pc_trace_seq, total);
|
||
|
||
int start_idx = (int)(s_sio_pc_trace_seq - (uint64_t)count);
|
||
int emitted = 0;
|
||
for (int i = 0; i < count; i++) {
|
||
int idx = (start_idx + i) % SIO_PC_TRACE_CAP;
|
||
SioPcTraceEntry *e = &s_sio_pc_trace[idx];
|
||
if (filter && (e->addr < addr_lo || e->addr >= addr_hi)) continue;
|
||
send_fmt("%s{\"seq\":%llu,\"pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"addr\":\"0x%08X\","
|
||
"\"value\":\"0x%08X\",\"byte_seq\":%u,\"width\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->pc, e->func,
|
||
e->addr, e->value, e->byte_seq, e->width);
|
||
emitted++;
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_sio_pc_window(int id, const char *json)
|
||
{
|
||
int seq = json_get_int(json, "byte_seq", -1);
|
||
int before = json_get_int(json, "before", 8);
|
||
int after = json_get_int(json, "after", 16);
|
||
char addr_lo_buf[32], addr_hi_buf[32];
|
||
uint32_t addr_lo = 0, addr_hi = 0;
|
||
if (json_get_str(json, "addr_lo", addr_lo_buf, sizeof(addr_lo_buf)))
|
||
addr_lo = hex_to_u32(addr_lo_buf);
|
||
if (json_get_str(json, "addr_hi", addr_hi_buf, sizeof(addr_hi_buf)))
|
||
addr_hi = hex_to_u32(addr_hi_buf);
|
||
int filter = (addr_hi > addr_lo);
|
||
if (seq < 0) { send_err(id, "missing byte_seq"); return; }
|
||
if (before < 0) before = 0;
|
||
if (after < 0) after = 0;
|
||
|
||
uint32_t lo = (uint32_t)((seq > before) ? (seq - before) : 0);
|
||
uint32_t hi = (uint32_t)(seq + after);
|
||
uint64_t total = s_sio_pc_trace_seq;
|
||
uint64_t avail = (total < SIO_PC_TRACE_CAP) ? total : SIO_PC_TRACE_CAP;
|
||
uint64_t start_seq = total - avail;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"byte_seq\":%d,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail, seq);
|
||
int emitted = 0;
|
||
for (uint64_t i = 0; i < avail; i++) {
|
||
uint64_t s = start_seq + i;
|
||
SioPcTraceEntry *e = &s_sio_pc_trace[s % SIO_PC_TRACE_CAP];
|
||
if (e->byte_seq < lo || e->byte_seq > hi) continue;
|
||
if (filter && (e->addr < addr_lo || e->addr >= addr_hi)) continue;
|
||
if (emitted > 0) send_fmt(",");
|
||
send_fmt("{\"seq\":%llu,\"pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"addr\":\"0x%08X\","
|
||
"\"value\":\"0x%08X\",\"byte_seq\":%u,\"width\":%u}",
|
||
(unsigned long long)e->seq, e->pc, e->func,
|
||
e->addr, e->value, e->byte_seq, e->width);
|
||
emitted++;
|
||
}
|
||
send_fmt("],\"emitted\":%d}\n", emitted);
|
||
}
|
||
|
||
static void emit_sio_ctrl_reg_entry(const SioCtrlRegTraceEntry *e, int first)
|
||
{
|
||
send_fmt("%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"value\":\"0x%08X\",\"byte_seq\":%u,\"cpu_pc\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\",\"v0\":\"0x%08X\","
|
||
"\"v1\":\"0x%08X\",\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"width\":%u,\"in_exc\":%u,\"ctr\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->pc, e->func,
|
||
e->value, e->byte_seq, e->cpu_pc, e->ra, e->sp,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||
e->sr, e->epc, e->istat, e->imask,
|
||
(unsigned)e->width, (unsigned)e->in_exception,
|
||
(unsigned)e->counter_7514);
|
||
}
|
||
|
||
static void handle_sio_ctrl_reg_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
if (count < 0) count = 0;
|
||
if (count > (int)SIO_CTRL_REG_TRACE_CAP) count = SIO_CTRL_REG_TRACE_CAP;
|
||
|
||
uint64_t total = s_sio_ctrl_reg_trace_seq;
|
||
uint64_t avail = (total < SIO_CTRL_REG_TRACE_CAP)
|
||
? total : SIO_CTRL_REG_TRACE_CAP;
|
||
if ((uint64_t)count > avail) count = (int)avail;
|
||
uint64_t start = total - (uint64_t)count;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
for (int i = 0; i < count; i++) {
|
||
uint64_t s = start + (uint64_t)i;
|
||
const SioCtrlRegTraceEntry *e =
|
||
&s_sio_ctrl_reg_trace[s % SIO_CTRL_REG_TRACE_CAP];
|
||
emit_sio_ctrl_reg_entry(e, i == 0);
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_sio_ctrl_reg_window(int id, const char *json)
|
||
{
|
||
int seq = json_get_int(json, "byte_seq", -1);
|
||
int before = json_get_int(json, "before", 8);
|
||
int after = json_get_int(json, "after", 16);
|
||
if (seq < 0) { send_err(id, "missing byte_seq"); return; }
|
||
if (before < 0) before = 0;
|
||
if (after < 0) after = 0;
|
||
|
||
uint32_t lo = (uint32_t)((seq > before) ? (seq - before) : 0);
|
||
uint32_t hi = (uint32_t)(seq + after);
|
||
uint64_t total = s_sio_ctrl_reg_trace_seq;
|
||
uint64_t avail = (total < SIO_CTRL_REG_TRACE_CAP)
|
||
? total : SIO_CTRL_REG_TRACE_CAP;
|
||
uint64_t start_seq = total - avail;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"byte_seq\":%d,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail, seq);
|
||
int emitted = 0;
|
||
for (uint64_t i = 0; i < avail; i++) {
|
||
uint64_t s = start_seq + i;
|
||
const SioCtrlRegTraceEntry *e =
|
||
&s_sio_ctrl_reg_trace[s % SIO_CTRL_REG_TRACE_CAP];
|
||
if (e->byte_seq < lo || e->byte_seq > hi) continue;
|
||
emit_sio_ctrl_reg_entry(e, emitted == 0);
|
||
emitted++;
|
||
}
|
||
send_fmt("],\"emitted\":%d}\n", emitted);
|
||
}
|
||
|
||
static void handle_sio_ctrl_reg_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
memset(s_sio_ctrl_reg_trace, 0, sizeof(s_sio_ctrl_reg_trace));
|
||
s_sio_ctrl_reg_trace_seq = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static const char *restore_kind_name(uint32_t kind)
|
||
{
|
||
switch (kind) {
|
||
case 1: return "restore_escape";
|
||
case 2: return "rfe_escape";
|
||
case 3: return "restore_resume";
|
||
case 4: return "rfe_resume";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void emit_restore_entry(const RestoreTraceEntry *e, int first)
|
||
{
|
||
send_fmt("%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\",\"jmp\":%u,"
|
||
"\"target\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||
"\"byte_seq\":%u,\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
|
||
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"frame\":%u,\"in_exc\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
|
||
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
|
||
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
|
||
e->frame, (unsigned)e->in_exception);
|
||
}
|
||
|
||
static size_t append_restore_entry(char *buf, size_t pos, size_t cap,
|
||
const RestoreTraceEntry *e, int first)
|
||
{
|
||
if (pos >= cap) return pos;
|
||
pos += snprintf(buf + pos, cap - pos,
|
||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\",\"jmp\":%u,"
|
||
"\"target\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||
"\"byte_seq\":%u,\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\",\"a0\":\"0x%08X\","
|
||
"\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"sr\":\"0x%08X\","
|
||
"\"epc\":\"0x%08X\",\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"frame\":%u,\"in_exc\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||
e->jmp_val, e->target_pc, e->cpu_pc, e->func, e->last_store_pc,
|
||
e->byte_seq, e->ra, e->sp, e->v0, e->v1, e->a0, e->a1, e->a2,
|
||
e->a3, e->s0, e->s1, e->sr, e->epc, e->istat, e->imask,
|
||
e->frame, (unsigned)e->in_exception);
|
||
return pos;
|
||
}
|
||
|
||
static void handle_restore_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
if (count < 0) count = 0;
|
||
if (count > (int)RESTORE_TRACE_CAP) count = RESTORE_TRACE_CAP;
|
||
|
||
uint64_t total = s_restore_trace_seq;
|
||
uint64_t avail = (total < RESTORE_TRACE_CAP) ? total : RESTORE_TRACE_CAP;
|
||
if ((uint64_t)count > avail) count = (int)avail;
|
||
uint64_t start = total - (uint64_t)count;
|
||
|
||
const size_t BUF_SZ = 256u + (size_t)count * 512u;
|
||
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,\"available\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
for (int i = 0; i < count; i++) {
|
||
uint64_t s = start + (uint64_t)i;
|
||
const RestoreTraceEntry *e = &s_restore_trace[s % RESTORE_TRACE_CAP];
|
||
if (pos > BUF_SZ - 512) break;
|
||
pos = append_restore_entry(buf, pos, BUF_SZ, e, i == 0);
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_restore_trace_window(int id, const char *json)
|
||
{
|
||
int seq = json_get_int(json, "byte_seq", -1);
|
||
int before = json_get_int(json, "before", 8);
|
||
int after = json_get_int(json, "after", 16);
|
||
if (seq < 0) { send_err(id, "missing byte_seq"); return; }
|
||
if (before < 0) before = 0;
|
||
if (after < 0) after = 0;
|
||
|
||
uint32_t lo = (uint32_t)((seq > before) ? (seq - before) : 0);
|
||
uint32_t hi = (uint32_t)(seq + after);
|
||
uint64_t total = s_restore_trace_seq;
|
||
uint64_t avail = (total < RESTORE_TRACE_CAP) ? total : RESTORE_TRACE_CAP;
|
||
uint64_t start_seq = total - avail;
|
||
|
||
const size_t BUF_SZ = 4 * 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,\"available\":%llu,"
|
||
"\"byte_seq\":%d,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail, seq);
|
||
int emitted = 0;
|
||
for (uint64_t i = 0; i < avail; i++) {
|
||
uint64_t s = start_seq + i;
|
||
const RestoreTraceEntry *e = &s_restore_trace[s % RESTORE_TRACE_CAP];
|
||
if (e->byte_seq < lo || e->byte_seq > hi) continue;
|
||
if (pos > BUF_SZ - 512) break;
|
||
pos = append_restore_entry(buf, pos, BUF_SZ, e, emitted == 0);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_restore_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
memset(s_restore_trace, 0, sizeof(s_restore_trace));
|
||
s_restore_trace_seq = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static const char *thread_kind_name(uint32_t kind)
|
||
{
|
||
switch (kind) {
|
||
case 1: return "save";
|
||
case 2: return "restore";
|
||
case 3: return "change_enter";
|
||
case 4: return "invalid";
|
||
case 5: return "same";
|
||
case 6: return "inactive_current";
|
||
case 7: return "target_missing";
|
||
case 8: return "switch_to";
|
||
case 9: return "switch_back";
|
||
case 10: return "fiber_entry";
|
||
case 11: return "fiber_done";
|
||
case 12: return "fiber_return_restore";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static size_t append_thread_entry(char *buf, size_t pos, size_t cap,
|
||
const ThreadTraceEntry *e, int first)
|
||
{
|
||
if (pos >= cap) return pos;
|
||
pos += snprintf(buf + pos, cap - pos,
|
||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\","
|
||
"\"current_tcb\":\"0x%08X\",\"target_tcb\":\"0x%08X\","
|
||
"\"current_state\":\"0x%08X\",\"target_state\":\"0x%08X\","
|
||
"\"current_ptr\":\"0x%08X\",\"target_pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"saved_a0\":\"0x%08X\",\"saved_a1\":\"0x%08X\","
|
||
"\"saved_a2\":\"0x%08X\",\"saved_a3\":\"0x%08X\","
|
||
"\"saved_s0\":\"0x%08X\",\"saved_s1\":\"0x%08X\","
|
||
"\"saved_s2\":\"0x%08X\",\"saved_s3\":\"0x%08X\","
|
||
"\"saved_sp\":\"0x%08X\","
|
||
"\"saved_ra\":\"0x%08X\",\"saved_pc\":\"0x%08X\","
|
||
"\"saved_sr\":\"0x%08X\","
|
||
"\"task_ptr\":\"0x%08X\",\"task_state\":\"0x%08X\","
|
||
"\"task_mode\":\"0x%08X\",\"task_submode\":\"0x%08X\","
|
||
"\"istat\":\"0x%08X\","
|
||
"\"imask\":\"0x%08X\",\"frame\":%u,\"in_exc\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->kind, thread_kind_name(e->kind),
|
||
e->current_tcb, e->target_tcb, e->current_state, e->target_state,
|
||
e->current_tcb_ptr, e->target_pc, e->func, e->last_store_pc,
|
||
e->ra, e->sp, e->a0, e->a1, e->a2, e->a3,
|
||
e->s0, e->s1, e->s2, e->s3, e->sr, e->epc,
|
||
e->saved_a0, e->saved_a1, e->saved_a2, e->saved_a3,
|
||
e->saved_s0, e->saved_s1, e->saved_s2, e->saved_s3,
|
||
e->saved_sp, e->saved_ra, e->saved_pc, e->saved_sr,
|
||
e->task_ptr, e->task_state, e->task_mode, e->task_submode,
|
||
e->istat, e->imask, e->frame,
|
||
(unsigned)e->in_exception);
|
||
return pos;
|
||
}
|
||
|
||
/* Thread context save/restore ring — declared in traps.c. */
|
||
typedef struct ThreadCtxRingEntry {
|
||
uint32_t seq;
|
||
uint32_t frame;
|
||
uint8_t op;
|
||
uint8_t pad0[3];
|
||
uint32_t tcb;
|
||
uint32_t resume_pc;
|
||
uint32_t gpr_29;
|
||
uint32_t gpr_31;
|
||
uint32_t cop0_sr;
|
||
uint32_t cop0_epc;
|
||
} ThreadCtxRingEntry;
|
||
#define THREAD_CTX_RING_CAP_DS 256u
|
||
extern ThreadCtxRingEntry g_thread_ctx_ring[THREAD_CTX_RING_CAP_DS];
|
||
extern uint64_t g_thread_ctx_ring_seq;
|
||
|
||
static void handle_thread_ctx_ring(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 64);
|
||
if (count < 0) count = 0;
|
||
if (count > (int)THREAD_CTX_RING_CAP_DS) count = THREAD_CTX_RING_CAP_DS;
|
||
|
||
uint64_t total = g_thread_ctx_ring_seq;
|
||
uint64_t avail = (total < THREAD_CTX_RING_CAP_DS) ? total : THREAD_CTX_RING_CAP_DS;
|
||
if ((uint64_t)count > avail) count = (int)avail;
|
||
uint64_t start = total - (uint64_t)count;
|
||
|
||
const size_t BUF_SZ = 256u + (size_t)count * 240u;
|
||
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,\"available\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
int emitted = 0;
|
||
for (uint64_t s = start; s < total && emitted < count; s++) {
|
||
const ThreadCtxRingEntry *e = &g_thread_ctx_ring[s & (THREAD_CTX_RING_CAP_DS - 1u)];
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%u,\"frame\":%u,\"op\":\"%s\",\"tcb\":\"0x%08X\","
|
||
"\"resume_pc\":\"0x%08X\",\"sp\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\"}",
|
||
emitted == 0 ? "" : ",",
|
||
e->seq, e->frame,
|
||
e->op == 0 ? "save" : "restore",
|
||
e->tcb, e->resume_pc, e->gpr_29, e->gpr_31, e->cop0_sr, e->cop0_epc);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_thread_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
if (count < 0) count = 0;
|
||
if (count > 2048) count = 2048;
|
||
|
||
uint64_t total = s_thread_trace_seq;
|
||
uint64_t avail = (total < THREAD_TRACE_CAP) ? total : THREAD_TRACE_CAP;
|
||
if ((uint64_t)count > avail) count = (int)avail;
|
||
uint64_t oldest = total - avail;
|
||
uint64_t start = total - (uint64_t)count;
|
||
uint32_t frame_lo = 0;
|
||
uint32_t frame_hi = 0xFFFFFFFFu;
|
||
uint32_t kind_filter = 0;
|
||
uint32_t current_filter = 0;
|
||
uint32_t target_filter = 0;
|
||
uint32_t either_filter = 0;
|
||
int has_kind = 0;
|
||
int has_current = 0;
|
||
int has_target = 0;
|
||
int has_either = 0;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
char seq_buf[32], val_buf[32];
|
||
if (json_get_str(json, "seq_lo", seq_buf, sizeof(seq_buf))) {
|
||
start = strtoull(seq_buf, NULL, 0);
|
||
if (start < oldest) start = oldest;
|
||
if (start > total) start = total;
|
||
if (start + (uint64_t)count > total)
|
||
count = (int)(total - start);
|
||
}
|
||
if (json_get_str(json, "frame_lo", val_buf, sizeof(val_buf)))
|
||
frame_lo = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
if (json_get_str(json, "frame_hi", val_buf, sizeof(val_buf)))
|
||
frame_hi = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
if (json_get_str(json, "kind", val_buf, sizeof(val_buf))) {
|
||
kind_filter = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
has_kind = 1;
|
||
}
|
||
if (json_get_str(json, "current_tcb", val_buf, sizeof(val_buf))) {
|
||
current_filter = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
has_current = 1;
|
||
}
|
||
if (json_get_str(json, "target_tcb", val_buf, sizeof(val_buf))) {
|
||
target_filter = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
has_target = 1;
|
||
}
|
||
if (json_get_str(json, "tcb", val_buf, sizeof(val_buf))) {
|
||
either_filter = (uint32_t)strtoul(val_buf, NULL, 0);
|
||
has_either = 1;
|
||
}
|
||
|
||
int has_filter = has_kind || has_current || has_target || has_either ||
|
||
frame_lo != 0 || frame_hi != 0xFFFFFFFFu;
|
||
if (has_filter && !json_get_str(json, "seq_lo", seq_buf, sizeof(seq_buf))) {
|
||
start = oldest;
|
||
}
|
||
|
||
const size_t BUF_SZ = 256u + (size_t)count * 1152u;
|
||
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,\"available\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
|
||
int emitted = 0;
|
||
if (newest_first) {
|
||
uint64_t stop = (start > oldest) ? start : oldest;
|
||
for (uint64_t s = total; s > stop && emitted < count; ) {
|
||
s--;
|
||
const ThreadTraceEntry *e = &s_thread_trace[s % THREAD_TRACE_CAP];
|
||
if (has_filter) {
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
if (has_kind && e->kind != kind_filter) continue;
|
||
if (has_current && e->current_tcb != current_filter) continue;
|
||
if (has_target && e->target_tcb != target_filter) continue;
|
||
if (has_either &&
|
||
e->current_tcb != either_filter &&
|
||
e->target_tcb != either_filter) continue;
|
||
}
|
||
if (pos > BUF_SZ - 1152) break;
|
||
pos = append_thread_entry(buf, pos, BUF_SZ, e, emitted == 0);
|
||
emitted++;
|
||
}
|
||
} else {
|
||
for (uint64_t s = start; s < total && emitted < count; s++) {
|
||
const ThreadTraceEntry *e = &s_thread_trace[s % THREAD_TRACE_CAP];
|
||
if (has_filter) {
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
if (has_kind && e->kind != kind_filter) continue;
|
||
if (has_current && e->current_tcb != current_filter) continue;
|
||
if (has_target && e->target_tcb != target_filter) continue;
|
||
if (has_either &&
|
||
e->current_tcb != either_filter &&
|
||
e->target_tcb != either_filter) continue;
|
||
}
|
||
if (pos > BUF_SZ - 1152) break;
|
||
pos = append_thread_entry(buf, pos, BUF_SZ, e, emitted == 0);
|
||
emitted++;
|
||
}
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"],\"emitted\":%d,\"oldest_seq\":%llu}",
|
||
emitted, (unsigned long long)oldest);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_thread_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
memset(s_thread_trace, 0, sizeof(s_thread_trace));
|
||
s_thread_trace_seq = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_sreg_trace_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t total = s_sreg_trace_seq;
|
||
uint64_t avail = (total < SREG_TRACE_CAP) ? total : SREG_TRACE_CAP;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"capacity\":%u}",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
(unsigned)SREG_TRACE_CAP);
|
||
}
|
||
|
||
static void handle_sreg_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
memset(s_sreg_trace, 0, sizeof(s_sreg_trace));
|
||
memset(s_sreg_last, 0, sizeof(s_sreg_last));
|
||
s_sreg_trace_seq = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static int sreg_trace_yield_func(uint32_t func)
|
||
{
|
||
return func == 0x800223E0u || func == 0x800171D4u || func == 0x8005B40Cu;
|
||
}
|
||
|
||
static size_t append_sreg_compact(char *buf, size_t pos, size_t cap,
|
||
const SregTraceEntry *e, int first)
|
||
{
|
||
return pos + snprintf(buf + pos, cap - pos,
|
||
"%s{\"seq\":%llu,\"frame\":%u,\"tcb\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"prev_s0\":\"0x%08X\",\"prev_s1\":\"0x%08X\","
|
||
"\"stk10\":\"0x%08X\",\"stk14\":\"0x%08X\",\"stk18\":\"0x%08X\","
|
||
"\"stk1c\":\"0x%08X\",\"stk20\":\"0x%08X\",\"stk40\":\"0x%08X\","
|
||
"\"task_state\":\"0x%08X\",\"task_mode\":\"0x%08X\",\"reason\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->frame, e->tcb,
|
||
e->func, e->ra, e->sp,
|
||
e->s0, e->s1, e->s2, e->s3,
|
||
e->prev_s0, e->prev_s1,
|
||
e->stack10, e->stack14, e->stack18,
|
||
e->stack1c, e->stack20, e->stack40,
|
||
e->task_state, e->task_mode, (unsigned)e->reason);
|
||
}
|
||
|
||
static void handle_sreg_trace_find(int id, const char *json)
|
||
{
|
||
uint64_t total = s_sreg_trace_seq;
|
||
uint64_t avail = (total < SREG_TRACE_CAP) ? total : SREG_TRACE_CAP;
|
||
uint64_t oldest = total - avail;
|
||
if (avail == 0) {
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"found\":false,\"total\":0,\"entries\":[]}", id);
|
||
return;
|
||
}
|
||
|
||
char val[32];
|
||
uint32_t tcb_filter = 0;
|
||
int has_tcb = 0;
|
||
if (json_get_str(json, "tcb", val, sizeof(val))) {
|
||
tcb_filter = hex_to_u32(val);
|
||
has_tcb = 1;
|
||
}
|
||
uint32_t frame_lo = 0;
|
||
uint32_t frame_hi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "frame_lo", val, sizeof(val)))
|
||
frame_lo = (uint32_t)strtoul(val, NULL, 0);
|
||
if (json_get_str(json, "frame_hi", val, sizeof(val)))
|
||
frame_hi = (uint32_t)strtoul(val, NULL, 0);
|
||
|
||
int want_zero = json_get_int(json, "zero", 1) != 0;
|
||
int yield_only = json_get_int(json, "yield_only", 1) != 0;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
int window = json_get_int(json, "window", 24);
|
||
if (window < 0) window = 0;
|
||
if (window > 80) window = 80;
|
||
|
||
uint64_t found = (uint64_t)-1;
|
||
if (newest_first) {
|
||
for (uint64_t s = total; s > oldest; ) {
|
||
s--;
|
||
const SregTraceEntry *e = &s_sreg_trace[s % SREG_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (has_tcb && e->tcb != tcb_filter) continue;
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
if (yield_only && !sreg_trace_yield_func(e->func)) continue;
|
||
if (want_zero && !(e->s0 == 0 && e->s1 == 0)) continue;
|
||
found = s;
|
||
break;
|
||
}
|
||
} else {
|
||
for (uint64_t s = oldest; s < total; s++) {
|
||
const SregTraceEntry *e = &s_sreg_trace[s % SREG_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (has_tcb && e->tcb != tcb_filter) continue;
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
if (yield_only && !sreg_trace_yield_func(e->func)) continue;
|
||
if (want_zero && !(e->s0 == 0 && e->s1 == 0)) continue;
|
||
found = s;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const size_t BUF_SZ = 512u + (size_t)(window * 2 + 1) * 640u;
|
||
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,\"available\":%llu,"
|
||
"\"oldest_seq\":%llu,\"found\":%s",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
(unsigned long long)oldest,
|
||
found == (uint64_t)-1 ? "false" : "true");
|
||
if (found != (uint64_t)-1) {
|
||
const SregTraceEntry *m = &s_sreg_trace[found % SREG_TRACE_CAP];
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
",\"match_seq\":%llu,\"match_frame\":%u,\"entries\":[",
|
||
(unsigned long long)found, m->frame);
|
||
uint64_t lo = (found > (uint64_t)window) ? found - (uint64_t)window : 0;
|
||
uint64_t hi = found + (uint64_t)window + 1u;
|
||
if (lo < oldest) lo = oldest;
|
||
if (hi > total) hi = total;
|
||
int first = 1;
|
||
for (uint64_t s = lo; s < hi && pos < BUF_SZ - 640; s++) {
|
||
const SregTraceEntry *e = &s_sreg_trace[s % SREG_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (has_tcb && e->tcb != tcb_filter) continue;
|
||
pos = append_sreg_compact(buf, pos, BUF_SZ, e, first);
|
||
first = 0;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}");
|
||
} else {
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, ",\"entries\":[]}");
|
||
}
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_sreg_trace_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > 4096) count = 4096;
|
||
|
||
uint64_t total = s_sreg_trace_seq;
|
||
uint64_t avail = (total < SREG_TRACE_CAP) ? total : SREG_TRACE_CAP;
|
||
uint64_t oldest = total - avail;
|
||
uint64_t start = total > (uint64_t)count ? total - (uint64_t)count : 0;
|
||
if (start < oldest) start = oldest;
|
||
|
||
uint32_t tcb_filter = 0;
|
||
uint32_t func_lo = 0;
|
||
uint32_t func_hi = 0xFFFFFFFFu;
|
||
uint32_t frame_lo = 0;
|
||
uint32_t frame_hi = 0xFFFFFFFFu;
|
||
int has_tcb = 0;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
char val[32];
|
||
if (json_get_str(json, "seq_lo", val, sizeof(val))) {
|
||
start = strtoull(val, NULL, 0);
|
||
if (start < oldest) start = oldest;
|
||
if (start > total) start = total;
|
||
}
|
||
if (json_get_str(json, "tcb", val, sizeof(val))) {
|
||
tcb_filter = hex_to_u32(val);
|
||
has_tcb = 1;
|
||
}
|
||
if (json_get_str(json, "func_lo", val, sizeof(val)))
|
||
func_lo = hex_to_u32(val);
|
||
if (json_get_str(json, "func_hi", val, sizeof(val)))
|
||
func_hi = hex_to_u32(val);
|
||
if (json_get_str(json, "frame_lo", val, sizeof(val)))
|
||
frame_lo = (uint32_t)strtoul(val, NULL, 0);
|
||
if (json_get_str(json, "frame_hi", val, sizeof(val)))
|
||
frame_hi = (uint32_t)strtoul(val, NULL, 0);
|
||
|
||
int has_filter = has_tcb || func_lo != 0 || func_hi != 0xFFFFFFFFu ||
|
||
frame_lo != 0 || frame_hi != 0xFFFFFFFFu;
|
||
if (has_filter && !json_get_str(json, "seq_lo", val, sizeof(val))) {
|
||
start = oldest;
|
||
}
|
||
|
||
const size_t BUF_SZ = 256u + (size_t)count * 1024u;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"oldest_seq\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
(unsigned long long)oldest);
|
||
|
||
if (newest_first) {
|
||
for (uint64_t s = total; s > start && emitted < count && pos < BUF_SZ - 1024; ) {
|
||
s--;
|
||
const SregTraceEntry *e = &s_sreg_trace[s % SREG_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (has_filter) {
|
||
if (has_tcb && e->tcb != tcb_filter) continue;
|
||
if (e->func < func_lo || e->func >= func_hi) continue;
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"tcb\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"s4\":\"0x%08X\",\"s5\":\"0x%08X\",\"s6\":\"0x%08X\",\"s7\":\"0x%08X\","
|
||
"\"prev_s0\":\"0x%08X\",\"prev_s1\":\"0x%08X\","
|
||
"\"prev_s2\":\"0x%08X\",\"prev_s3\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"stk10\":\"0x%08X\",\"stk14\":\"0x%08X\",\"stk18\":\"0x%08X\",\"stk1c\":\"0x%08X\","
|
||
"\"stk20\":\"0x%08X\",\"stk28\":\"0x%08X\",\"stk40\":\"0x%08X\","
|
||
"\"task_ptr\":\"0x%08X\",\"task_state\":\"0x%08X\","
|
||
"\"task_mode\":\"0x%08X\",\"task_submode\":\"0x%08X\","
|
||
"\"frame\":%u,\"reason\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->tcb, e->func, e->ra, e->sp,
|
||
e->s0, e->s1, e->s2, e->s3, e->s4, e->s5, e->s6, e->s7,
|
||
e->prev_s0, e->prev_s1, e->prev_s2, e->prev_s3,
|
||
e->a0, e->a1, e->a2, e->a3,
|
||
e->stack10, e->stack14, e->stack18, e->stack1c,
|
||
e->stack20, e->stack28, e->stack40,
|
||
e->task_ptr, e->task_state, e->task_mode, e->task_submode,
|
||
e->frame, (unsigned)e->reason);
|
||
emitted++;
|
||
}
|
||
} else {
|
||
for (uint64_t s = start; s < total && emitted < count && pos < BUF_SZ - 1024; s++) {
|
||
const SregTraceEntry *e = &s_sreg_trace[s % SREG_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (has_filter) {
|
||
if (has_tcb && e->tcb != tcb_filter) continue;
|
||
if (e->func < func_lo || e->func >= func_hi) continue;
|
||
if (e->frame < frame_lo || e->frame > frame_hi) continue;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"tcb\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"s4\":\"0x%08X\",\"s5\":\"0x%08X\",\"s6\":\"0x%08X\",\"s7\":\"0x%08X\","
|
||
"\"prev_s0\":\"0x%08X\",\"prev_s1\":\"0x%08X\","
|
||
"\"prev_s2\":\"0x%08X\",\"prev_s3\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"stk10\":\"0x%08X\",\"stk14\":\"0x%08X\",\"stk18\":\"0x%08X\",\"stk1c\":\"0x%08X\","
|
||
"\"stk20\":\"0x%08X\",\"stk28\":\"0x%08X\",\"stk40\":\"0x%08X\","
|
||
"\"task_ptr\":\"0x%08X\",\"task_state\":\"0x%08X\","
|
||
"\"task_mode\":\"0x%08X\",\"task_submode\":\"0x%08X\","
|
||
"\"frame\":%u,\"reason\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->tcb, e->func, e->ra, e->sp,
|
||
e->s0, e->s1, e->s2, e->s3, e->s4, e->s5, e->s6, e->s7,
|
||
e->prev_s0, e->prev_s1, e->prev_s2, e->prev_s3,
|
||
e->a0, e->a1, e->a2, e->a3,
|
||
e->stack10, e->stack14, e->stack18, e->stack1c,
|
||
e->stack20, e->stack28, e->stack40,
|
||
e->task_ptr, e->task_state, e->task_mode, e->task_submode,
|
||
e->frame, (unsigned)e->reason);
|
||
emitted++;
|
||
}
|
||
}
|
||
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void emit_probe_entry(const ProbeTraceEntry *e, int first)
|
||
{
|
||
send_fmt("%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"store_pc\":\"0x%08X\",\"byte_seq\":%u,"
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"frame\":%u,\"in_exc\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->pc, e->func,
|
||
e->last_store_pc, e->byte_seq, e->ra, e->sp,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||
e->sr, e->epc, e->istat, e->imask,
|
||
e->frame, (unsigned)e->in_exception);
|
||
}
|
||
|
||
static void handle_probe_trace(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
if (count < 0) count = 0;
|
||
if (count > (int)PROBE_TRACE_CAP) count = PROBE_TRACE_CAP;
|
||
|
||
uint64_t total = s_probe_trace_seq;
|
||
uint64_t avail = (total < PROBE_TRACE_CAP) ? total : PROBE_TRACE_CAP;
|
||
if ((uint64_t)count > avail) count = (int)avail;
|
||
uint64_t start = total - (uint64_t)count;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
for (int i = 0; i < count; i++) {
|
||
uint64_t s = start + (uint64_t)i;
|
||
const ProbeTraceEntry *e = &s_probe_trace[s % PROBE_TRACE_CAP];
|
||
emit_probe_entry(e, i == 0);
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_probe_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
memset(s_probe_trace, 0, sizeof(s_probe_trace));
|
||
s_probe_trace_seq = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
/* ---- MMIO write trace (separate ring buffer) ----
|
||
* Records every write to 0x1F801xxx MMIO registers. Unconditional (no filtering).
|
||
* 1<<22 entries, heap-allocated in debug_server_init().
|
||
* Kept in step with the SIO PC trace so generic MMIO history has enough
|
||
* retention for post-failure queries. */
|
||
#define MMIO_TRACE_CAP (1 << 18)
|
||
typedef struct {
|
||
uint64_t seq;
|
||
uint32_t addr; /* 0x1F801xxx */
|
||
uint32_t val; /* value written */
|
||
uint32_t func_addr; /* dispatch target */
|
||
uint32_t pc; /* g_debug_last_store_pc */
|
||
uint32_t cpu_pc; /* live CPUState.pc, useful across nonlocal returns */
|
||
uint32_t ra; /* $ra */
|
||
uint32_t sp; /* $sp */
|
||
uint32_t a0;
|
||
uint32_t a1;
|
||
uint32_t a2;
|
||
uint32_t a3;
|
||
uint32_t sr;
|
||
uint32_t epc;
|
||
uint32_t istat;
|
||
uint32_t imask;
|
||
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;
|
||
|
||
/* ---- GP1 display-control trace (ALWAYS-ON, dedicated) ----
|
||
* The general MMIO ring rolls over in well under a minute of gameplay
|
||
* (SPU/DMA traffic), evicting the boot-window display history before a
|
||
* post-hoc probe can ask "who toggled the display during the logo?".
|
||
* GP1 writes are ~10/frame in-game (measured: Tomba attract ≈ 600/s),
|
||
* so 512K entries ≈ 15 minutes — enough to attribute a boot window from
|
||
* well after the title screen. ~40 MB heap.
|
||
* Same entry layout as the general ring; dumped via `gp1_dump`. */
|
||
#define GP1_TRACE_CAP (1 << 19)
|
||
static MmioTraceEntry *s_gp1_trace = NULL;
|
||
static uint64_t s_gp1_trace_seq = 0;
|
||
static uint32_t s_gp1_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 ---- */
|
||
|
||
/* TCP serve-stall telemetry. The server is pumped on the main thread, so
|
||
* every millisecond spent inside send_all_blocking is a millisecond the
|
||
* emulator did not run. Surfaced in the freeze heartbeat and wedge dumps
|
||
* so a TCP-throttled run can never be misread as a guest-side bug
|
||
* (2026-06-10: two pre-fix attract "degradations" were exactly this —
|
||
* 6 fps crawl + final stall, all main-thread time inside WS2_32!send). */
|
||
static uint64_t s_tcp_send_stall_ms = 0;
|
||
static uint32_t s_tcp_clients_dropped = 0;
|
||
|
||
uint64_t debug_server_get_tcp_stall_ms(void) { return s_tcp_send_stall_ms; }
|
||
uint32_t debug_server_get_tcp_drops(void) { return s_tcp_clients_dropped; }
|
||
|
||
static uint64_t monotonic_ms(void)
|
||
{
|
||
#ifdef _WIN32
|
||
return (uint64_t)GetTickCount64();
|
||
#else
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000);
|
||
#endif
|
||
}
|
||
|
||
/* Bounded blocking send. Returns 0 on success, -1 on failure/timeout.
|
||
*
|
||
* The server is pumped on the main thread, so an unbounded send to a
|
||
* client that has stopped draining stalls the emulator until the
|
||
* starvation watchdog (4 s) kills the whole process. Each chunk gets a
|
||
* 2 s send timeout and a watchdog heartbeat. A slow-but-alive client is
|
||
* additionally bounded by a TOTAL budget per call: a trickle-draining
|
||
* client used to be able to throttle the emulator indefinitely (progress
|
||
* heartbeats kept the watchdog quiet). Past the budget the client loses
|
||
* its connection — never the runtime. Responses too big to send inside
|
||
* the budget must use the *_dump_file commands instead. */
|
||
#define SEND_TOTAL_BUDGET_MS 15000u
|
||
|
||
static int send_all_blocking(sock_t sock, const char *data, size_t len)
|
||
{
|
||
int ok = 0;
|
||
uint64_t t_start = monotonic_ms();
|
||
#ifdef _WIN32
|
||
u_long mode = 0;
|
||
ioctlsocket(sock, FIONBIO, &mode);
|
||
DWORD tmo = 2000;
|
||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (const char *)&tmo, sizeof(tmo));
|
||
#else
|
||
int flags = fcntl(sock, F_GETFL, 0);
|
||
fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
|
||
struct timeval tmo = { 2, 0 };
|
||
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tmo, sizeof(tmo));
|
||
#endif
|
||
size_t sent = 0;
|
||
while (sent < len) {
|
||
if (monotonic_ms() - t_start > SEND_TOTAL_BUDGET_MS) {
|
||
ok = -1;
|
||
break;
|
||
}
|
||
size_t want = len - sent;
|
||
if (want > (1u << 20)) want = (1u << 20);
|
||
int n = send(sock, data + sent, (int)want, 0);
|
||
if (n > 0) {
|
||
sent += (size_t)n;
|
||
/* Legitimate debug traffic in flight — not starvation. */
|
||
extern void starvation_watchdog_heartbeat(void);
|
||
starvation_watchdog_heartbeat();
|
||
continue;
|
||
}
|
||
ok = -1;
|
||
break;
|
||
}
|
||
#ifdef _WIN32
|
||
mode = 1;
|
||
ioctlsocket(sock, FIONBIO, &mode);
|
||
#else
|
||
fcntl(sock, F_SETFL, flags);
|
||
#endif
|
||
s_tcp_send_stall_ms += monotonic_ms() - t_start;
|
||
return ok;
|
||
}
|
||
|
||
void debug_server_send_line(const char *json)
|
||
{
|
||
if (s_client == SOCK_INVALID) return;
|
||
size_t len = strlen(json);
|
||
if (send_all_blocking(s_client, json, len) != 0 ||
|
||
send_all_blocking(s_client, "\n", 1) != 0) {
|
||
/* Client stopped draining — drop the connection, keep the
|
||
* runtime. The next poll() accepts a fresh client. */
|
||
sock_close(s_client);
|
||
s_client = SOCK_INVALID;
|
||
s_tcp_clients_dropped++;
|
||
}
|
||
}
|
||
|
||
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
|
||
|
||
int debug_server_dirty_break_maybe_pause(uint32_t target, CPUState *cpu)
|
||
{
|
||
if (!s_dirty_break_active) return 0;
|
||
if (target < s_dirty_break_lo || target >= s_dirty_break_hi) return 0;
|
||
|
||
/* Record the hit so dirty_break_state can be queried, but do NOT
|
||
* pause the runtime. The hit recording is its own ring (effectively
|
||
* size 1 — latest hit only). For broader history, use wtrace or
|
||
* fn_entry over the same address window instead of relying on a
|
||
* "stop and inspect" workflow. */
|
||
s_dirty_break_active = 0;
|
||
s_dirty_break_hits++;
|
||
s_dirty_break_target = target;
|
||
s_dirty_break_ra = cpu ? cpu->gpr[31] : 0;
|
||
s_dirty_break_a0 = cpu ? cpu->gpr[4] : 0;
|
||
s_dirty_break_a1 = cpu ? cpu->gpr[5] : 0;
|
||
s_dirty_break_a2 = cpu ? cpu->gpr[6] : 0;
|
||
s_dirty_break_a3 = cpu ? cpu->gpr[7] : 0;
|
||
s_dirty_break_sp = cpu ? cpu->gpr[29] : 0;
|
||
s_dirty_break_frame = (uint32_t)s_frame_count;
|
||
return 1;
|
||
}
|
||
|
||
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;
|
||
/* Surface accumulated dispatch misses on every ping so they can't go
|
||
* unnoticed across sessions (NES recomp template PRINCIPLES.md §13a:
|
||
* "A dispatch miss is a SILENT GAME-BREAKING BUG"). 0 = healthy. */
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%llu,"
|
||
"\"dispatch_miss_total\":%llu,"
|
||
"\"dispatch_miss_unique\":%d}",
|
||
id, (unsigned long long)s_frame_count,
|
||
(unsigned long long)s_unknown_seq,
|
||
s_unknown_unique_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;
|
||
/* Effectively the entire 2 MB RAM in one shot. Response uses a heap-
|
||
* sized envelope so we don't truncate. */
|
||
if (len > 0x200000) len = 0x200000;
|
||
|
||
/* Heap buffer for hex chars + JSON envelope. Each byte = 2 hex chars. */
|
||
size_t env = 256;
|
||
size_t total = (size_t)len * 2 + env;
|
||
char *out = (char *)malloc(total);
|
||
if (!out) { send_err(id, "alloc failed"); return; }
|
||
int hdr = snprintf(out, env,
|
||
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"len\":%d,\"hex\":\"",
|
||
id, addr, len);
|
||
char *hex = out + hdr;
|
||
/* Nibble-table encode: snprintf per byte costs seconds for a 2 MB
|
||
* read, which stalls the main-thread-pumped server (and the SDL
|
||
* event loop) long enough to look like a wedge. */
|
||
static const char H[] = "0123456789abcdef";
|
||
for (int i = 0; i < len; i++) {
|
||
uint8_t b = psx_read_byte(addr + (uint32_t)i);
|
||
hex[(size_t)i * 2] = H[b >> 4];
|
||
hex[(size_t)i * 2 + 1] = H[b & 0xF];
|
||
}
|
||
char *tail = hex + (size_t)len * 2;
|
||
memcpy(tail, "\"}", 3);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
/* "dump_ram" is an alias of "read_ram". The old implementation answered a
|
||
* single request with one response line per 256-byte chunk; any client that
|
||
* follows the one-request/one-response protocol left the extra lines unread,
|
||
* the socket send buffer filled, the main-thread-pumped server blocked, and
|
||
* the freeze watchdog killed the process. One request, one response. */
|
||
|
||
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,"
|
||
"\"depth\":%d,\"depth24\":%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.depth24 ? 24 : 15, di.depth24,
|
||
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_mem_words(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 count = json_get_int(json, "count", 16);
|
||
if (count < 1) count = 1;
|
||
if (count > 256) count = 256;
|
||
|
||
size_t bufsz = 256u + (size_t)count * 32u;
|
||
char *buf = (char *)malloc(bufsz);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"words\":[",
|
||
id, addr);
|
||
for (int i = 0; i < count && pos < bufsz - 32; i++) {
|
||
uint32_t a = addr + (uint32_t)i * 4u;
|
||
uint32_t v = psx_read_word(a);
|
||
pos += snprintf(buf + pos, bufsz - pos, "%s\"0x%08X\"",
|
||
i ? "," : "", v);
|
||
}
|
||
pos += snprintf(buf + pos, bufsz - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
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());
|
||
}
|
||
|
||
static void handle_timers_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint16_t counter[3], target[3];
|
||
uint32_t mode[3], frac[3];
|
||
int32_t irq_line[3];
|
||
timers_get_snapshot(counter, mode, target, irq_line, frac);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"timers\":["
|
||
"{\"ch\":0,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u,"
|
||
"\"irq_line\":%d,\"frac\":%u},"
|
||
"{\"ch\":1,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u,"
|
||
"\"irq_line\":%d,\"frac\":%u},"
|
||
"{\"ch\":2,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u,"
|
||
"\"irq_line\":%d,\"frac\":%u}]}",
|
||
id,
|
||
counter[0], mode[0], target[0], irq_line[0], frac[0],
|
||
counter[1], mode[1], target[1], irq_line[1], frac[1],
|
||
counter[2], mode[2], target[2], irq_line[2], frac[2]);
|
||
}
|
||
|
||
/* GPU opcode counter — defined in gpu.c */
|
||
static const char *cdrom_trace_kind_name(uint8_t kind)
|
||
{
|
||
switch (kind) {
|
||
case 'N': return "init";
|
||
case 'C': return "cmd";
|
||
case 'I': return "set_irq";
|
||
case 'F': return "fire_irq";
|
||
case 'f': return "irq_masked";
|
||
case 'S': return "sector";
|
||
case 's': return "sector_skip";
|
||
case 'A': return "xa_audio";
|
||
case 'a': return "xa_skip";
|
||
case 'X': return "xa_unsupported";
|
||
case 'O': return "overwrite";
|
||
case 'R': return "read";
|
||
case 'W': return "write";
|
||
case 'D': return "dma";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void handle_cdrom_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
CDROMDebugState s;
|
||
cdrom_debug_snapshot(&s);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"seq\":%llu,\"has_disc\":%d,"
|
||
"\"index\":%u,\"stat\":\"0x%02X\","
|
||
"\"request\":\"0x%02X\","
|
||
"\"irq_enable\":\"0x%02X\",\"irq_flag\":\"0x%02X\","
|
||
"\"mode\":\"0x%02X\","
|
||
"\"param_count\":%d,\"response_read\":%d,\"response_count\":%d,"
|
||
"\"sector_available\":%d,\"sector_read_pos\":%d,\"sector_size\":%d,"
|
||
"\"reading\":%d,\"read_msf\":[%d,%d,%d],"
|
||
"\"read_cmd\":\"0x%02X\",\"read_delay\":%d,"
|
||
"\"filter_file\":%u,\"filter_channel\":%u,\"muted\":%u,"
|
||
"\"seek_msf\":[%u,%u,%u],"
|
||
"\"pending\":{\"cmd\":\"0x%02X\",\"active\":%d,\"delay\":%d,\"phase\":%d},"
|
||
"\"last_sector\":{\"lba\":%d,\"size\":%d,\"frame\":%u,"
|
||
"\"mode\":\"0x%02X\",\"have_raw\":%u},"
|
||
"\"i_stat\":\"0x%08X\"}",
|
||
id, (unsigned long long)s.seq, s.has_disc,
|
||
s.index_reg, s.stat_reg, s.request_reg, s.irq_enable, s.irq_flag,
|
||
s.mode_reg,
|
||
s.param_count, s.response_read, s.response_count,
|
||
s.sector_available, s.sector_read_pos, s.sector_size,
|
||
s.reading, s.read_min, s.read_sec, s.read_sect,
|
||
s.read_cmd, s.read_delay,
|
||
s.filter_file, s.filter_channel, s.muted,
|
||
s.seek_min, s.seek_sec, s.seek_sect,
|
||
s.pending_cmd, s.pending_pending, s.pending_delay,
|
||
s.pending_phase,
|
||
s.last_sector_lba, s.last_sector_size, s.last_sector_frame,
|
||
s.last_sector_mode, s.last_sector_have_raw,
|
||
s.i_stat);
|
||
}
|
||
|
||
static void handle_cdrom_sector_dump(int id, const char *json)
|
||
{
|
||
int offset = json_get_int(json, "offset", 0);
|
||
int len = json_get_int(json, "len", 128);
|
||
if (offset < 0) offset = 0;
|
||
if (len < 1) len = 1;
|
||
if (len > 2340) len = 2340;
|
||
|
||
uint8_t *bytes = (uint8_t *)malloc((size_t)len);
|
||
if (!bytes) { send_err(id, "oom"); return; }
|
||
|
||
CDROMSectorDebugState s;
|
||
uint32_t got = cdrom_debug_copy_last_sector((uint32_t)offset,
|
||
(uint32_t)len,
|
||
bytes, &s);
|
||
|
||
size_t bufsz = 512u + (size_t)got * 2u;
|
||
char *buf = (char *)malloc(bufsz);
|
||
if (!buf) {
|
||
free(bytes);
|
||
send_err(id, "oom");
|
||
return;
|
||
}
|
||
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"{\"id\":%d,\"ok\":true,"
|
||
"\"current\":{\"available\":%d,\"read_pos\":%d,\"size\":%d},"
|
||
"\"last\":{\"lba\":%d,\"size\":%d,\"frame\":%u,"
|
||
"\"mode\":\"0x%02X\",\"have_raw\":%u},"
|
||
"\"offset\":%d,\"len\":%u,\"hex\":\"",
|
||
id,
|
||
s.current_available, s.current_read_pos, s.current_size,
|
||
s.last_lba, s.last_size, s.last_frame,
|
||
s.last_mode, s.last_have_raw,
|
||
offset, got);
|
||
for (uint32_t i = 0; i < got && pos + 3 < bufsz; i++) {
|
||
pos += snprintf(buf + pos, bufsz - pos, "%02x", bytes[i]);
|
||
}
|
||
snprintf(buf + pos, bufsz - pos, "\"}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
free(bytes);
|
||
}
|
||
|
||
static void append_hex_bytes(char *buf, size_t bufsz, size_t *pos,
|
||
const uint8_t *bytes, uint32_t len)
|
||
{
|
||
for (uint32_t i = 0; i < len && *pos + 3 < bufsz; i++) {
|
||
*pos += snprintf(buf + *pos, bufsz - *pos, "%02x", bytes[i]);
|
||
}
|
||
}
|
||
|
||
static void handle_cdrom_sector_history(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 64);
|
||
if (count < 1) count = 1;
|
||
if (count > CDROM_SECTOR_HISTORY_CAP) count = CDROM_SECTOR_HISTORY_CAP;
|
||
|
||
int filter_lba = -1;
|
||
char lba_str[32];
|
||
if (json_get_str(json, "lba", lba_str, sizeof(lba_str))) {
|
||
filter_lba = (int)hex_to_u32(lba_str);
|
||
}
|
||
|
||
const CDROMSectorHistoryEntry *entries = NULL;
|
||
uint64_t total = cdrom_debug_get_sector_history(&entries);
|
||
uint64_t oldest = (total > CDROM_SECTOR_HISTORY_CAP)
|
||
? total - CDROM_SECTOR_HISTORY_CAP : 0;
|
||
|
||
size_t bufsz = 256u + (size_t)count * 760u;
|
||
char *buf = (char *)malloc(bufsz);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,"
|
||
"\"oldest\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total,
|
||
(unsigned long long)oldest);
|
||
|
||
uint64_t seq = total;
|
||
while (seq > oldest && emitted < count && pos < bufsz - 760) {
|
||
seq--;
|
||
const CDROMSectorHistoryEntry *e =
|
||
&entries[seq % CDROM_SECTOR_HISTORY_CAP];
|
||
if (e->seq != seq) continue;
|
||
if (filter_lba >= 0 && e->lba != filter_lba) continue;
|
||
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"%s{\"seq\":%llu,\"lba\":%d,\"size\":%d,"
|
||
"\"frame\":%u,\"mode\":\"0x%02X\","
|
||
"\"have_raw\":%u,\"raw_mode\":\"0x%02X\","
|
||
"\"xa_file\":%u,\"xa_channel\":%u,"
|
||
"\"xa_submode\":\"0x%02X\",\"xa_coding\":\"0x%02X\","
|
||
"\"data_delivered\":%u,\"xa_audio_delivered\":%u,"
|
||
"\"skip_reason\":%u,\"bytes_len\":%u,\"hex\":\"",
|
||
emitted ? "," : "",
|
||
(unsigned long long)e->seq, e->lba, e->size,
|
||
e->frame, e->mode, e->have_raw, e->raw_mode,
|
||
e->xa_file, e->xa_channel, e->xa_submode, e->xa_coding,
|
||
e->data_delivered, e->xa_audio_delivered,
|
||
e->skip_reason, e->bytes_len);
|
||
append_hex_bytes(buf, bufsz, &pos, e->bytes, e->bytes_len);
|
||
pos += snprintf(buf + pos, bufsz - pos, "\"}");
|
||
emitted++;
|
||
}
|
||
|
||
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_cdrom_sector_history_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
cdrom_debug_clear_sector_history();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_cdrom_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
cdrom_debug_clear_trace();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_cdrom_trace_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > CDROM_TRACE_CAP) count = CDROM_TRACE_CAP;
|
||
|
||
const CDROMTraceEntry *entries = NULL;
|
||
uint64_t total = cdrom_debug_get_trace(&entries);
|
||
uint64_t oldest = (total > CDROM_TRACE_CAP) ? total - CDROM_TRACE_CAP : 0;
|
||
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
|
||
if (start < oldest) start = oldest;
|
||
|
||
size_t bufsz = 256u + (size_t)count * 360u;
|
||
char *buf = (char *)malloc(bufsz);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)oldest);
|
||
for (uint64_t seq = start; seq < total && pos < bufsz - 400; seq++) {
|
||
const CDROMTraceEntry *e = &entries[seq % CDROM_TRACE_CAP];
|
||
if (e->seq != seq) continue;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"%s{\"seq\":%llu,\"kind\":\"%s\",\"addr\":\"0x%08X\","
|
||
"\"val\":\"0x%08X\",\"w\":%u,\"func\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"frame\":%u,\"i_stat\":\"0x%08X\","
|
||
"\"index\":%u,\"stat\":\"0x%02X\","
|
||
"\"request\":\"0x%02X\","
|
||
"\"irq_enable\":\"0x%02X\",\"irq_flag\":\"0x%02X\","
|
||
"\"mode\":\"0x%02X\","
|
||
"\"param\":%u,\"resp_read\":%u,\"resp_count\":%u,"
|
||
"\"sector_avail\":%u,\"sector_pos\":%d,\"sector_size\":%d,"
|
||
"\"pending_cmd\":\"0x%02X\",\"pending\":%u,"
|
||
"\"pending_delay\":%d,\"reading\":%u,"
|
||
"\"read_cmd\":\"0x%02X\",\"read_delay\":%d}",
|
||
emitted ? "," : "",
|
||
(unsigned long long)e->seq, cdrom_trace_kind_name(e->kind),
|
||
e->addr, e->val, (unsigned)e->width,
|
||
e->func, e->pc, e->frame, e->i_stat,
|
||
e->index_reg, e->stat_reg, e->request_reg, e->irq_enable, e->irq_flag,
|
||
e->mode_reg,
|
||
e->param_count, e->response_read, e->response_count,
|
||
e->sector_available, e->sector_read_pos, e->sector_size,
|
||
e->pending_cmd, e->pending_pending,
|
||
e->pending_delay, e->reading,
|
||
e->read_cmd, e->read_delay);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static const char *dma_trace_kind_name(uint32_t kind)
|
||
{
|
||
switch (kind) {
|
||
case 'S': return "start";
|
||
case 'C': return "complete";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void handle_dma_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
DMADebugState s;
|
||
dma_debug_get_state(&s);
|
||
|
||
char buf[2048];
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"{\"id\":%d,\"ok\":true,\"dpcr\":\"0x%08X\","
|
||
"\"dicr\":\"0x%08X\",\"channels\":[",
|
||
id, s.dpcr, s.dicr);
|
||
for (int i = 0; i < 7 && pos < sizeof(buf) - 192; i++) {
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"%s{\"ch\":%d,\"madr\":\"0x%08X\","
|
||
"\"bcr\":\"0x%08X\",\"chcr\":\"0x%08X\","
|
||
"\"active\":%u,\"remaining_words\":%u,"
|
||
"\"cycles_accum\":%u}",
|
||
i ? "," : "",
|
||
i, s.channels[i].madr, s.channels[i].bcr,
|
||
s.channels[i].chcr, s.channels[i].active,
|
||
s.channels[i].remaining_words,
|
||
s.channels[i].cycles_accum);
|
||
}
|
||
snprintf(buf + pos, sizeof(buf) - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
}
|
||
|
||
static void handle_dma_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
dma_debug_clear_trace();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_dma_trace_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > DMA_TRACE_CAP) count = DMA_TRACE_CAP;
|
||
|
||
const DMATraceEntry *entries = NULL;
|
||
uint64_t total = dma_debug_get_trace(&entries);
|
||
uint64_t oldest = (total > DMA_TRACE_CAP) ? total - DMA_TRACE_CAP : 0;
|
||
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
|
||
if (start < oldest) start = oldest;
|
||
|
||
size_t bufsz = 256u + (size_t)count * 512u;
|
||
char *buf = (char *)malloc(bufsz);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)oldest);
|
||
for (uint64_t seq = start; seq < total && pos < bufsz - 512; seq++) {
|
||
const DMATraceEntry *e = &entries[seq % DMA_TRACE_CAP];
|
||
if (e->seq != seq) continue;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\",\"ch\":%u,"
|
||
"\"words\":%u,\"madr\":\"0x%08X\",\"bcr\":\"0x%08X\","
|
||
"\"chcr\":\"0x%08X\",\"dpcr\":\"0x%08X\","
|
||
"\"dicr_before\":\"0x%08X\",\"dicr_after\":\"0x%08X\","
|
||
"\"i_stat_before\":\"0x%08X\",\"i_stat_after\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"pc\":\"0x%08X\"}",
|
||
emitted ? "," : "",
|
||
(unsigned long long)e->seq, e->frame,
|
||
dma_trace_kind_name(e->kind), e->channel,
|
||
e->total_words, e->madr, e->bcr, e->chcr, e->dpcr,
|
||
e->dicr_before, e->dicr_after,
|
||
e->i_stat_before, e->i_stat_after,
|
||
e->func, e->pc);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
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_gpu_ring_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint32_t oldest = 0, newest = 0;
|
||
gpu_gp0_ring_frame_span(&oldest, &newest);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%u,"
|
||
"\"max_words\":%u,\"oldest_frame\":%u,\"newest_frame\":%u}",
|
||
id,
|
||
(unsigned long long)gpu_gp0_ring_total(),
|
||
gpu_gp0_ring_capacity(),
|
||
gpu_gp0_ring_max_words(),
|
||
oldest, newest);
|
||
}
|
||
|
||
static void handle_gpu_frame_dump(int id, const char *json)
|
||
{
|
||
int target = json_get_int(json, "frame", -1);
|
||
if (target < 0) { send_err(id, "missing frame"); return; }
|
||
int max_entries = json_get_int(json, "count", 8192);
|
||
if (max_entries < 1) max_entries = 1;
|
||
if (max_entries > 65536) max_entries = 65536;
|
||
|
||
GpuGp0RingEntry *entries = (GpuGp0RingEntry *)malloc(
|
||
(size_t)max_entries * sizeof(GpuGp0RingEntry));
|
||
if (!entries) { send_err(id, "alloc failed"); return; }
|
||
|
||
int n = gpu_gp0_ring_dump_frame((uint32_t)target, entries, max_entries);
|
||
|
||
/* ~190 bytes per entry in JSON; budget conservatively. */
|
||
size_t buf_sz = 256 + (size_t)n * 280u;
|
||
char *buf = (char *)malloc(buf_sz);
|
||
if (!buf) { free(entries); send_err(id, "alloc failed"); return; }
|
||
|
||
size_t pos = (size_t)snprintf(buf, buf_sz,
|
||
"{\"id\":%d,\"ok\":true,\"frame\":%u,\"count\":%d,\"max_words\":%u,\"entries\":[",
|
||
id, (uint32_t)target, n, gpu_gp0_ring_max_words());
|
||
|
||
for (int i = 0; i < n && pos < buf_sz - 256; i++) {
|
||
const GpuGp0RingEntry *e = &entries[i];
|
||
pos += (size_t)snprintf(buf + pos, buf_sz - pos,
|
||
"%s{\"seq\":%u,\"op\":\"0x%02X\",\"n\":%u,"
|
||
"\"src\":\"0x%08X\",\"pc\":\"0x%08X\",\"w\":[",
|
||
i ? "," : "", e->seq, e->opcode, e->n_words,
|
||
e->src_addr, e->pc);
|
||
int show = e->n_words < GPU_GP0_RING_MAX_WORDS
|
||
? e->n_words : GPU_GP0_RING_MAX_WORDS;
|
||
for (int k = 0; k < show && pos < buf_sz - 32; k++) {
|
||
pos += (size_t)snprintf(buf + pos, buf_sz - pos,
|
||
"%s\"0x%08X\"", k ? "," : "", e->cmd[k]);
|
||
}
|
||
pos += (size_t)snprintf(buf + pos, buf_sz - pos, "]}");
|
||
}
|
||
snprintf(buf + pos, buf_sz - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
free(entries);
|
||
}
|
||
|
||
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 json_escape_string(char *dst, size_t dst_size, const char *src)
|
||
{
|
||
static const char hex[] = "0123456789ABCDEF";
|
||
size_t out = 0;
|
||
if (!dst || dst_size == 0) return;
|
||
if (!src) src = "";
|
||
|
||
for (size_t in = 0; src[in] && out + 1 < dst_size; in++) {
|
||
unsigned char c = (unsigned char)src[in];
|
||
if (c == '"' || c == '\\') {
|
||
if (out + 2 >= dst_size) break;
|
||
dst[out++] = '\\';
|
||
dst[out++] = (char)c;
|
||
} else if (c < 0x20) {
|
||
if (out + 6 >= dst_size) break;
|
||
dst[out++] = '\\';
|
||
dst[out++] = 'u';
|
||
dst[out++] = '0';
|
||
dst[out++] = '0';
|
||
dst[out++] = hex[c >> 4];
|
||
dst[out++] = hex[c & 0x0F];
|
||
} else {
|
||
dst[out++] = (char)c;
|
||
}
|
||
}
|
||
dst[out] = '\0';
|
||
}
|
||
|
||
static void handle_mc_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const char *p0 = "", *p1 = "";
|
||
char p0_json[1024], p1_json[1024];
|
||
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);
|
||
json_escape_string(p0_json, sizeof(p0_json), p0);
|
||
json_escape_string(p1_json, sizeof(p1_json), p1);
|
||
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_json,
|
||
(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_json,
|
||
(m1[0] >= 0x20 && m1[0] < 0x7F) ? m1[0] : '?',
|
||
(m1[1] >= 0x20 && m1[1] < 0x7F) ? m1[1] : '?',
|
||
m1[0], m1[1]);
|
||
}
|
||
|
||
static void handle_spu_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
SpuDebugInfo info;
|
||
spu_debug_info(&info);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"ctrl\":\"0x%04X\",\"active_mask\":\"0x%06X\","
|
||
"\"main_l\":%d,\"main_r\":%d,"
|
||
"\"cd_l\":%d,\"cd_r\":%d,"
|
||
"\"key_on_count\":%u,"
|
||
"\"render_frames\":%llu,\"nonzero_frames\":%llu,"
|
||
"\"last_peak\":%d,\"peak\":%d,"
|
||
"\"cd_frames\":%u,\"cd_push_frames\":%llu,"
|
||
"\"cd_overflow_frames\":%llu,\"cd_underflow_frames\":%llu}",
|
||
id,
|
||
info.ctrl & 0xFFFFu,
|
||
info.active_mask & 0xFFFFFFu,
|
||
info.main_l,
|
||
info.main_r,
|
||
info.cd_l,
|
||
info.cd_r,
|
||
info.key_on_count,
|
||
(unsigned long long)info.render_frames,
|
||
(unsigned long long)info.nonzero_frames,
|
||
info.last_peak,
|
||
info.peak,
|
||
info.cd_frames,
|
||
(unsigned long long)info.cd_push_frames,
|
||
(unsigned long long)info.cd_overflow_frames,
|
||
(unsigned long long)info.cd_underflow_frames);
|
||
}
|
||
|
||
/* ---- Per-voice SPU snapshot. Mirrors fields the Beetle oracle exposes
|
||
* via PS_SPU::GetRegister(GSREG_V0_*) so cross-process diff tooling sees
|
||
* the same JSON schema on both port 4370 and 4380.
|
||
*
|
||
* Single-shot emission: assemble the entire response into a heap buffer
|
||
* and fire one send_fmt. debug_server_send_line appends '\n' on every
|
||
* call, so multi-call patterns produce multi-line garbage on the wire. */
|
||
static void handle_spu_voices(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
SpuGlobalState g;
|
||
spu_get_global_state(&g);
|
||
|
||
size_t cap = 8192;
|
||
char *out = (char *)malloc(cap);
|
||
if (!out) { send_fmt("{\"id\":%d,\"ok\":false,\"err\":\"alloc\"}", id); return; }
|
||
size_t off = 0;
|
||
int n = snprintf(out + off, cap - off,
|
||
"{\"id\":%d,\"ok\":true,"
|
||
"\"ctrl\":\"0x%04X\",\"main_l\":\"0x%04X\",\"main_r\":\"0x%04X\","
|
||
"\"kon\":\"0x%06X\",\"koff\":\"0x%06X\","
|
||
"\"pmon\":\"0x%06X\",\"non\":\"0x%06X\",\"eon\":\"0x%06X\","
|
||
"\"endx\":\"0x%06X\",\"active_mask\":\"0x%06X\","
|
||
"\"voices\":[",
|
||
id,
|
||
g.ctrl, g.main_vol_l, g.main_vol_r,
|
||
g.kon_latch, g.koff_latch,
|
||
g.pmon, g.non, g.eon,
|
||
g.endx, g.active_mask);
|
||
if (n > 0) off += (size_t)n;
|
||
|
||
for (int v = 0; v < 24; v++) {
|
||
SpuVoiceState s;
|
||
spu_get_voice_state(v, &s);
|
||
n = snprintf(out + off, cap - off,
|
||
"%s{\"v\":%d,\"active\":%d,"
|
||
"\"vol_l\":\"0x%04X\",\"vol_r\":\"0x%04X\","
|
||
"\"pitch\":\"0x%04X\","
|
||
"\"start\":\"0x%05X\",\"loop\":\"0x%05X\","
|
||
"\"adsr_lo\":\"0x%04X\",\"adsr_hi\":\"0x%04X\","
|
||
"\"cur_addr\":\"0x%05X\",\"repeat_addr\":\"0x%05X\","
|
||
"\"flags\":\"0x%02X\",\"sample_idx\":%d,\"phase\":\"0x%04X\"}",
|
||
v == 0 ? "" : ",",
|
||
v, s.active,
|
||
s.vol_ctrl_l, s.vol_ctrl_r,
|
||
s.pitch,
|
||
(uint32_t)s.start_lo << 3,
|
||
(uint32_t)s.loop_lo << 3,
|
||
s.adsr_lo, s.adsr_hi,
|
||
s.cur_addr, s.repeat_addr,
|
||
s.last_flags, s.sample_idx, s.phase);
|
||
if (n > 0) off += (size_t)n;
|
||
}
|
||
n = snprintf(out + off, cap - off, "]}");
|
||
if (n > 0) off += (size_t)n;
|
||
send_fmt("%s", out);
|
||
free(out);
|
||
}
|
||
|
||
/* ---- SPU event ring dump. Returns the most recent N events
|
||
* (KEYON / KEYOFF / END_STOP / END_LOOP) with frame timestamps. */
|
||
static void handle_spu_events(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > 4096) count = 4096;
|
||
SpuEvent *evs = (SpuEvent *)malloc((size_t)count * sizeof(SpuEvent));
|
||
if (!evs) { send_fmt("{\"id\":%d,\"ok\":false,\"err\":\"alloc\"}", id); return; }
|
||
uint32_t got = spu_event_get(evs, (uint32_t)count);
|
||
uint64_t total = spu_event_total();
|
||
static const char *kind_names[5] = { "?", "KEYON", "KEYOFF", "END_STOP", "END_LOOP" };
|
||
|
||
/* Worst case ~200 chars per event; 64 KB is plenty for 4096 events. */
|
||
size_t cap = 256u + (size_t)got * 256u;
|
||
char *out = (char *)malloc(cap);
|
||
if (!out) { free(evs); send_fmt("{\"id\":%d,\"ok\":false,\"err\":\"alloc\"}", id); return; }
|
||
size_t off = 0;
|
||
int n = snprintf(out + off, cap - off,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%u,\"events\":[",
|
||
id, (unsigned long long)total, (unsigned)got);
|
||
if (n > 0) off += (size_t)n;
|
||
for (uint32_t i = 0; i < got; i++) {
|
||
const SpuEvent *e = &evs[i];
|
||
const char *kn = (e->kind <= 4) ? kind_names[e->kind] : "?";
|
||
n = snprintf(out + off, cap - off,
|
||
"%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\",\"v\":%d,"
|
||
"\"pitch\":\"0x%04X\",\"addr\":\"0x%05X\","
|
||
"\"adsr_lo\":\"0x%04X\",\"adsr_hi\":\"0x%04X\","
|
||
"\"vol_l\":\"0x%04X\",\"vol_r\":\"0x%04X\"}",
|
||
i == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->frame, kn, (int)e->voice,
|
||
e->pitch, e->addr,
|
||
e->adsr_lo, e->adsr_hi,
|
||
e->vol_l, e->vol_r);
|
||
if (n > 0) off += (size_t)n;
|
||
}
|
||
n = snprintf(out + off, cap - off, "]}");
|
||
if (n > 0) off += (size_t)n;
|
||
send_fmt("%s", out);
|
||
free(out);
|
||
free(evs);
|
||
}
|
||
|
||
static void handle_spu_events_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
spu_event_reset();
|
||
send_fmt("{\"id\":%d,\"ok\":true}\n", id);
|
||
}
|
||
|
||
/* ---- SIO IRQ-arm audit -----------------------------------------------
|
||
* Reports counts of TX writes that reached the IRQ-arm decision in
|
||
* sio_write SIO_TX_DATA, partitioned by active_device. Tells us at the
|
||
* arm-time gate whether ACK + ACK_IRQ_EN were set, or which side blocked
|
||
* the arm. Steps 3-4 of the IRQ-chain audit. */
|
||
extern void sio_get_card_arm_audit(uint32_t out[3][7]);
|
||
extern int sio_get_card_arm_countdown_after(void);
|
||
extern void sio_get_burst_stats(uint64_t out[10]);
|
||
extern void sio_get_pace_state(uint64_t out[16]);
|
||
extern volatile int g_sio_timing_active;
|
||
|
||
/* Phase 1.0c-v2 telemetry: cycle-paced SIO state snapshot. Read-only.
|
||
* In 1.0c-v2 the TX path is still synchronous, g_sio_timing_active
|
||
* stays 0, and all dynamic shifter/ack fields stay zero. */
|
||
static void handle_pace_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t s[16];
|
||
sio_get_pace_state(s);
|
||
const char *model = s[0] ? "cycle_paced" : "access_legacy";
|
||
const char *owner = s[7] == 0 ? "none" : s[7] == 1 ? "card"
|
||
: s[7] == 2 ? "pad" : "unknown";
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"sio_model\":\"%s\","
|
||
"\"sio_quantum_cycles\":%llu,"
|
||
"\"timing_active\":%d,"
|
||
"\"shift_active\":%llu,"
|
||
"\"shift_remaining\":%llu,"
|
||
"\"tx_buffered\":%llu,"
|
||
"\"ack_active\":%llu,"
|
||
"\"ack_remaining\":%llu,"
|
||
"\"bus_owner\":\"%s\","
|
||
"\"bus_byte_index\":%llu,"
|
||
"\"tx_writes_buffered\":%llu,"
|
||
"\"tx_writes_dropped_busy\":%llu,"
|
||
"\"tx_writes_dropped_cross_device\":%llu,"
|
||
"\"tx_buffer_promoted\":%llu,"
|
||
"\"tx_buffer_promoted_during_card\":%llu,"
|
||
"\"pad_byte_processed_in_card_data\":%llu,"
|
||
"\"cross_device_pad_during_card\":%llu}\n",
|
||
id, model,
|
||
(unsigned long long)s[1],
|
||
g_sio_timing_active,
|
||
(unsigned long long)s[2], (unsigned long long)s[3],
|
||
(unsigned long long)s[4], (unsigned long long)s[5],
|
||
(unsigned long long)s[6],
|
||
owner,
|
||
(unsigned long long)s[8], (unsigned long long)s[9],
|
||
(unsigned long long)s[10], (unsigned long long)s[11],
|
||
(unsigned long long)s[12], (unsigned long long)s[13],
|
||
(unsigned long long)s[14], (unsigned long long)s[15]);
|
||
}
|
||
|
||
static void handle_sio_burst_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t s[10];
|
||
sio_get_burst_stats(s);
|
||
const char *reason_str =
|
||
s[8] == 1 ? "idle" :
|
||
s[8] == 2 ? "mode_clear" :
|
||
s[8] == 3 ? "capped" : "n/a";
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"calls\":%llu,\"iters_total\":%llu,\"iter_max\":%llu,"
|
||
"\"break_idle\":%llu,\"break_mode_clear\":%llu,\"break_capped\":%llu,"
|
||
"\"fires_in_burst\":%llu,\"last_iters\":%llu,"
|
||
"\"last_break_reason\":\"%s\"}\n",
|
||
id,
|
||
(unsigned long long)s[0], (unsigned long long)s[1], (unsigned long long)s[2],
|
||
(unsigned long long)s[3], (unsigned long long)s[4], (unsigned long long)s[5],
|
||
(unsigned long long)s[6], (unsigned long long)s[7],
|
||
reason_str);
|
||
}
|
||
|
||
static void handle_sio_arm_audit(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint32_t a[3][7];
|
||
sio_get_card_arm_audit(a);
|
||
int cd_after = sio_get_card_arm_countdown_after();
|
||
const char *names[3] = { "card", "pad", "none" };
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"countdown_after_last_card\":%d,\"by_device\":{",
|
||
id, cd_after);
|
||
for (int i = 0; i < 3; i++) {
|
||
if (i > 0) send_fmt(",");
|
||
send_fmt("\"%s\":{\"tx_total\":%u,\"armed\":%u,\"no_ack\":%u,\"no_ackirqen\":%u,"
|
||
"\"ctrl_last\":\"0x%04X\",\"stat_pre_last\":\"0x%04X\",\"stat_post_last\":\"0x%04X\"}",
|
||
names[i], a[i][0], a[i][1], a[i][2], a[i][3], a[i][4], a[i][5], a[i][6]);
|
||
}
|
||
send_fmt("}}\n");
|
||
}
|
||
|
||
/* ---- Memory card raw buffer dump (in-memory cards[].data) ----
|
||
* Used by the audit harness to verify that what the runtime loaded matches
|
||
* the on-disk file byte-for-byte. Returns hex string, chunked. */
|
||
static void handle_card_buffer_dump(int id, const char *json)
|
||
{
|
||
int slot = json_get_int(json, "slot", 0);
|
||
int offset = json_get_int(json, "offset", 0);
|
||
int len = json_get_int(json, "len", 256);
|
||
if (slot < 0 || slot > 1) { send_err(id, "bad slot"); return; }
|
||
if (offset < 0 || offset > 0x20000) { send_err(id, "bad offset"); return; }
|
||
if (len < 1) { send_err(id, "bad len"); return; }
|
||
if (len > 0x20000) len = 0x20000;
|
||
if (offset + len > 0x20000) len = 0x20000 - offset;
|
||
|
||
uint8_t *buf = (uint8_t *)malloc((size_t)len);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
int got = memcard_debug_read_buffer(slot, (uint32_t)offset, (uint32_t)len, buf);
|
||
if (got <= 0) { free(buf); send_err(id, "slot empty or read failed"); return; }
|
||
|
||
/* hex envelope: 2 chars/byte + ~256 envelope */
|
||
size_t env = 256;
|
||
size_t total = (size_t)got * 2 + env;
|
||
char *out = (char *)malloc(total);
|
||
if (!out) { free(buf); send_err(id, "alloc failed"); return; }
|
||
int hdr = snprintf(out, env,
|
||
"{\"id\":%d,\"ok\":true,\"slot\":%d,\"offset\":%d,\"len\":%d,\"hex\":\"",
|
||
id, slot, offset, got);
|
||
char *hex = out + hdr;
|
||
for (int i = 0; i < got; i++)
|
||
snprintf(hex + (size_t)i * 2, 3, "%02x", buf[i]);
|
||
char *tail = hex + (size_t)got * 2;
|
||
memcpy(tail, "\"}", 3);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
free(buf);
|
||
}
|
||
|
||
/* ---- I_MASK bit 7 trace (card protocol flow) ---- */
|
||
typedef struct {
|
||
uint32_t old_mask;
|
||
uint32_t new_mask;
|
||
uint32_t caller;
|
||
uint32_t store_pc;
|
||
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", 64);
|
||
int only_b7c = json_get_int(json, "only_b7c", 0);
|
||
int idx, total;
|
||
const ImaskTraceEntry *buf = memory_get_imask_trace(&idx, &total);
|
||
int cap = 4096; /* 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);
|
||
|
||
int first = 1;
|
||
for (int i = 0; i < count; i++) {
|
||
int ii = (start + i) % cap;
|
||
const ImaskTraceEntry *e = &buf[ii];
|
||
if (only_b7c && !e->bit7_clear) continue;
|
||
if (!first) send_fmt(",");
|
||
first = 0;
|
||
send_fmt("{\"old\":\"0x%03X\",\"new\":\"0x%03X\","
|
||
"\"func\":\"0x%08X\",\"pc\":\"0x%08X\",\"w\":%d,"
|
||
"\"b7s\":%d,\"b7c\":%d,\"exc\":%d}",
|
||
e->old_mask, e->new_mask,
|
||
(unsigned)e->caller, (unsigned)e->store_pc, 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\","
|
||
"\"slot0\":%d,\"slot1\":%d}",
|
||
(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,
|
||
e->slot0_state, e->slot1_state);
|
||
}
|
||
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
static void handle_sio_trace_window(int id, const char *json)
|
||
{
|
||
int seq = json_get_int(json, "seq", -1);
|
||
int before = json_get_int(json, "before", 8);
|
||
int after = json_get_int(json, "after", 16);
|
||
if (seq < 0) { send_err(id, "missing seq"); return; }
|
||
if (before < 0) before = 0;
|
||
if (after < 0) after = 0;
|
||
|
||
const SioTraceEntry *buf;
|
||
int write_idx;
|
||
uint32_t total_seq = sio_get_trace(&buf, &write_idx);
|
||
(void)write_idx;
|
||
uint32_t oldest = (total_seq > (uint32_t)SIO_TRACE_CAP)
|
||
? total_seq - (uint32_t)SIO_TRACE_CAP : 0;
|
||
uint32_t lo = (uint32_t)((seq > before) ? (seq - before) : 0);
|
||
uint32_t hi = (uint32_t)(seq + after);
|
||
if (lo < oldest) lo = oldest;
|
||
if (hi >= total_seq) hi = total_seq ? total_seq - 1 : 0;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"oldest\":%u,"
|
||
"\"seq\":%d,\"entries\":[",
|
||
id, (unsigned)total_seq, (unsigned)oldest, seq);
|
||
|
||
int emitted = 0;
|
||
if (total_seq > 0 && lo <= hi) {
|
||
for (uint32_t s = lo; s <= hi; s++) {
|
||
const SioTraceEntry *e = &buf[s % SIO_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
if (emitted > 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\","
|
||
"\"slot0\":%d,\"slot1\":%d}",
|
||
(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,
|
||
e->slot0_state, e->slot1_state);
|
||
emitted++;
|
||
}
|
||
}
|
||
send_fmt("],\"emitted\":%d}\n", emitted);
|
||
}
|
||
|
||
/* ---- 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_sio_irq_window(int id, const char *json)
|
||
{
|
||
int byte_seq = json_get_int(json, "byte_seq", -1);
|
||
int before = json_get_int(json, "before", 8);
|
||
int after = json_get_int(json, "after", 16);
|
||
int src_filter = json_get_int(json, "src", -1);
|
||
if (byte_seq < 0) { send_err(id, "missing byte_seq"); return; }
|
||
if (before < 0) before = 0;
|
||
if (after < 0) after = 0;
|
||
|
||
uint32_t lo = (uint32_t)((byte_seq > before) ? (byte_seq - before) : 0);
|
||
uint32_t hi = (uint32_t)(byte_seq + after);
|
||
const SioIrqEntry *buf;
|
||
int write_idx;
|
||
uint32_t total_seq = sio_get_irq_ring(&buf, &write_idx);
|
||
(void)write_idx;
|
||
uint32_t avail = (total_seq < (uint32_t)SIO_IRQ_RING_CAP)
|
||
? total_seq : (uint32_t)SIO_IRQ_RING_CAP;
|
||
uint32_t start = total_seq - avail;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"available\":%u,"
|
||
"\"byte_seq\":%d,\"src_filter\":%d,\"entries\":[",
|
||
id, (unsigned)total_seq, (unsigned)avail, byte_seq, src_filter);
|
||
int emitted = 0;
|
||
for (uint32_t i = 0; i < avail; i++) {
|
||
const SioIrqEntry *e = &buf[(start + i) % SIO_IRQ_RING_CAP];
|
||
if (e->byte_seq < lo || e->byte_seq > hi) continue;
|
||
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_card_read_summary(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
const CardReadSummary *buf = NULL;
|
||
uint32_t n = card_read_summary_get(&buf);
|
||
|
||
char out[16 * 1024];
|
||
int o = snprintf(out, sizeof(out),
|
||
"{\"id\":%d,\"ok\":true,\"count\":%u,\"cap\":%d,\"entries\":[",
|
||
id, (unsigned)n, CARD_READ_SUMMARY_CAP);
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
const CardReadSummary *e = &buf[i];
|
||
char peek[2 * CARD_READ_SUMMARY_PEEK + 1];
|
||
int pi = 0;
|
||
for (int b = 0; b < CARD_READ_SUMMARY_PEEK; b++) {
|
||
pi += snprintf(peek + pi, sizeof(peek) - pi, "%02X", e->data_peek[b]);
|
||
}
|
||
peek[pi] = 0;
|
||
o += snprintf(out + o, sizeof(out) - o,
|
||
"%s{\"seq\":%llu,\"cyc\":%llu,"
|
||
"\"slot\":%u,\"cmd\":\"0x%02X\",\"sector\":%u,"
|
||
"\"checksum\":\"0x%02X\",\"data_idx\":%u,"
|
||
"\"current_func\":\"0x%08X\",\"last_store_pc\":\"0x%08X\","
|
||
"\"dest_ram\":\"0x%08X\",\"data_peek\":\"%s\"}",
|
||
i == 0 ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
(unsigned long long)e->psx_cycle_count,
|
||
e->slot, e->cmd, e->sector,
|
||
e->checksum_card, e->data_idx_at_end,
|
||
e->current_func, e->last_store_pc,
|
||
e->dest_ram_addr, peek);
|
||
if (o >= (int)sizeof(out) - 512) break;
|
||
}
|
||
snprintf(out + o, sizeof(out) - o, "]}");
|
||
send_fmt("%s", out);
|
||
}
|
||
|
||
static void handle_card_read_summary_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
card_read_summary_reset();
|
||
send_fmt("{\"id\":%d,\"ok\":true}\n", id);
|
||
}
|
||
|
||
static void handle_card_data_writes(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > CARD_DATA_WRITES_CAP) count = CARD_DATA_WRITES_CAP;
|
||
|
||
const CardDataWriteEntry *buf = NULL;
|
||
uint64_t total_seq = 0;
|
||
uint32_t head = 0;
|
||
uint32_t avail = card_data_writes_get(&buf, &total_seq, &head);
|
||
if ((uint32_t)count > avail) count = (int)avail;
|
||
|
||
char out[64 * 1024];
|
||
int o = snprintf(out, sizeof(out),
|
||
"{\"id\":%d,\"ok\":true,\"total_seq\":%llu,"
|
||
"\"avail\":%u,\"count\":%d,\"entries\":[",
|
||
id, (unsigned long long)total_seq, avail, count);
|
||
|
||
/* Iterate the OLDEST `count` entries first (reading from
|
||
* (head - avail) forward, then taking the last `count`). */
|
||
int start = ((int)head - count + (int)CARD_DATA_WRITES_CAP) % (int)CARD_DATA_WRITES_CAP;
|
||
for (int i = 0; i < count; i++) {
|
||
int idx = (start + i) % (int)CARD_DATA_WRITES_CAP;
|
||
const CardDataWriteEntry *e = &buf[idx];
|
||
o += snprintf(out + o, sizeof(out) - o,
|
||
"%s{\"seq\":%llu,\"cyc\":%llu,\"addr\":\"0x%08X\","
|
||
"\"value\":\"0x%08X\",\"width\":%u,"
|
||
"\"mc_state\":%u,\"mc_idx\":%u,\"slot\":%u,"
|
||
"\"store_pc\":\"0x%08X\",\"func\":\"0x%08X\"}",
|
||
i == 0 ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
(unsigned long long)e->psx_cycle_count,
|
||
e->addr, e->value, e->width,
|
||
e->mc_state_at_read, e->mc_data_idx_at_read,
|
||
e->slot, e->store_pc, e->func_addr);
|
||
if (o >= (int)sizeof(out) - 512) break;
|
||
}
|
||
snprintf(out + o, sizeof(out) - o, "]}");
|
||
send_fmt("%s", out);
|
||
}
|
||
|
||
static void handle_card_data_writes_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
card_data_writes_reset();
|
||
send_fmt("{\"id\":%d,\"ok\":true}\n", id);
|
||
}
|
||
|
||
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 the current pad word and any active input override. */
|
||
extern uint16_t sio_get_pad_buttons(void);
|
||
static void handle_pad_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint16_t pad = sio_get_pad_buttons();
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"pad\":\"0x%04X\","
|
||
"\"override\":%d,\"override_frames\":%d}\n",
|
||
id, pad, s_input_override, s_input_frames);
|
||
}
|
||
|
||
static void handle_clear_input(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_input_override = -1;
|
||
s_input_frames = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_turbo(int id, const char *json)
|
||
{
|
||
int enabled = json_get_int(json, "enabled", -1);
|
||
if (enabled < 0) {
|
||
enabled = json_get_int(json, "on", -1);
|
||
}
|
||
if (enabled < 0) {
|
||
send_err(id, "missing enabled");
|
||
return;
|
||
}
|
||
s_turbo_enabled = enabled ? 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d}", id, s_turbo_enabled);
|
||
}
|
||
|
||
static void handle_turbo_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d}", id, s_turbo_enabled);
|
||
}
|
||
|
||
/* pause / continue / step / run_to_frame: REMOVED.
|
||
*
|
||
* Per CLAUDE.md global rule #2 ("Never time/attach for observability —
|
||
* always consume ring buffers"), pause/step is the wrong primitive for
|
||
* observation. It produces synthesized snapshots ("what's state right
|
||
* NOW") rather than reading the system's own continuously-recorded
|
||
* history. Worse, it tempts the observer to think pause-step-read is
|
||
* cheap; in this codebase it forced the runtime into a wait loop where
|
||
* a dropped client connection looked like a freeze.
|
||
*
|
||
* Replacements (already in the runtime):
|
||
* - fn_entry_dump / fn_entry_tail for what code ran
|
||
* - wtrace_dump for what memory was written
|
||
* - gpu_frame_dump frame=N for what GP0 commands were issued
|
||
* - mdec_trace for MDEC events
|
||
* - sio_trace / sio_pc_trace for SIO history
|
||
* - frame_range / get_frame for per-frame state snapshots
|
||
*
|
||
* Handlers below return an error explaining the migration. The state
|
||
* variables (s_paused, s_step_count, s_run_to) are kept as zero so
|
||
* freeze_check still reports them and any stale callsite that reads
|
||
* them gets a benign value. */
|
||
static void handle_pause(int id, const char *json) {
|
||
(void)json;
|
||
send_err(id, "pause is removed; query a ring buffer (fn_entry_tail, wtrace_dump, gpu_frame_dump, etc.) instead of synthesizing a snapshot");
|
||
}
|
||
|
||
static void handle_continue(int id, const char *json) {
|
||
(void)json;
|
||
send_err(id, "continue is removed (pause is removed; nothing to resume)");
|
||
}
|
||
|
||
static void handle_step(int id, const char *json) {
|
||
(void)json;
|
||
send_err(id, "step is removed; query a ring buffer over the window of interest instead of advancing N frames synchronously");
|
||
}
|
||
|
||
static void handle_run_to_frame(int id, const char *json) {
|
||
(void)json;
|
||
send_err(id, "run_to_frame is removed; use frame_range / read_frame_ram against the live frame ring buffer instead");
|
||
}
|
||
|
||
static void handle_dirty_break_range(int id, const char *json)
|
||
{
|
||
char buf[32];
|
||
if (!json_get_str(json, "lo", buf, sizeof(buf))) {
|
||
send_err(id, "missing lo");
|
||
return;
|
||
}
|
||
uint32_t lo = hex_to_u32(buf);
|
||
if (!json_get_str(json, "hi", buf, sizeof(buf))) {
|
||
send_err(id, "missing hi");
|
||
return;
|
||
}
|
||
uint32_t hi = hex_to_u32(buf);
|
||
if (hi <= lo) {
|
||
send_err(id, "invalid range");
|
||
return;
|
||
}
|
||
|
||
s_dirty_break_lo = lo;
|
||
s_dirty_break_hi = hi;
|
||
s_dirty_break_target = 0;
|
||
s_dirty_break_ra = 0;
|
||
s_dirty_break_a0 = 0;
|
||
s_dirty_break_a1 = 0;
|
||
s_dirty_break_a2 = 0;
|
||
s_dirty_break_a3 = 0;
|
||
s_dirty_break_sp = 0;
|
||
s_dirty_break_frame = 0;
|
||
s_dirty_break_active = 1;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"active\":true,"
|
||
"\"lo\":\"0x%08X\",\"hi\":\"0x%08X\"}",
|
||
id, s_dirty_break_lo, s_dirty_break_hi);
|
||
}
|
||
|
||
static void handle_dirty_break_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_dirty_break_active = 0;
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_dirty_break_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"active\":%s,"
|
||
"\"lo\":\"0x%08X\",\"hi\":\"0x%08X\",\"hits\":%llu,"
|
||
"\"target\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"sp\":\"0x%08X\",\"frame\":%u,\"paused\":%s}",
|
||
id, s_dirty_break_active ? "true" : "false",
|
||
s_dirty_break_lo, s_dirty_break_hi,
|
||
(unsigned long long)s_dirty_break_hits,
|
||
s_dirty_break_target, s_dirty_break_ra,
|
||
s_dirty_break_a0, s_dirty_break_a1,
|
||
s_dirty_break_a2, s_dirty_break_a3,
|
||
s_dirty_break_sp, s_dirty_break_frame,
|
||
"false" /* paused field kept for protocol stability; pause was removed */);
|
||
}
|
||
|
||
/* ---- 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]);
|
||
}
|
||
|
||
/* ---- Minimal self-contained PNG writer ------------------------------------
|
||
* Emits a valid 8-bit truecolor (RGB) PNG with zero external dependencies: the
|
||
* IDAT payload is a zlib stream whose DEFLATE body is "stored" (uncompressed)
|
||
* blocks. Bigger on disk than a compressed PNG, but a real PNG that every
|
||
* viewer (and the harness Read tool) accepts, and nothing new to link against
|
||
* — which keeps the self-contained static runtime self-contained. */
|
||
static uint32_t s_png_crc_tbl[256];
|
||
static int s_png_crc_init = 0;
|
||
static void png_crc_build(void) {
|
||
for (uint32_t n = 0; n < 256; n++) {
|
||
uint32_t c = n;
|
||
for (int k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
||
s_png_crc_tbl[n] = c;
|
||
}
|
||
s_png_crc_init = 1;
|
||
}
|
||
static uint32_t png_crc_update(uint32_t crc, const uint8_t *p, size_t n) {
|
||
if (!s_png_crc_init) png_crc_build();
|
||
for (size_t i = 0; i < n; i++) crc = s_png_crc_tbl[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
|
||
return crc;
|
||
}
|
||
static uint32_t png_adler32(const uint8_t *p, size_t n) {
|
||
uint32_t a = 1, b = 0;
|
||
/* process in blocks so the 16-bit sums never overflow before the mod */
|
||
while (n) {
|
||
size_t k = n < 5552 ? n : 5552;
|
||
for (size_t i = 0; i < k; i++) { a += p[i]; b += a; }
|
||
a %= 65521; b %= 65521; p += k; n -= k;
|
||
}
|
||
return (b << 16) | a;
|
||
}
|
||
static void png_put_be32(FILE *f, uint32_t v) {
|
||
uint8_t b[4] = { (uint8_t)(v >> 24), (uint8_t)(v >> 16),
|
||
(uint8_t)(v >> 8), (uint8_t)v };
|
||
fwrite(b, 1, 4, f);
|
||
}
|
||
static void png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len) {
|
||
png_put_be32(f, (uint32_t)len);
|
||
uint32_t crc = 0xFFFFFFFFu;
|
||
crc = png_crc_update(crc, (const uint8_t *)type, 4);
|
||
crc = png_crc_update(crc, data, len);
|
||
fwrite(type, 1, 4, f);
|
||
if (len) fwrite(data, 1, len, f);
|
||
png_put_be32(f, crc ^ 0xFFFFFFFFu);
|
||
}
|
||
/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */
|
||
static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||
static const uint8_t sig[8] = { 137,80,78,71,13,10,26,10 };
|
||
fwrite(sig, 1, 8, f);
|
||
|
||
uint8_t ihdr[13];
|
||
ihdr[0]=(uint8_t)(w>>24); ihdr[1]=(uint8_t)(w>>16); ihdr[2]=(uint8_t)(w>>8); ihdr[3]=(uint8_t)w;
|
||
ihdr[4]=(uint8_t)(h>>24); ihdr[5]=(uint8_t)(h>>16); ihdr[6]=(uint8_t)(h>>8); ihdr[7]=(uint8_t)h;
|
||
ihdr[8]=8; /* bit depth */
|
||
ihdr[9]=2; /* color type 2 = truecolor RGB */
|
||
ihdr[10]=0; /* compression */
|
||
ihdr[11]=0; /* filter */
|
||
ihdr[12]=0; /* interlace */
|
||
png_chunk(f, "IHDR", ihdr, sizeof ihdr);
|
||
|
||
/* Filtered raw scanlines: each row prefixed with filter byte 0 (None). */
|
||
size_t raw_len = (size_t)h * (1 + (size_t)w * 3);
|
||
uint8_t *raw = (uint8_t *)malloc(raw_len);
|
||
if (!raw) return 0;
|
||
for (uint32_t y = 0; y < h; y++) {
|
||
uint8_t *row = raw + (size_t)y * (1 + (size_t)w * 3);
|
||
row[0] = 0;
|
||
memcpy(row + 1, rgb + (size_t)y * w * 3, (size_t)w * 3);
|
||
}
|
||
|
||
/* zlib stream: 2-byte header + stored DEFLATE blocks + 4-byte Adler32. */
|
||
size_t nblocks = (raw_len + 65534) / 65535; if (nblocks == 0) nblocks = 1;
|
||
size_t z_len = 2 + nblocks * 5 + raw_len + 4;
|
||
uint8_t *z = (uint8_t *)malloc(z_len);
|
||
if (!z) { free(raw); return 0; }
|
||
size_t zi = 0;
|
||
z[zi++] = 0x78; /* CMF: 32K window, deflate */
|
||
z[zi++] = 0x01; /* FLG: check bits make 0x7801 a multiple of 31 */
|
||
size_t off = 0;
|
||
while (off < raw_len) {
|
||
size_t n = raw_len - off; if (n > 65535) n = 65535;
|
||
int final = (off + n >= raw_len);
|
||
z[zi++] = (uint8_t)(final ? 1 : 0); /* BFINAL | BTYPE=00 */
|
||
z[zi++] = (uint8_t)(n & 0xFF); z[zi++] = (uint8_t)(n >> 8);
|
||
uint16_t nl = (uint16_t)~n;
|
||
z[zi++] = (uint8_t)(nl & 0xFF); z[zi++] = (uint8_t)(nl >> 8);
|
||
memcpy(z + zi, raw + off, n); zi += n; off += n;
|
||
}
|
||
uint32_t ad = png_adler32(raw, raw_len);
|
||
z[zi++] = (uint8_t)(ad >> 24); z[zi++] = (uint8_t)(ad >> 16);
|
||
z[zi++] = (uint8_t)(ad >> 8); z[zi++] = (uint8_t)ad;
|
||
free(raw);
|
||
|
||
png_chunk(f, "IDAT", z, zi);
|
||
free(z);
|
||
png_chunk(f, "IEND", NULL, 0);
|
||
return 1;
|
||
}
|
||
|
||
/* Unified screenshot: writes an 8-bit RGB PNG of the current display to "path"
|
||
* (default psx_screenshot.png in the runtime cwd) and answers with a single
|
||
* metadata line. Registered as both "screenshot" and "screenshot_file";
|
||
* the old "screenshot" inline-hex-row variant streamed h+1 response lines
|
||
* per request, which violated the one-request/one-response protocol and
|
||
* poisoned every client connection that used it. */
|
||
static void handle_screenshot_file(int id, const char *json)
|
||
{
|
||
/* Under the OpenGL FBO-present path, CPU VRAM can be stale (the FBO holds
|
||
* the freshest frame and is presented without a readback). Sync it down so
|
||
* the capture reflects what's on screen. No-op for the software backend or
|
||
* when no GPU frame is pending. Safe here: the debug server is pumped on
|
||
* the main (GL-context) thread. */
|
||
extern void gl_renderer_sync_cpu(void);
|
||
gl_renderer_sync_cpu();
|
||
|
||
GpuDisplayInfo di;
|
||
gpu_get_display_info(&di);
|
||
if (di.disabled || di.width == 0 || di.height == 0) {
|
||
send_err(id, "display disabled"); return;
|
||
}
|
||
|
||
uint32_t w = di.width; if (w > 640) w = 640;
|
||
uint32_t h = di.height; if (h > 512) h = 512;
|
||
|
||
char path[512];
|
||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||
strncpy(path, "psx_screenshot.png", sizeof(path) - 1);
|
||
path[sizeof(path) - 1] = '\0';
|
||
FILE *f = fopen(path, "wb");
|
||
if (!f) { send_err(id, "cannot open file"); return; }
|
||
|
||
/* Gather pixels top-down, RGB. */
|
||
uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * 3);
|
||
if (!rgb) { fclose(f); send_err(id, "alloc failed"); return; }
|
||
for (uint32_t y = 0; y < h; y++) {
|
||
for (uint32_t x = 0; x < w; x++) {
|
||
uint8_t r, g, b;
|
||
gpu_display_pixel_rgb(&di, x, y, &r, &g, &b);
|
||
uint8_t *p = rgb + ((size_t)y * w + x) * 3;
|
||
p[0] = r; p[1] = g; p[2] = b;
|
||
}
|
||
}
|
||
int ok = png_write_rgb(f, rgb, w, h);
|
||
free(rgb);
|
||
fclose(f);
|
||
if (!ok) { send_err(id, "png encode failed"); return; }
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%u,\"height\":%u}",
|
||
id, path, w, h);
|
||
}
|
||
|
||
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 < 1) w = 1;
|
||
if (h < 1) h = 1;
|
||
if (w > 128) w = 128;
|
||
if (h > 128) h = 128;
|
||
size_t hex_len = (size_t)w * h * 4 + 1;
|
||
char *hex = (char *)malloc(hex_len);
|
||
if (!hex) { send_err(id, "alloc failed"); return; }
|
||
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, hex_len - 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);
|
||
free(hex);
|
||
}
|
||
|
||
/* GL-backend diagnostic: peek the GPU-side (FBO) VRAM for a rect, plus the
|
||
* coherency flags/rects — lets probes diff FBO truth against CPU truth. */
|
||
extern int gl_renderer_fbo_peek(int x, int y, int w, int h, uint16_t *out);
|
||
extern void gl_renderer_diag(int *gpu_dirty, int pending[5], int pack[5]);
|
||
|
||
static void handle_gl_fbo_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 < 1) w = 1;
|
||
if (h < 1) h = 1;
|
||
if (w > 128) w = 128;
|
||
if (h > 128) h = 128;
|
||
uint16_t px[128 * 128];
|
||
if (!gl_renderer_fbo_peek(x, y, w, h, px)) {
|
||
send_err(id, "GL pipeline inactive or rect out of range"); return;
|
||
}
|
||
int gpu_dirty = 0, pend[5] = {0}, pack[5] = {0};
|
||
gl_renderer_diag(&gpu_dirty, pend, pack);
|
||
size_t hex_len = (size_t)w * h * 4 + 1;
|
||
char *hex = (char *)malloc(hex_len);
|
||
if (!hex) { send_err(id, "alloc failed"); return; }
|
||
int pos = 0;
|
||
for (int i = 0; i < w * h; i++)
|
||
pos += snprintf(hex + pos, hex_len - pos, "%04x", px[i]);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d,"
|
||
"\"gpu_dirty\":%d,"
|
||
"\"pending\":[%d,%d,%d,%d,%d],\"pack\":[%d,%d,%d,%d,%d],"
|
||
"\"hex\":\"%s\"}",
|
||
id, x, y, w, h, gpu_dirty,
|
||
pend[0], pend[1], pend[2], pend[3], pend[4],
|
||
pack[0], pack[1], pack[2], pack[3], pack[4], hex);
|
||
free(hex);
|
||
}
|
||
|
||
extern int gl_renderer_vram_diff(uint32_t *count, int bbox[4],
|
||
int samples[8][2], uint16_t samples_px[8][2]);
|
||
|
||
static void handle_gl_vram_diff(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint32_t n = 0;
|
||
int bbox[4] = {0}, samples[8][2] = {{0}};
|
||
uint16_t spx[8][2] = {{0}};
|
||
int r = gl_renderer_vram_diff(&n, bbox, samples, spx);
|
||
if (!r) { send_err(id, "GL pipeline inactive"); return; }
|
||
int ns = r - 1;
|
||
int gpu_dirty = 0, pend[5] = {0}, pack[5] = {0};
|
||
gl_renderer_diag(&gpu_dirty, pend, pack);
|
||
char smp[512]; int pos = 0; smp[0] = 0;
|
||
for (int i = 0; i < ns; i++)
|
||
pos += snprintf(smp + pos, sizeof(smp) - pos,
|
||
"%s[%d,%d,\"0x%04X\",\"0x%04X\"]", i ? "," : "",
|
||
samples[i][0], samples[i][1], spx[i][0], spx[i][1]);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"mismatches\":%u,"
|
||
"\"bbox\":[%d,%d,%d,%d],\"gpu_dirty\":%d,"
|
||
"\"samples_xy_fbo_cpu\":[%s]}",
|
||
id, n, bbox[0], bbox[1], bbox[2], bbox[3], gpu_dirty, smp);
|
||
}
|
||
|
||
/* GL-backend coherency event ring: dump the last n events (default 200),
|
||
* optionally only events from frame >= frame_min. Always-on capture; this
|
||
* just reads a window. */
|
||
static const char *gl_coh_kind_name(int k)
|
||
{
|
||
switch (k) {
|
||
case GL_COH_FLUSH: return "flush";
|
||
case GL_COH_FILL: return "fill";
|
||
case GL_COH_COPY_SRC: return "copy_src";
|
||
case GL_COH_COPY: return "copy";
|
||
case GL_COH_DRAW: return "draw";
|
||
case GL_COH_PACK: return "pack";
|
||
case GL_COH_ENSURE: return "ensure";
|
||
case GL_COH_PRESENT: return "present";
|
||
case GL_COH_UPLOAD: return "upload";
|
||
case GL_COH_PEEK: return "peek";
|
||
case GL_COH_DIFF: return "diff";
|
||
default: return "?";
|
||
}
|
||
}
|
||
|
||
static void handle_gl_coh_ring(int id, const char *json)
|
||
{
|
||
int n = json_get_int(json, "n", 200);
|
||
long frame_min = json_get_int(json, "frame_min", -1);
|
||
if (n < 1) n = 1;
|
||
if (n > 8192) n = 8192;
|
||
uint64_t total = gl_renderer_coh_total();
|
||
uint64_t start = total > (uint64_t)n ? total - (uint64_t)n : 0;
|
||
int bufsz = 64 + n * 64;
|
||
char *buf = (char *)malloc((size_t)bufsz);
|
||
if (!buf) { send_err(id, "alloc failed"); return; }
|
||
int pos = snprintf(buf, bufsz,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"events\":[",
|
||
id, (unsigned long long)total);
|
||
int first = 1;
|
||
for (uint64_t s = start; s < total && pos < bufsz - 128; s++) {
|
||
GlCohEvent e;
|
||
if (!gl_renderer_coh_get(s, &e)) continue;
|
||
if (frame_min >= 0 && e.frame < (uint32_t)frame_min) continue;
|
||
pos += snprintf(buf + pos, bufsz - pos,
|
||
"%s[%llu,%u,\"%s\",%d,%d,%d,%d]",
|
||
first ? "" : ",", (unsigned long long)s, e.frame,
|
||
gl_coh_kind_name(e.kind), e.x0, e.y0, e.x1, e.y1);
|
||
first = 0;
|
||
}
|
||
pos += snprintf(buf + pos, bufsz - pos, "]}");
|
||
send_fmt("%s", buf);
|
||
free(buf);
|
||
}
|
||
|
||
/* ---- Write trace: hook + handlers (Tier 1 reverse debugger) ---- */
|
||
extern CPUState *debug_cpu_ptr;
|
||
|
||
static void wtrace_fill_entry(WriteTraceEntry *e, uint64_t seq,
|
||
uint32_t phys, uint32_t old_val,
|
||
uint32_t new_val, uint8_t width)
|
||
{
|
||
uint32_t ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
e->seq = 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->pc = g_debug_last_store_pc;
|
||
e->cpu_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0;
|
||
e->sp = debug_cpu_ptr ? debug_cpu_ptr->gpr[29] : 0;
|
||
e->v0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[2] : 0;
|
||
e->v1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[3] : 0;
|
||
e->a0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[4] : 0;
|
||
e->a1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[5] : 0;
|
||
e->a2 = debug_cpu_ptr ? debug_cpu_ptr->gpr[6] : 0;
|
||
e->a3 = debug_cpu_ptr ? debug_cpu_ptr->gpr[7] : 0;
|
||
e->t0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[8] : 0;
|
||
e->t1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[9] : 0;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
}
|
||
|
||
/* 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;
|
||
WriteTraceEntry *e = &s_wtrace[s_wtrace_head];
|
||
wtrace_fill_entry(e, s_wtrace_seq++, phys, old_val, new_val, width);
|
||
s_wtrace_head = (s_wtrace_head + 1) % WRITE_TRACE_CAP;
|
||
}
|
||
|
||
static void wtrace_boot_record(uint32_t phys, uint32_t old_val,
|
||
uint32_t new_val, uint8_t width)
|
||
{
|
||
if (!s_wtrace_boot || s_wtrace_boot_range_count == 0) return;
|
||
int match = 0;
|
||
for (int i = 0; i < s_wtrace_boot_range_count; i++) {
|
||
if (phys >= s_wtrace_boot_ranges[i].lo &&
|
||
phys < s_wtrace_boot_ranges[i].hi) {
|
||
match = 1;
|
||
break;
|
||
}
|
||
}
|
||
if (!match) return;
|
||
|
||
uint64_t seq = s_wtrace_boot_total++;
|
||
if (s_wtrace_boot_count >= WRITE_TRACE_BOOT_CAP) return;
|
||
WriteTraceEntry *e = &s_wtrace_boot[s_wtrace_boot_count++];
|
||
wtrace_fill_entry(e, seq, phys, old_val, new_val, width);
|
||
}
|
||
|
||
/* Always-on catch-all recorder. Lean record (no register window). */
|
||
static void wtrace_all_record(uint32_t phys, uint32_t new_val, uint8_t width)
|
||
{
|
||
if (!s_wtrace_all) return;
|
||
WriteTraceAllEntry *e = &s_wtrace_all[s_wtrace_all_head];
|
||
e->seq = s_wtrace_all_seq++;
|
||
e->addr = phys;
|
||
e->new_val = new_val;
|
||
e->pc = g_debug_last_store_pc;
|
||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->w = width;
|
||
s_wtrace_all_head = (s_wtrace_all_head + 1) % WRITE_TRACE_ALL_CAP;
|
||
}
|
||
|
||
static void wtrace_transition_record(uint32_t phys, uint32_t old_val,
|
||
uint32_t new_val, uint8_t width)
|
||
{
|
||
if (!s_wtrace_trans || s_wtrace_trans_range_count == 0) return;
|
||
if (old_val == new_val) return;
|
||
|
||
int match = 0;
|
||
for (int i = 0; i < s_wtrace_trans_range_count; i++) {
|
||
if (phys >= s_wtrace_trans_ranges[i].lo &&
|
||
phys < s_wtrace_trans_ranges[i].hi) {
|
||
match = 1;
|
||
break;
|
||
}
|
||
}
|
||
if (!match) return;
|
||
|
||
WriteTraceTransEntry *e = &s_wtrace_trans[s_wtrace_trans_head];
|
||
e->seq = s_wtrace_trans_seq++;
|
||
e->addr = phys;
|
||
e->old_val = old_val;
|
||
e->new_val = new_val;
|
||
e->pc = g_debug_last_store_pc;
|
||
e->func_addr = g_debug_current_func_addr;
|
||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
e->sp = debug_cpu_ptr ? debug_cpu_ptr->gpr[29] : 0;
|
||
e->s0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[16] : 0;
|
||
e->s1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[17] : 0;
|
||
e->s2 = debug_cpu_ptr ? debug_cpu_ptr->gpr[18] : 0;
|
||
e->s3 = debug_cpu_ptr ? debug_cpu_ptr->gpr[19] : 0;
|
||
e->stk20 = (debug_cpu_ptr && e->sp) ? trace_read_word(debug_cpu_ptr, e->sp + 20u) : 0;
|
||
e->stk40 = (debug_cpu_ptr && e->sp) ? trace_read_word(debug_cpu_ptr, e->sp + 40u) : 0;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
s_wtrace_trans_head = (s_wtrace_trans_head + 1) % WRITE_TRACE_TRANS_CAP;
|
||
}
|
||
|
||
/* Compat no-op: ape-flavored generated code emits debug_server_log_call_entry_cpu
|
||
* at JAL sites (call-entry logging). Not needed for the card-driver comparison;
|
||
* stub it so the good baseline links + runs. */
|
||
void debug_server_log_call_entry_cpu(uint32_t func_addr, CPUState *cpu) {
|
||
(void)func_addr; (void)cpu;
|
||
}
|
||
|
||
/* Dedicated sparse card-driver-state ring (ported from ape-fw for good-vs-bad
|
||
* comparison): card state table 0x9F20, result flags 0xB9D0, byte counter 0x72F0,
|
||
* chain success 0x7520, chain ptrs 0x7528, and the EvCB card-event entries
|
||
* 0xE044-0xE0D0 (status/spec/mode). Sparse => no eviction over a session. */
|
||
#define CARD_TRACE_CAP (1u << 16)
|
||
typedef struct {
|
||
uint64_t seq; uint32_t phys; uint32_t old_val; uint32_t new_val;
|
||
uint32_t pc; uint32_t cpu_pc; uint32_t ra; uint32_t func; uint32_t frame;
|
||
uint8_t width; uint8_t in_exception;
|
||
} CardTraceEntry;
|
||
static CardTraceEntry s_card_trace[CARD_TRACE_CAP];
|
||
static uint64_t s_card_trace_seq = 0;
|
||
static inline int is_card_critical_addr(uint32_t phys) {
|
||
return (phys >= 0x00009F20u && phys < 0x00009F40u) ||
|
||
(phys >= 0x0000B9D0u && phys < 0x0000B9F0u) ||
|
||
(phys >= 0x000072F0u && phys < 0x000072F4u) ||
|
||
(phys >= 0x00007520u && phys < 0x00007524u) ||
|
||
(phys >= 0x00007528u && phys < 0x00007530u) ||
|
||
(phys >= 0x0000E044u && phys < 0x0000E0D0u);
|
||
}
|
||
static void card_trace_record(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width) {
|
||
uint64_t seq = s_card_trace_seq++;
|
||
CardTraceEntry *e = &s_card_trace[seq & (CARD_TRACE_CAP - 1u)];
|
||
e->seq = seq; e->phys = phys; e->old_val = old_val; e->new_val = new_val;
|
||
e->pc = g_debug_last_store_pc;
|
||
e->cpu_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0;
|
||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
e->func = g_debug_current_func_addr;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
e->in_exception = (uint8_t)(psx_get_in_exception() ? 1u : 0u);
|
||
}
|
||
|
||
static void handle_card_trace_dump(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 200);
|
||
if (count > (int)CARD_TRACE_CAP) count = CARD_TRACE_CAP;
|
||
char alo[32] = {0}, ahi[32] = {0};
|
||
uint32_t flo = 0, fhi = 0xFFFFFFFFu; int filt = 0;
|
||
if (json_get_str(json, "addr_lo", alo, sizeof alo)) { flo = (uint32_t)strtoul(alo, NULL, 0); filt = 1; }
|
||
if (json_get_str(json, "addr_hi", ahi, sizeof ahi)) { fhi = (uint32_t)strtoul(ahi, NULL, 0); filt = 1; }
|
||
uint64_t total = s_card_trace_seq;
|
||
uint64_t avail = (total < CARD_TRACE_CAP) ? total : CARD_TRACE_CAP;
|
||
const size_t BUF_SZ = 2 * 1024 * 1024;
|
||
char *out = (char *)malloc(BUF_SZ);
|
||
if (!out) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%d,\"filtered\":%d,\"entries\":[",
|
||
id, (unsigned long long)total, count, filt);
|
||
uint64_t start = filt ? (total - avail) : (total - ((uint64_t)count < avail ? (uint64_t)count : avail));
|
||
int emitted = 0; int first = 1;
|
||
for (uint64_t s = start; s < total && emitted < count; s++) {
|
||
CardTraceEntry *e = &s_card_trace[s & (CARD_TRACE_CAP - 1u)];
|
||
if (filt && (e->phys < flo || e->phys >= fhi)) continue;
|
||
if (pos > BUF_SZ - 512) break;
|
||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"phys\":\"0x%05X\",\"old\":\"0x%08X\",\"new\":\"0x%08X\","
|
||
"\"w\":%u,\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"in_exc\":%u,\"frame\":%u}",
|
||
first ? "" : ",", (unsigned long long)e->seq, e->phys,
|
||
e->old_val, e->new_val, e->width, e->pc, e->cpu_pc, e->ra,
|
||
e->func, e->in_exception, e->frame);
|
||
first = 0; emitted++;
|
||
}
|
||
pos += snprintf(out + pos, BUF_SZ - pos, "]}\n");
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
|
||
/* Multi-range check called from memory.c write paths.
|
||
* Iterates up to 8 ranges; records if any match.
|
||
* The always-on catch-all ring is recorded UNCONDITIONALLY first so
|
||
* late-connecting probes can still see recent writes without arming. */
|
||
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||
uint32_t new_val, uint8_t width)
|
||
{
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)phys; (void)old_val; (void)new_val; (void)width;
|
||
return;
|
||
#endif
|
||
if (is_card_critical_addr(phys)) card_trace_record(phys, old_val, new_val, width);
|
||
wtrace_all_record(phys, new_val, width);
|
||
wtrace_transition_record(phys, old_val, new_val, width);
|
||
wtrace_boot_record(phys, old_val, new_val, 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)
|
||
{
|
||
#ifdef PSX_NO_DEBUG_TOOLS
|
||
(void)addr; (void)val; (void)width;
|
||
return;
|
||
#endif
|
||
if (!s_mmio_trace) return;
|
||
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->pc = g_debug_last_store_pc;
|
||
e->cpu_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0;
|
||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||
e->sp = debug_cpu_ptr ? debug_cpu_ptr->gpr[29] : 0;
|
||
e->a0 = debug_cpu_ptr ? debug_cpu_ptr->gpr[4] : 0;
|
||
e->a1 = debug_cpu_ptr ? debug_cpu_ptr->gpr[5] : 0;
|
||
e->a2 = debug_cpu_ptr ? debug_cpu_ptr->gpr[6] : 0;
|
||
e->a3 = debug_cpu_ptr ? debug_cpu_ptr->gpr[7] : 0;
|
||
e->sr = debug_cpu_ptr ? debug_cpu_ptr->cop0[12] : 0;
|
||
e->epc = debug_cpu_ptr ? debug_cpu_ptr->cop0[14] : 0;
|
||
e->istat = i_stat;
|
||
e->imask = i_mask;
|
||
e->frame = (uint32_t)s_frame_count;
|
||
e->width = width;
|
||
s_mmio_trace_head = (s_mmio_trace_head + 1) % MMIO_TRACE_CAP;
|
||
|
||
/* Mirror GP1 (0x1F801814) writes into the dedicated long-retention
|
||
* display-control ring. */
|
||
if (addr == 0x1F801814u && s_gp1_trace) {
|
||
MmioTraceEntry *g = &s_gp1_trace[s_gp1_trace_head];
|
||
*g = *e;
|
||
g->seq = s_gp1_trace_seq++;
|
||
s_gp1_trace_head = (s_gp1_trace_head + 1) % GP1_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);
|
||
}
|
||
|
||
/* Normalized verb: disarm every range in one shot. Parity with
|
||
* psx-beetle's wtrace_disarm_all. */
|
||
static void handle_wtrace_disarm_all(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_wtrace_range_count = 0;
|
||
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);
|
||
}
|
||
|
||
/* Liveness/freeze diagnostic: returns a single snapshot of every counter
|
||
* that distinguishes "stuck in a tight handler loop" from "just slow" or
|
||
* "starved on TCP poll". Pass {"window":N} (default 256) to also include
|
||
* a top-N histogram of function entries within the last `window` slots
|
||
* of the fn_entry ring, and a sample of the last `window` SIO IRQ ring
|
||
* entries (delay_applied + i_stat_after).
|
||
*
|
||
* Discriminators:
|
||
* - exc_entries delta between two snapshots → handler dispatch rate
|
||
* - exc_reentry_blocks > 0 → check_interrupts called
|
||
* from inside the handler
|
||
* - dirty_ram blocks/insns delta → install-stub interp loop
|
||
* - fn_entry histogram dominance → stuck-in-N-functions loop
|
||
* - dispatch_count change between calls → recompiled code progressing
|
||
* - sio.irq_pending + countdown stuck → IRQ pacing breakdown */
|
||
static void handle_freeze_check(int id, const char *json)
|
||
{
|
||
int window = json_get_int(json, "window", 256);
|
||
if (window < 1) window = 1;
|
||
if (window > 65536) window = 65536;
|
||
|
||
extern int sio_get_mc_max_state(void);
|
||
extern int sio_get_mc_abort_count(void);
|
||
extern int sio_get_mc_read_done(void);
|
||
extern int sio_get_tx_writes(void);
|
||
|
||
uint64_t total_checks = 0;
|
||
uint32_t dispatch_count = 0;
|
||
int in_exc = 0;
|
||
int post_cool = 0;
|
||
uint64_t exc_entries = 0;
|
||
uint64_t exc_reentry = 0;
|
||
psx_get_freeze_diag(&total_checks, &dispatch_count, &in_exc,
|
||
&post_cool, &exc_entries, &exc_reentry);
|
||
|
||
int sio_irq_pending = 0;
|
||
int sio_irq_countdown = 0;
|
||
uint16_t sio_stat = 0;
|
||
uint16_t sio_ctrl = 0;
|
||
int card_active = 0;
|
||
sio_get_freeze_diag(&sio_irq_pending, &sio_irq_countdown,
|
||
&sio_stat, &sio_ctrl, &card_active);
|
||
|
||
extern uint64_t g_dirty_ram_blocks_run;
|
||
extern uint64_t g_dirty_ram_insns_run;
|
||
extern uint64_t g_dirty_ram_aborts;
|
||
|
||
/* Top-K fn_entry histogram over the last `window` slots. */
|
||
typedef struct { uint32_t func; uint32_t count; } HistBucket;
|
||
enum { HIST_CAP = 16 };
|
||
HistBucket hist[HIST_CAP];
|
||
int hist_n = 0;
|
||
uint32_t recent_total = 0;
|
||
uint32_t recent_min_func = 0xFFFFFFFFu, recent_max_func = 0;
|
||
if (s_fn_entry && s_fn_entry_seq > 0) {
|
||
uint64_t end_seq = s_fn_entry_seq;
|
||
uint64_t start_seq = (end_seq > (uint64_t)window) ? end_seq - window : 0;
|
||
for (uint64_t i = start_seq; i < end_seq; i++) {
|
||
FnEntryEntry *e = &s_fn_entry[i % FN_TRACE_CAP];
|
||
uint32_t f = e->func_addr;
|
||
if (f < recent_min_func) recent_min_func = f;
|
||
if (f > recent_max_func) recent_max_func = f;
|
||
recent_total++;
|
||
int found = 0;
|
||
for (int k = 0; k < hist_n; k++) {
|
||
if (hist[k].func == f) { hist[k].count++; found = 1; break; }
|
||
}
|
||
if (!found) {
|
||
if (hist_n < HIST_CAP) {
|
||
hist[hist_n].func = f;
|
||
hist[hist_n].count = 1;
|
||
hist_n++;
|
||
} else {
|
||
/* Replace lowest count with this one if the lowest is 1
|
||
* (rare bucket, may be noise). Keeps the top-K mostly
|
||
* stable for repeating-function-loop diagnosis. */
|
||
int min_idx = 0;
|
||
for (int k = 1; k < hist_n; k++)
|
||
if (hist[k].count < hist[min_idx].count) min_idx = k;
|
||
if (hist[min_idx].count <= 1) {
|
||
hist[min_idx].func = f;
|
||
hist[min_idx].count = 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* Sort hist descending by count (insertion sort, n<=16). */
|
||
for (int i = 1; i < hist_n; i++) {
|
||
HistBucket cur = hist[i];
|
||
int j = i - 1;
|
||
while (j >= 0 && hist[j].count < cur.count) {
|
||
hist[j+1] = hist[j];
|
||
j--;
|
||
}
|
||
hist[j+1] = cur;
|
||
}
|
||
|
||
/* Recent SIO IRQ ring sample. */
|
||
const SioIrqEntry *irq_buf = NULL;
|
||
int irq_widx = 0;
|
||
uint32_t irq_total = sio_get_irq_ring(&irq_buf, &irq_widx);
|
||
|
||
char buf[8192];
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"{\"id\":%d,\"ok\":true,"
|
||
"\"current_func\":\"0x%08X\","
|
||
"\"last_store_pc\":\"0x%08X\","
|
||
"\"total_checks\":%llu,"
|
||
"\"dispatch_count\":%u,"
|
||
"\"in_exception\":%d,"
|
||
"\"post_exception_cooldown\":%d,"
|
||
"\"exception_entries\":%llu,"
|
||
"\"exception_reentry_blocks\":%llu,"
|
||
"\"sio_irq_pending\":%d,"
|
||
"\"sio_irq_countdown\":%d,"
|
||
"\"sio_stat\":\"0x%04X\","
|
||
"\"sio_ctrl\":\"0x%04X\","
|
||
"\"sio_card_active\":%d,"
|
||
"\"i_stat\":\"0x%08X\","
|
||
"\"i_mask\":\"0x%08X\","
|
||
"\"dirty_ram_blocks\":%llu,"
|
||
"\"dirty_ram_insns\":%llu,"
|
||
"\"dirty_ram_aborts\":%llu,"
|
||
"\"dirty_ram_guard_yields\":%llu,"
|
||
"\"fn_entry_total\":%llu,"
|
||
"\"sio_irq_total\":%u,"
|
||
"\"sio_byte_seq\":%u,"
|
||
"\"mc_max_state\":%d,"
|
||
"\"mc_aborts\":%d,"
|
||
"\"mc_read_done\":%d,"
|
||
"\"tx_writes\":%d,"
|
||
"\"psx_cycle_count\":%llu,"
|
||
"\"frame_count\":%llu,"
|
||
"\"paused\":%d,"
|
||
"\"step_count\":%d,"
|
||
"\"run_to\":%u,"
|
||
"\"client_connected\":%d,"
|
||
"\"window\":%d,"
|
||
"\"recent_func_min\":\"0x%08X\","
|
||
"\"recent_func_max\":\"0x%08X\","
|
||
"\"recent_total\":%u,"
|
||
"\"hist\":[",
|
||
id,
|
||
g_debug_current_func_addr,
|
||
g_debug_last_store_pc,
|
||
(unsigned long long)total_checks,
|
||
dispatch_count,
|
||
in_exc,
|
||
post_cool,
|
||
(unsigned long long)exc_entries,
|
||
(unsigned long long)exc_reentry,
|
||
sio_irq_pending,
|
||
sio_irq_countdown,
|
||
(unsigned)sio_stat,
|
||
(unsigned)sio_ctrl,
|
||
card_active,
|
||
i_stat, i_mask,
|
||
(unsigned long long)g_dirty_ram_blocks_run,
|
||
(unsigned long long)g_dirty_ram_insns_run,
|
||
(unsigned long long)g_dirty_ram_aborts,
|
||
(unsigned long long)g_dirty_ram_guard_yields,
|
||
(unsigned long long)s_fn_entry_seq,
|
||
irq_total,
|
||
sio_get_seq(),
|
||
sio_get_mc_max_state(),
|
||
sio_get_mc_abort_count(),
|
||
sio_get_mc_read_done(),
|
||
sio_get_tx_writes(),
|
||
(unsigned long long)psx_get_cycle_count(),
|
||
(unsigned long long)s_frame_count,
|
||
s_paused,
|
||
s_step_count,
|
||
s_run_to,
|
||
(s_client != SOCK_INVALID),
|
||
window,
|
||
(recent_total ? recent_min_func : 0),
|
||
recent_max_func,
|
||
recent_total);
|
||
for (int i = 0; i < hist_n && pos < sizeof(buf) - 64; i++) {
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos,
|
||
"%s{\"func\":\"0x%08X\",\"count\":%u}",
|
||
i ? "," : "", hist[i].func, hist[i].count);
|
||
}
|
||
pos += snprintf(buf + pos, sizeof(buf) - pos, "]}");
|
||
debug_server_send_line(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_boot_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t newest = (s_wtrace_boot_total > 0) ? s_wtrace_boot_total - 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"stored\":%u,"
|
||
"\"capacity\":%d,\"newest_seq\":%llu,\"ranges\":%d,"
|
||
"\"full\":%s}",
|
||
id, (unsigned long long)s_wtrace_boot_total,
|
||
s_wtrace_boot_count, WRITE_TRACE_BOOT_CAP,
|
||
(unsigned long long)newest, s_wtrace_boot_range_count,
|
||
(s_wtrace_boot_count >= WRITE_TRACE_BOOT_CAP) ? "true" : "false");
|
||
}
|
||
|
||
static void handle_wtrace_boot_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_wtrace_boot_total = 0;
|
||
s_wtrace_boot_count = 0;
|
||
if (s_wtrace_boot)
|
||
memset(s_wtrace_boot, 0,
|
||
(size_t)WRITE_TRACE_BOOT_CAP * sizeof(WriteTraceEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_wtrace_boot_dump(int id, const char *json)
|
||
{
|
||
if (!s_wtrace_boot) { send_err(id, "boot trace not initialized"); return; }
|
||
|
||
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;
|
||
|
||
int max_out = json_get_int(json, "count", 256);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > WRITE_TRACE_BOOT_CAP) max_out = WRITE_TRACE_BOOT_CAP;
|
||
if (max_out > 1024) max_out = 1024;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 512u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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,\"stored\":%u,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)s_wtrace_boot_total,
|
||
s_wtrace_boot_count);
|
||
for (uint32_t i = 0; i < s_wtrace_boot_count &&
|
||
emitted < MAX_OUT &&
|
||
pos < BUF_SZ - 256; i++) {
|
||
uint32_t idx = newest_first ? (s_wtrace_boot_count - 1u - i) : i;
|
||
WriteTraceEntry *e = &s_wtrace_boot[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\","
|
||
"\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"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->pc, e->cpu_pc, e->sp,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3,
|
||
e->t0, e->t1, e->frame, (unsigned)e->width);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
typedef struct {
|
||
int used;
|
||
uint32_t addr;
|
||
uint64_t writes;
|
||
uint64_t first_seq;
|
||
uint64_t last_seq;
|
||
uint32_t first_new;
|
||
uint32_t last_new;
|
||
uint32_t first_pc;
|
||
uint32_t last_pc;
|
||
uint32_t first_ra;
|
||
uint32_t last_ra;
|
||
uint32_t first_func;
|
||
uint32_t last_func;
|
||
uint32_t first_frame;
|
||
uint32_t last_frame;
|
||
uint32_t first_width;
|
||
uint32_t last_width;
|
||
uint32_t nonzero_writes;
|
||
uint32_t min_new;
|
||
uint32_t max_new;
|
||
uint32_t or_new;
|
||
uint32_t has_ff;
|
||
uint32_t has_ffff;
|
||
uint64_t first_nonzero_seq;
|
||
uint32_t first_nonzero_new;
|
||
uint32_t first_nonzero_pc;
|
||
uint32_t first_nonzero_frame;
|
||
uint32_t transition_count;
|
||
uint32_t transitions[8];
|
||
} WTraceBootSummary;
|
||
|
||
static void handle_wtrace_boot_summary(int id, const char *json)
|
||
{
|
||
if (!s_wtrace_boot) { send_err(id, "boot trace not initialized"); return; }
|
||
|
||
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;
|
||
|
||
int max_addrs = json_get_int(json, "max_addrs", 256);
|
||
if (max_addrs < 1) max_addrs = 1;
|
||
if (max_addrs > 1024) max_addrs = 1024;
|
||
|
||
WTraceBootSummary *sum =
|
||
(WTraceBootSummary *)calloc((size_t)max_addrs, sizeof(WTraceBootSummary));
|
||
if (!sum) { send_err(id, "oom"); return; }
|
||
|
||
int distinct = 0;
|
||
int overflow = 0;
|
||
for (uint32_t i = 0; i < s_wtrace_boot_count; i++) {
|
||
WriteTraceEntry *e = &s_wtrace_boot[i];
|
||
if (e->addr < filter_lo || e->addr >= filter_hi) continue;
|
||
|
||
int slot = -1;
|
||
for (int j = 0; j < distinct; j++) {
|
||
if (sum[j].used && sum[j].addr == e->addr) {
|
||
slot = j;
|
||
break;
|
||
}
|
||
}
|
||
if (slot < 0) {
|
||
if (distinct >= max_addrs) {
|
||
overflow = 1;
|
||
continue;
|
||
}
|
||
slot = distinct++;
|
||
sum[slot].used = 1;
|
||
sum[slot].addr = e->addr;
|
||
sum[slot].first_seq = e->seq;
|
||
sum[slot].first_new = e->new_val;
|
||
sum[slot].first_pc = e->pc;
|
||
sum[slot].first_ra = e->ra;
|
||
sum[slot].first_func = e->func_addr;
|
||
sum[slot].first_frame = e->frame;
|
||
sum[slot].first_width = e->width;
|
||
sum[slot].min_new = e->new_val;
|
||
sum[slot].max_new = e->new_val;
|
||
sum[slot].or_new = e->new_val;
|
||
if (e->new_val == 0xFFu) sum[slot].has_ff = 1;
|
||
if (e->new_val == 0xFFFFu) sum[slot].has_ffff = 1;
|
||
sum[slot].transitions[0] = e->new_val;
|
||
sum[slot].transition_count = 1;
|
||
} else if (sum[slot].last_new != e->new_val) {
|
||
if (sum[slot].transition_count < 8)
|
||
sum[slot].transitions[sum[slot].transition_count] = e->new_val;
|
||
sum[slot].transition_count++;
|
||
}
|
||
|
||
sum[slot].writes++;
|
||
sum[slot].last_seq = e->seq;
|
||
sum[slot].last_new = e->new_val;
|
||
sum[slot].last_pc = e->pc;
|
||
sum[slot].last_ra = e->ra;
|
||
sum[slot].last_func = e->func_addr;
|
||
sum[slot].last_frame = e->frame;
|
||
sum[slot].last_width = e->width;
|
||
if (e->new_val < sum[slot].min_new) sum[slot].min_new = e->new_val;
|
||
if (e->new_val > sum[slot].max_new) sum[slot].max_new = e->new_val;
|
||
sum[slot].or_new |= e->new_val;
|
||
if (e->new_val == 0xFFu) sum[slot].has_ff = 1;
|
||
if (e->new_val == 0xFFFFu) sum[slot].has_ffff = 1;
|
||
if (e->new_val != 0) {
|
||
if (sum[slot].nonzero_writes == 0) {
|
||
sum[slot].first_nonzero_seq = e->seq;
|
||
sum[slot].first_nonzero_new = e->new_val;
|
||
sum[slot].first_nonzero_pc = e->pc;
|
||
sum[slot].first_nonzero_frame = e->frame;
|
||
}
|
||
sum[slot].nonzero_writes++;
|
||
}
|
||
}
|
||
|
||
size_t BUF_SZ = 512u + (size_t)distinct * 768u;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { free(sum); send_err(id, "oom"); return; }
|
||
|
||
size_t pos = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"stored\":%u,"
|
||
"\"distinct\":%d,\"overflow\":%s,\"entries\":[",
|
||
id, (unsigned long long)s_wtrace_boot_total,
|
||
s_wtrace_boot_count, distinct, overflow ? "true" : "false");
|
||
for (int i = 0; i < distinct && pos < BUF_SZ - 512; i++) {
|
||
WTraceBootSummary *s = &sum[i];
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"addr\":\"0x%08X\",\"writes\":%llu,"
|
||
"\"first_seq\":%llu,\"last_seq\":%llu,"
|
||
"\"first_new\":\"0x%08X\",\"last_new\":\"0x%08X\","
|
||
"\"first_pc\":\"0x%08X\",\"last_pc\":\"0x%08X\","
|
||
"\"first_ra\":\"0x%08X\",\"last_ra\":\"0x%08X\","
|
||
"\"first_func\":\"0x%08X\",\"last_func\":\"0x%08X\","
|
||
"\"first_frame\":%u,\"last_frame\":%u,"
|
||
"\"first_w\":%u,\"last_w\":%u,"
|
||
"\"nonzero_writes\":%u,"
|
||
"\"min_new\":\"0x%08X\",\"max_new\":\"0x%08X\","
|
||
"\"or_new\":\"0x%08X\",\"has_ff\":%s,\"has_ffff\":%s",
|
||
(i == 0) ? "" : ",",
|
||
s->addr, (unsigned long long)s->writes,
|
||
(unsigned long long)s->first_seq,
|
||
(unsigned long long)s->last_seq,
|
||
s->first_new, s->last_new, s->first_pc, s->last_pc,
|
||
s->first_ra, s->last_ra, s->first_func, s->last_func,
|
||
s->first_frame, s->last_frame,
|
||
s->first_width, s->last_width, s->nonzero_writes,
|
||
s->min_new, s->max_new, s->or_new,
|
||
s->has_ff ? "true" : "false",
|
||
s->has_ffff ? "true" : "false");
|
||
if (s->nonzero_writes != 0) {
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
",\"first_nonzero_seq\":%llu,"
|
||
"\"first_nonzero_new\":\"0x%08X\","
|
||
"\"first_nonzero_pc\":\"0x%08X\","
|
||
"\"first_nonzero_frame\":%u",
|
||
(unsigned long long)s->first_nonzero_seq,
|
||
s->first_nonzero_new,
|
||
s->first_nonzero_pc,
|
||
s->first_nonzero_frame);
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, ",\"transitions\":[");
|
||
uint32_t transition_emit = s->transition_count;
|
||
if (transition_emit > 8) transition_emit = 8;
|
||
for (uint32_t t = 0; t < transition_emit && pos < BUF_SZ - 64; t++) {
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s\"0x%08X\"", (t == 0) ? "" : ",",
|
||
s->transitions[t]);
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"],\"transition_count\":%u}", s->transition_count);
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
free(sum);
|
||
}
|
||
|
||
/* ---- Always-on catch-all wtrace handlers (parity with psx-beetle) ---- */
|
||
|
||
static void handle_wtrace_all_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t total = s_wtrace_all_seq;
|
||
uint64_t oldest = (total <= WRITE_TRACE_ALL_CAP) ? 0 : total - WRITE_TRACE_ALL_CAP;
|
||
uint64_t newest = (total > 0) ? total - 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||
"\"oldest_seq\":%llu,\"newest_seq\":%llu}",
|
||
id, (unsigned long long)total, WRITE_TRACE_ALL_CAP,
|
||
(unsigned long long)oldest, (unsigned long long)newest);
|
||
}
|
||
|
||
static void handle_wtrace_all_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_wtrace_all_seq = 0;
|
||
s_wtrace_all_head = 0;
|
||
if (s_wtrace_all)
|
||
memset(s_wtrace_all, 0,
|
||
(size_t)WRITE_TRACE_ALL_CAP * sizeof(WriteTraceAllEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_wtrace_all_dump(int id, const char *json)
|
||
{
|
||
if (!s_wtrace_all) { send_err(id, "wtrace_all not initialized"); return; }
|
||
|
||
/* Optional post-hoc address filter (matches wtrace_dump shape). */
|
||
char lo_str[32], hi_str[32];
|
||
uint32_t flo = 0, fhi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "addr_lo", lo_str, sizeof(lo_str)))
|
||
flo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
|
||
fhi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||
|
||
uint64_t total = s_wtrace_all_seq;
|
||
uint32_t avail = (total < WRITE_TRACE_ALL_CAP)
|
||
? (uint32_t)total : WRITE_TRACE_ALL_CAP;
|
||
uint32_t start = (total < WRITE_TRACE_ALL_CAP) ? 0 : s_wtrace_all_head;
|
||
|
||
int max_out = json_get_int(json, "count", 256);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > 2048) max_out = 2048;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 200u;
|
||
if (BUF_SZ > (size_t)64 * 1024 * 1024) BUF_SZ = (size_t)64 * 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;
|
||
if (newest_first) {
|
||
uint32_t newest = (s_wtrace_all_head + WRITE_TRACE_ALL_CAP - 1u)
|
||
% WRITE_TRACE_ALL_CAP;
|
||
idx = (newest + WRITE_TRACE_ALL_CAP - (i % WRITE_TRACE_ALL_CAP))
|
||
% WRITE_TRACE_ALL_CAP;
|
||
} else {
|
||
idx = (start + i) % WRITE_TRACE_ALL_CAP;
|
||
}
|
||
WriteTraceAllEntry *e = &s_wtrace_all[idx];
|
||
if (e->addr < flo || e->addr >= fhi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||
"\"new\":\"0x%08X\",\"pc\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"frame\":%u,\"w\":%u}",
|
||
(emitted == 0) ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->addr, e->new_val, e->pc, e->ra,
|
||
e->frame, (unsigned)e->w);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_wtrace_trans_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t total = s_wtrace_trans_seq;
|
||
uint64_t oldest = (total <= WRITE_TRACE_TRANS_CAP) ? 0 : total - WRITE_TRACE_TRANS_CAP;
|
||
uint64_t newest = (total > 0) ? total - 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||
"\"oldest_seq\":%llu,\"newest_seq\":%llu,\"ranges\":%d}",
|
||
id, (unsigned long long)total, WRITE_TRACE_TRANS_CAP,
|
||
(unsigned long long)oldest, (unsigned long long)newest,
|
||
s_wtrace_trans_range_count);
|
||
}
|
||
|
||
static void handle_wtrace_trans_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_wtrace_trans_seq = 0;
|
||
s_wtrace_trans_head = 0;
|
||
if (s_wtrace_trans)
|
||
memset(s_wtrace_trans, 0,
|
||
(size_t)WRITE_TRACE_TRANS_CAP * sizeof(WriteTraceTransEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_wtrace_trans_dump(int id, const char *json)
|
||
{
|
||
if (!s_wtrace_trans) { send_err(id, "wtrace_trans not initialized"); return; }
|
||
|
||
char lo_str[32], hi_str[32], seq_str[32];
|
||
uint32_t flo = 0, fhi = 0xFFFFFFFFu;
|
||
if (json_get_str(json, "addr_lo", lo_str, sizeof(lo_str)))
|
||
flo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
|
||
fhi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||
|
||
uint64_t total = s_wtrace_trans_seq;
|
||
uint64_t avail = (total < WRITE_TRACE_TRANS_CAP)
|
||
? total : WRITE_TRACE_TRANS_CAP;
|
||
uint64_t oldest = total - avail;
|
||
|
||
int max_out = json_get_int(json, "count", 256);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > 2048) max_out = 2048;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
uint64_t start = oldest;
|
||
if (json_get_str(json, "seq_lo", seq_str, sizeof(seq_str))) {
|
||
start = strtoull(seq_str, NULL, 0);
|
||
if (start < oldest) start = oldest;
|
||
if (start > total) start = total;
|
||
}
|
||
|
||
const size_t BUF_SZ = 256u + (size_t)max_out * 448u;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail);
|
||
|
||
if (newest_first) {
|
||
for (uint64_t s = total; s > oldest && emitted < max_out && pos < BUF_SZ - 448; ) {
|
||
s--;
|
||
const WriteTraceTransEntry *e = &s_wtrace_trans[s % WRITE_TRACE_TRANS_CAP];
|
||
if (e->addr < flo || e->addr >= fhi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||
"\"old\":\"0x%08X\",\"new\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"stk20\":\"0x%08X\",\"stk40\":\"0x%08X\","
|
||
"\"frame\":%u,\"w\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->addr,
|
||
e->old_val, e->new_val, e->pc, e->func_addr,
|
||
e->ra, e->sp, e->s0, e->s1, e->s2, e->s3,
|
||
e->stk20, e->stk40, e->frame, (unsigned)e->width);
|
||
emitted++;
|
||
}
|
||
} else {
|
||
for (uint64_t s = start; s < total && emitted < max_out && pos < BUF_SZ - 448; s++) {
|
||
const WriteTraceTransEntry *e = &s_wtrace_trans[s % WRITE_TRACE_TRANS_CAP];
|
||
if (e->addr < flo || e->addr >= fhi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||
"\"old\":\"0x%08X\",\"new\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||
"\"s2\":\"0x%08X\",\"s3\":\"0x%08X\","
|
||
"\"stk20\":\"0x%08X\",\"stk40\":\"0x%08X\","
|
||
"\"frame\":%u,\"w\":%u}",
|
||
emitted == 0 ? "" : ",",
|
||
(unsigned long long)e->seq, e->addr,
|
||
e->old_val, e->new_val, e->pc, e->func_addr,
|
||
e->ra, e->sp, e->s0, e->s1, e->s2, e->s3,
|
||
e->stk20, e->stk40, e->frame, (unsigned)e->width);
|
||
emitted++;
|
||
}
|
||
}
|
||
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_call_focus_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
uint64_t total = s_call_focus_seq;
|
||
uint64_t oldest = (total <= CALL_FOCUS_CAP) ? 0 : total - CALL_FOCUS_CAP;
|
||
uint64_t newest = (total > 0) ? total - 1 : 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||
"\"oldest_seq\":%llu,\"newest_seq\":%llu}",
|
||
id, (unsigned long long)total, CALL_FOCUS_CAP,
|
||
(unsigned long long)oldest, (unsigned long long)newest);
|
||
}
|
||
|
||
static void handle_call_focus_reset(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
s_call_focus_seq = 0;
|
||
if (s_call_focus)
|
||
memset(s_call_focus, 0,
|
||
(size_t)CALL_FOCUS_CAP * sizeof(CallFocusEntry));
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_call_focus_dump(int id, const char *json)
|
||
{
|
||
if (!s_call_focus) { send_err(id, "call_focus not initialized"); return; }
|
||
|
||
char val[32];
|
||
uint64_t total = s_call_focus_seq;
|
||
uint64_t avail = (total < CALL_FOCUS_CAP) ? total : CALL_FOCUS_CAP;
|
||
uint64_t oldest = total - avail;
|
||
uint64_t seq_lo = oldest;
|
||
uint64_t seq_hi = total;
|
||
uint32_t func_lo = 0;
|
||
uint32_t func_hi = 0xFFFFFFFFu;
|
||
uint32_t frame_lo = 0;
|
||
uint32_t frame_hi = 0xFFFFFFFFu;
|
||
|
||
if (json_get_str(json, "seq_lo", val, sizeof(val))) {
|
||
seq_lo = strtoull(val, NULL, 0);
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_lo > total) seq_lo = total;
|
||
}
|
||
if (json_get_str(json, "seq_hi", val, sizeof(val))) {
|
||
seq_hi = strtoull(val, NULL, 0);
|
||
if (seq_hi < oldest) seq_hi = oldest;
|
||
if (seq_hi > total) seq_hi = total;
|
||
}
|
||
if (json_get_str(json, "func", val, sizeof(val))) {
|
||
func_lo = hex_to_u32(val);
|
||
func_hi = func_lo + 1u;
|
||
} else {
|
||
if (json_get_str(json, "func_lo", val, sizeof(val)))
|
||
func_lo = hex_to_u32(val);
|
||
if (json_get_str(json, "func_hi", val, sizeof(val)))
|
||
func_hi = hex_to_u32(val);
|
||
}
|
||
if (json_get_str(json, "frame_lo", val, sizeof(val)))
|
||
frame_lo = (uint32_t)strtoul(val, NULL, 0);
|
||
if (json_get_str(json, "frame_hi", val, sizeof(val)))
|
||
frame_hi = (uint32_t)strtoul(val, NULL, 0);
|
||
|
||
int max_out = json_get_int(json, "count", 128);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > 512) max_out = 512;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const size_t ENTRY_BUDGET = 1280u;
|
||
const size_t BUF_SZ = 256u + (size_t)max_out * ENTRY_BUDGET;
|
||
char *buf = (char *)malloc(BUF_SZ);
|
||
if (!buf) { send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
int emitted = 0;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%llu,"
|
||
"\"oldest\":%llu,\"seq_lo\":%llu,\"seq_hi\":%llu,"
|
||
"\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)avail,
|
||
(unsigned long long)oldest,
|
||
(unsigned long long)seq_lo, (unsigned long long)seq_hi);
|
||
|
||
#define CALL_FOCUS_EMIT_ENTRY(E) do { \
|
||
const CallFocusEntry *ce__ = (E); \
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, \
|
||
"%s{\"seq\":%llu,\"func\":\"0x%08X\",\"ra\":\"0x%08X\"," \
|
||
"\"pc\":\"0x%08X\",\"frame\":%u,\"sp\":\"0x%08X\"," \
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\"," \
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\"," \
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\"," \
|
||
"\"s0\":\"0x%08X\",\"s1\":\"0x%08X\",\"s2\":\"0x%08X\",\"s3\":\"0x%08X\"," \
|
||
"\"stk10\":\"0x%08X\",\"stk14\":\"0x%08X\",\"stk18\":\"0x%08X\"," \
|
||
"\"stk20\":\"0x%08X\",\"stk40\":\"0x%08X\"," \
|
||
"\"obj\":\"0x%08X\",\"obj10\":\"0x%08X\",\"obj14\":\"0x%08X\",\"obj18\":\"0x%08X\"," \
|
||
"\"obj30\":\"0x%08X\",\"obj30_0\":%u,\"obj30_1\":%u," \
|
||
"\"obj34\":%u,\"obj35\":%u,\"obj36\":%u,\"obj37\":%u," \
|
||
"\"obj38\":%u,\"obj3c\":\"0x%08X\",\"obj40\":\"0x%08X\",\"obj44\":%u,\"obj45\":%u," \
|
||
"\"obj46\":%u,\"obj49\":%u,\"obj4a\":%u,\"obj50\":%u," \
|
||
"\"obje0\":%u,\"obje3\":%u,\"obje4\":%u,\"obje5\":%u," \
|
||
"\"obje6\":%u,\"obje8\":%u,\"obje9\":%u,\"objea\":%u}", \
|
||
emitted == 0 ? "" : ",", \
|
||
(unsigned long long)ce__->seq, ce__->func_addr, ce__->ra, ce__->pc, \
|
||
ce__->frame, ce__->sp, ce__->v0, ce__->v1, \
|
||
ce__->a0, ce__->a1, ce__->a2, ce__->a3, ce__->t0, ce__->t1, \
|
||
ce__->s0, ce__->s1, ce__->s2, ce__->s3, \
|
||
ce__->stk10, ce__->stk14, ce__->stk18, ce__->stk20, ce__->stk40, \
|
||
ce__->obj, ce__->obj_10, ce__->obj_14, ce__->obj_18, \
|
||
ce__->obj_30, ce__->obj_30_0, ce__->obj_30_1, \
|
||
ce__->obj_34, ce__->obj_35, ce__->obj_36, ce__->obj_37, \
|
||
ce__->obj_38, ce__->obj_3c, ce__->obj_40, ce__->obj_44, ce__->obj_45, \
|
||
ce__->obj_46, ce__->obj_49, ce__->obj_4a, ce__->obj_50, \
|
||
ce__->obj_e0, ce__->obj_e3, ce__->obj_e4, ce__->obj_e5, \
|
||
ce__->obj_e6, ce__->obj_e8, ce__->obj_e9, ce__->obj_ea); \
|
||
emitted++; \
|
||
} while (0)
|
||
|
||
if (newest_first) {
|
||
for (uint64_t s = seq_hi; s > seq_lo && emitted < max_out && pos < BUF_SZ - ENTRY_BUDGET; ) {
|
||
s--;
|
||
const CallFocusEntry *e = &s_call_focus[s % CALL_FOCUS_CAP];
|
||
if (e->seq != s) continue;
|
||
if (e->func_addr < func_lo || e->func_addr >= func_hi) continue;
|
||
if (e->frame < frame_lo || e->frame >= frame_hi) continue;
|
||
CALL_FOCUS_EMIT_ENTRY(e);
|
||
}
|
||
} else {
|
||
for (uint64_t s = seq_lo; s < seq_hi && emitted < max_out && pos < BUF_SZ - ENTRY_BUDGET; s++) {
|
||
const CallFocusEntry *e = &s_call_focus[s % CALL_FOCUS_CAP];
|
||
if (e->seq != s) continue;
|
||
if (e->func_addr < func_lo || e->func_addr >= func_hi) continue;
|
||
if (e->frame < frame_lo || e->frame >= frame_hi) continue;
|
||
CALL_FOCUS_EMIT_ENTRY(e);
|
||
}
|
||
}
|
||
|
||
#undef CALL_FOCUS_EMIT_ENTRY
|
||
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
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;
|
||
|
||
int max_out = json_get_int(json, "count", 256);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > 2048) max_out = 2048;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 512u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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;
|
||
if (newest_first) {
|
||
uint32_t newest = (s_wtrace_head + WRITE_TRACE_CAP - 1u) % WRITE_TRACE_CAP;
|
||
idx = (newest + WRITE_TRACE_CAP - (i % WRITE_TRACE_CAP)) % WRITE_TRACE_CAP;
|
||
} else {
|
||
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\","
|
||
"\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"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->pc, e->cpu_pc, e->sp,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3, e->t0, e->t1,
|
||
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;
|
||
|
||
int max_out = json_get_int(json, "count", 65536);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > (int)MMIO_TRACE_CAP) max_out = (int)MMIO_TRACE_CAP;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 512u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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;
|
||
if (newest_first) {
|
||
uint32_t newest = (s_mmio_trace_head + MMIO_TRACE_CAP - 1u) % MMIO_TRACE_CAP;
|
||
idx = (newest + MMIO_TRACE_CAP - (i % MMIO_TRACE_CAP)) % MMIO_TRACE_CAP;
|
||
} else {
|
||
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\",\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||
"\"frame\":%u,\"w\":%u}",
|
||
(emitted == 0) ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->addr, e->val, e->func_addr, e->pc, e->cpu_pc,
|
||
e->ra, e->sp, e->a0, e->a1, e->a2, e->a3,
|
||
e->sr, e->epc, e->istat, e->imask,
|
||
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);
|
||
}
|
||
|
||
/* gp1_dump — dump the dedicated GP1 display-control ring. Optional
|
||
* `frame_lo`/`frame_hi` filter (applied server-side over the FULL ring,
|
||
* like wtrace_dump's address filter), optional `count` (default 4096),
|
||
* optional `newest` (1 = newest-first). */
|
||
static void handle_gp1_dump(int id, const char *json)
|
||
{
|
||
if (!s_gp1_trace) { send_err(id, "gp1 trace not initialized"); return; }
|
||
|
||
int frame_lo = json_get_int(json, "frame_lo", 0);
|
||
int frame_hi = json_get_int(json, "frame_hi", 0x7FFFFFFF);
|
||
|
||
uint64_t total = s_gp1_trace_seq;
|
||
uint32_t avail = (total < GP1_TRACE_CAP) ? (uint32_t)total : GP1_TRACE_CAP;
|
||
uint32_t start = (total < GP1_TRACE_CAP) ? 0 : s_gp1_trace_head;
|
||
|
||
int max_out = json_get_int(json, "count", 4096);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > (int)GP1_TRACE_CAP) max_out = (int)GP1_TRACE_CAP;
|
||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||
|
||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 512u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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;
|
||
if (newest_first) {
|
||
uint32_t newest = (s_gp1_trace_head + GP1_TRACE_CAP - 1u) % GP1_TRACE_CAP;
|
||
idx = (newest + GP1_TRACE_CAP - (i % GP1_TRACE_CAP)) % GP1_TRACE_CAP;
|
||
} else {
|
||
idx = (start + i) % GP1_TRACE_CAP;
|
||
}
|
||
MmioTraceEntry *e = &s_gp1_trace[idx];
|
||
if ((int)e->frame < frame_lo || (int)e->frame > frame_hi) continue;
|
||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||
"%s{\"seq\":%llu,\"val\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||
"\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"frame\":%u}",
|
||
(emitted == 0) ? "" : ",",
|
||
(unsigned long long)e->seq,
|
||
e->val, e->func_addr, e->pc, e->cpu_pc,
|
||
e->ra, e->sp, e->a0, e->a1,
|
||
e->sr, e->epc, e->frame);
|
||
emitted++;
|
||
}
|
||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static const char *mdec_event_kind_name(uint32_t kind)
|
||
{
|
||
switch (kind) {
|
||
case 1: return "reset";
|
||
case 2: return "ctrl_write";
|
||
case 3: return "cmd_begin";
|
||
case 4: return "cmd_done";
|
||
case 5: return "decode_done";
|
||
case 6: return "dma_in_start";
|
||
case 7: return "dma_in_end";
|
||
case 8: return "dma_out_start";
|
||
case 9: return "dma_out_end";
|
||
case 10: return "output_drained";
|
||
case 11: return "read_underflow";
|
||
default: return "unknown";
|
||
}
|
||
}
|
||
|
||
static void handle_mdec_state(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
MDECDebugState s;
|
||
mdec_debug_get_state(&s);
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"command\":\"0x%08X\",\"expected_halfwords\":%u,"
|
||
"\"input_count\":%u,\"output_size\":%u,\"output_pos\":%u,"
|
||
"\"output_depth\":%u,\"output_signed\":%u,\"output_bit15\":%u,"
|
||
"\"busy\":%u,\"input_full\":%u,\"enable_dma_in\":%u,\"enable_dma_out\":%u,"
|
||
"\"last_status\":\"0x%08X\","
|
||
"\"decode_macroblocks\":%u,\"decode_blocks\":%u,"
|
||
"\"decode_stop_reason\":%u,\"decode_input_pos\":%u,\"decode_input_end\":%u,"
|
||
"\"dma_in_words\":%u,\"dma_out_words\":%u,\"dma_read_underflows\":%u,"
|
||
"\"trace_total\":%llu}",
|
||
id, s.command, s.expected_halfwords, s.input_count,
|
||
s.output_size, s.output_pos, s.output_depth, s.output_signed,
|
||
s.output_bit15, s.busy, s.input_full, s.enable_dma_in,
|
||
s.enable_dma_out, s.last_status, s.decode_macroblocks,
|
||
s.decode_blocks, s.decode_stop_reason, s.decode_input_pos,
|
||
s.decode_input_end, s.dma_in_words, s.dma_out_words,
|
||
s.dma_read_underflows,
|
||
(unsigned long long)mdec_debug_get_event_total());
|
||
}
|
||
|
||
static void handle_mdec_trace(int id, const char *json)
|
||
{
|
||
uint64_t total = mdec_debug_get_event_total();
|
||
uint64_t oldest = (total > 4096ull) ? total - 4096ull : 0;
|
||
uint64_t seq_hi = total;
|
||
uint64_t seq_lo = (total > 256ull) ? total - 256ull : oldest;
|
||
char buf32[32];
|
||
if (json_get_str(json, "seq_lo", buf32, sizeof(buf32))) seq_lo = strtoull(buf32, NULL, 0);
|
||
if (json_get_str(json, "seq_hi", buf32, sizeof(buf32))) seq_hi = strtoull(buf32, NULL, 0);
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_hi > total) seq_hi = total;
|
||
|
||
int max_out = json_get_int(json, "count", 256);
|
||
if (max_out < 1) max_out = 1;
|
||
if (max_out > 4096) max_out = 4096;
|
||
|
||
MDECDebugEvent *events = (MDECDebugEvent *)malloc((size_t)max_out * sizeof(MDECDebugEvent));
|
||
if (!events) { send_err(id, "oom"); return; }
|
||
uint32_t n = mdec_debug_copy_events(seq_lo, seq_hi, events, (uint32_t)max_out);
|
||
|
||
size_t buf_sz = 256u + (size_t)n * 384u;
|
||
char *out = (char *)malloc(buf_sz);
|
||
if (!out) { free(events); send_err(id, "oom"); return; }
|
||
size_t pos = 0;
|
||
pos += snprintf(out + pos, buf_sz - pos,
|
||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"oldest\":%llu,"
|
||
"\"seq_lo\":%llu,\"seq_hi\":%llu,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)oldest,
|
||
(unsigned long long)seq_lo, (unsigned long long)seq_hi);
|
||
for (uint32_t i = 0; i < n && pos < buf_sz - 384u; i++) {
|
||
const MDECDebugEvent *e = &events[i];
|
||
pos += snprintf(out + pos, buf_sz - pos,
|
||
"%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\",\"value\":\"0x%08X\","
|
||
"\"command\":\"0x%08X\",\"input_count\":%u,\"expected_halfwords\":%u,"
|
||
"\"output_size\":%u,\"output_pos\":%u,\"macroblocks\":%u,"
|
||
"\"blocks\":%u,\"stop_reason\":%u,\"underruns\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->frame,
|
||
mdec_event_kind_name(e->kind), e->value, e->command,
|
||
e->input_count, e->expected_halfwords,
|
||
e->output_size, e->output_pos, e->macroblocks,
|
||
e->blocks, e->stop_reason, e->underruns);
|
||
}
|
||
pos += snprintf(out + pos, buf_sz - pos, "],\"emitted\":%u}", n);
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
free(events);
|
||
}
|
||
|
||
static void handle_mdec_trace_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
mdec_debug_clear();
|
||
send_ok(id);
|
||
}
|
||
|
||
static void handle_quit(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_ok(id);
|
||
psx_crash_trace_set_exit_origin("tcp_quit");
|
||
debug_server_shutdown();
|
||
exit(0);
|
||
}
|
||
|
||
/* ---- Command dispatch table ---- */
|
||
|
||
/* dispatch_stats: static hit vs. miss coverage summary */
|
||
static void handle_dispatch_stats(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||
"\"static_hits\":%llu,"
|
||
"\"miss_total\":%llu,"
|
||
"\"miss_unique\":%d}",
|
||
id,
|
||
(unsigned long long)g_dispatch_static_hits,
|
||
(unsigned long long)s_unknown_seq,
|
||
s_unknown_unique_count);
|
||
}
|
||
|
||
/* 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");
|
||
}
|
||
|
||
static void handle_card_mgr_trace(int id, const char *json) {
|
||
int count = json_get_int(json, "count", 256);
|
||
if (count < 1) count = 1;
|
||
if (count > CARD_MGR_TRACE_CAP) count = CARD_MGR_TRACE_CAP;
|
||
int newest = json_get_int(json, "newest", 0) != 0;
|
||
|
||
uint64_t total = s_card_mgr_trace_seq;
|
||
uint64_t oldest = (total > CARD_MGR_TRACE_CAP) ? total - CARD_MGR_TRACE_CAP : 0;
|
||
uint64_t available = total - oldest;
|
||
if ((uint64_t)count > available) count = (int)available;
|
||
uint64_t start = total - (uint64_t)count;
|
||
if (start < oldest) start = oldest;
|
||
|
||
size_t buf_sz = 256u + (size_t)count * 640u;
|
||
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,\"entries\":[",
|
||
id, (unsigned long long)total, (unsigned long long)oldest);
|
||
|
||
int first = 1;
|
||
for (int i = 0; i < count && pos < buf_sz - 640u; i++) {
|
||
uint64_t seq = newest ? (total - 1ull - (uint64_t)i) : (start + (uint64_t)i);
|
||
if (seq < oldest || seq >= total) continue;
|
||
const CardMgrTraceEntry *e = &s_card_mgr_trace[seq % CARD_MGR_TRACE_CAP];
|
||
if (e->seq != seq) continue;
|
||
pos += snprintf(buf + pos, buf_sz - pos,
|
||
"%s{\"seq\":%llu,\"src\":%u,\"frame\":%u,\"func\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||
"\"s9f20\":\"0x%08X\",\"s9f24\":\"0x%08X\",\"s9f28\":\"0x%08X\","
|
||
"\"s9f2c\":\"0x%08X\",\"s9f30\":\"0x%08X\",\"s9f34\":\"0x%08X\","
|
||
"\"s7258\":\"0x%08X\",\"s725c\":\"0x%08X\",\"s7264\":\"0x%08X\","
|
||
"\"s74bc\":\"0x%08X\",\"s7500\":\"0x%08X\",\"s7504\":\"0x%08X\","
|
||
"\"s7508\":\"0x%08X\",\"s750c\":\"0x%08X\",\"s7510\":\"0x%08X\","
|
||
"\"s7514\":\"0x%08X\",\"s7518\":\"0x%08X\",\"s751c\":\"0x%08X\","
|
||
"\"s7520\":\"0x%08X\","
|
||
"\"s7528\":\"0x%08X\",\"s752c\":\"0x%08X\",\"s7558\":\"0x%08X\","
|
||
"\"s7568\":\"0x%08X\",\"s756c\":\"0x%08X\"}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->source, e->frame, e->func_addr,
|
||
e->pc, e->ra, e->a0, e->a1, e->a2, e->a3, e->v0, e->t0, e->t1,
|
||
e->state_9f20, e->state_9f24, e->state_9f28,
|
||
e->state_9f2c, e->state_9f30, e->state_9f34,
|
||
e->state_7258, e->state_725c, e->state_7264,
|
||
e->state_74bc, e->state_7500, e->state_7504,
|
||
e->state_7508, e->state_750c, e->state_7510,
|
||
e->state_7514, e->state_7518, e->state_751c,
|
||
e->state_7520,
|
||
e->state_7528, e->state_752c, e->state_7558,
|
||
e->state_7568, e->state_756c);
|
||
first = 0;
|
||
}
|
||
pos += snprintf(buf + pos, buf_sz - pos, "]}");
|
||
debug_server_send_line(buf);
|
||
free(buf);
|
||
}
|
||
|
||
static void handle_card_mgr_clear(int id, const char *json) {
|
||
(void)json;
|
||
s_card_mgr_trace_seq = 0;
|
||
memset(s_card_mgr_trace, 0, sizeof(s_card_mgr_trace));
|
||
send_ok(id);
|
||
}
|
||
|
||
/* ---- 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);
|
||
/* Enable per-dispatch shadow-stack tracking when a real range is set;
|
||
* a "lo=0,hi=0xFFFFFFFF" call also flips it on (caller wants everything). */
|
||
s_fn_trace_active = 1;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"lo\":\"0x%08X\",\"hi\":\"0x%08X\",\"active\":%d}\n",
|
||
id, s_fn_trace_filter_lo, s_fn_trace_filter_hi, s_fn_trace_active);
|
||
}
|
||
|
||
static void handle_fn_disable(int id, const char *json) {
|
||
(void)json;
|
||
s_fn_trace_active = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"active\":0}\n", id);
|
||
}
|
||
|
||
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;
|
||
s_fn_direct_seen = 0;
|
||
s_fn_direct_no_cpu = 0;
|
||
s_fn_direct_filtered = 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_EXIT_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,"
|
||
"\"entry_capacity\":%llu,\"exit_capacity\":%llu,"
|
||
"\"filter_lo\":\"0x%08X\",\"filter_hi\":\"0x%08X\","
|
||
"\"active\":%d,\"direct_seen\":%llu,"
|
||
"\"direct_no_cpu\":%llu,\"direct_filtered\":%llu}\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,
|
||
(unsigned long long)FN_TRACE_CAP,
|
||
(unsigned long long)FN_EXIT_TRACE_CAP,
|
||
s_fn_trace_filter_lo, s_fn_trace_filter_hi,
|
||
s_fn_trace_active,
|
||
(unsigned long long)s_fn_direct_seen,
|
||
(unsigned long long)s_fn_direct_no_cpu,
|
||
(unsigned long long)s_fn_direct_filtered);
|
||
}
|
||
|
||
/* Helper: parse filter / range / count for fn_*_dump.
|
||
*
|
||
* Windowing defaults to the most recent 1M entries unless the caller
|
||
* explicitly passes seq_lo. Keep emitted results bounded: the debug server
|
||
* shares process health with the runtime, so giant JSON replies can look like
|
||
* a game freeze. */
|
||
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];
|
||
static const uint64_t DEFAULT_WINDOW = 1ull << 20; /* 1M entries */
|
||
static const int DEFAULT_COUNT = 256;
|
||
static const int MAX_COUNT = 2048;
|
||
*out_seq_hi = total;
|
||
*out_seq_lo = (total > DEFAULT_WINDOW) ? total - DEFAULT_WINDOW : 0;
|
||
*out_addr_lo = 0;
|
||
*out_addr_hi = 0xFFFFFFFFu;
|
||
*out_max = DEFAULT_COUNT;
|
||
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);
|
||
if (json_get_str(json, "addr_hi", buf, sizeof(buf))) *out_addr_hi = hex_to_u32(buf);
|
||
*out_max = json_get_int(json, "count", *out_max);
|
||
if (*out_max < 1) *out_max = 1;
|
||
if (*out_max > MAX_COUNT) *out_max = MAX_COUNT;
|
||
}
|
||
|
||
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;
|
||
|
||
size_t BUF_SZ = 256u + (size_t)max_count * 640u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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 - 640; 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\",\"s0\":\"0x%08X\",\"s1\":\"0x%08X\","
|
||
"\"s2\":\"0x%08X\",\"s3\":\"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->s0, e->s1, e->s2, e->s3, 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_entry_tail(int id, const char *json) {
|
||
if (!s_fn_entry) { send_err(id, "not initialized"); return; }
|
||
int count = json_get_int(json, "count", 64);
|
||
if (count < 1) count = 1;
|
||
if (count > 256) count = 256;
|
||
|
||
uint64_t total = s_fn_entry_seq;
|
||
uint64_t oldest = (total > FN_TRACE_CAP) ? total - FN_TRACE_CAP : 0;
|
||
uint64_t start = (total > (uint64_t)count) ? total - (uint64_t)count : 0;
|
||
if (start < oldest) start = oldest;
|
||
|
||
size_t BUF_SZ = 256u + (size_t)count * 512u;
|
||
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,\"entries\":[",
|
||
id, (unsigned long long)total);
|
||
int first = 1;
|
||
int emitted = 0;
|
||
for (uint64_t s = start; s < total && pos < BUF_SZ - 512; s++) {
|
||
FnEntryEntry *e = &s_fn_entry[s % FN_TRACE_CAP];
|
||
if (e->seq != s) 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\",\"s0\":\"0x%08X\","
|
||
"\"s1\":\"0x%08X\",\"s2\":\"0x%08X\",\"s3\":\"0x%08X\",\"frame\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->func_addr, e->ra,
|
||
e->a0, e->a1, e->a2, e->a3, e->t1,
|
||
e->s0, e->s1, e->s2, e->s3, e->frame);
|
||
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_EXIT_TRACE_CAP) ? s_fn_exit_seq - FN_EXIT_TRACE_CAP : 0;
|
||
if (seq_lo < oldest) seq_lo = oldest;
|
||
if (seq_hi > s_fn_exit_seq) seq_hi = s_fn_exit_seq;
|
||
|
||
size_t BUF_SZ = 256u + (size_t)max_count * 512u;
|
||
if (BUF_SZ > (size_t)128 * 1024 * 1024) BUF_SZ = (size_t)128 * 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_EXIT_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);
|
||
}
|
||
|
||
/* cd_read_log: dump the CD DMA transfer ring. Each entry records the
|
||
* setloc LBA, destination RAM address, and byte count for one forward
|
||
* channel-3 DMA transfer. Use with overlay_dump to map overlay regions
|
||
* back to their disc positions for the extract_overlays.py disc scanner.
|
||
*
|
||
* Parameters: "tail" (int, default 256) — how many recent entries to return.
|
||
*/
|
||
static void handle_cd_read_log(int id, const char *json)
|
||
{
|
||
extern uint32_t cd_dma_log_get_total(void);
|
||
extern void cd_dma_log_get_entry(uint32_t idx, int *lba,
|
||
uint32_t *dest, uint32_t *size);
|
||
|
||
int tail = json_get_int(json, "tail", 256);
|
||
uint32_t total = cd_dma_log_get_total();
|
||
uint32_t cap = 65536;
|
||
uint32_t avail = total < cap ? total : cap;
|
||
if ((uint32_t)tail > avail) tail = (int)avail;
|
||
|
||
uint32_t start_idx = total > (uint32_t)tail ? total - (uint32_t)tail : 0;
|
||
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,\"count\":%d,\"entries\":[",
|
||
id, total, tail);
|
||
int first = 1;
|
||
for (uint32_t i = start_idx; i < total; i++) {
|
||
int lba; uint32_t dest, size;
|
||
cd_dma_log_get_entry(i, &lba, &dest, &size);
|
||
if (lba < 0) continue;
|
||
send_fmt("%s{\"lba\":%d,\"dest\":\"0x%08X\",\"size\":%u}",
|
||
first ? "" : ",", lba, dest, size);
|
||
first = 0;
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
/* cdrom_instant_rate: get/set the 'instant' per-frame sector-IRQ budget
|
||
* (step 3 tunable). Param "n" (optional int): new budget, clamped by
|
||
* cdrom_set_instant_rate. Always returns the current value, so a no-arg
|
||
* call is a pure query. */
|
||
static void handle_cdrom_instant_rate(int id, const char *json)
|
||
{
|
||
int n = json_get_int(json, "n", 0);
|
||
if (n > 0) cdrom_set_instant_rate(n);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"instant_max_per_frame\":%d}\n",
|
||
id, cdrom_get_instant_rate());
|
||
}
|
||
|
||
/* turbo_loads: get/set the turbo-through-loads enable (step 4). Param "n"
|
||
* (optional: 0/1). Reports the enable, whether the load predicate holds RIGHT
|
||
* NOW, and how many vblanks have run unpaced. */
|
||
static void handle_turbo_loads(int id, const char *json)
|
||
{
|
||
extern int g_turbo_loads_enabled;
|
||
extern uint64_t g_turbo_loads_frames;
|
||
extern int fntrace_is_game_started(void);
|
||
int n = json_get_int(json, "n", -1);
|
||
if (n == 0 || n == 1) g_turbo_loads_enabled = n;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"load_active\":%d,"
|
||
"\"game_started\":%d,\"turbo_frames\":%llu}\n",
|
||
id, g_turbo_loads_enabled, cdrom_load_in_progress(),
|
||
fntrace_is_game_started(),
|
||
(unsigned long long)g_turbo_loads_frames);
|
||
}
|
||
|
||
/* cdrom_bursts: dump the always-on CD load-burst ring, newest first. Each
|
||
* record is one gap-separated run of delivered data sectors — i.e. one load.
|
||
* Param "count" (optional, default 32, max 128). */
|
||
static void handle_cdrom_bursts(int id, const char *json)
|
||
{
|
||
int count = json_get_int(json, "count", 32);
|
||
if (count < 1) count = 1;
|
||
if (count > 128) count = 128;
|
||
CdBurstRecord recs[128];
|
||
int n = cdrom_get_bursts(recs, count);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%u,"
|
||
"\"instant_max_per_frame\":%d,\"bursts\":[",
|
||
id, cdrom_get_burst_total(), cdrom_get_instant_rate());
|
||
for (int i = 0; i < n; i++) {
|
||
CdBurstRecord *b = &recs[i];
|
||
send_fmt("%s{\"start_frame\":%u,\"end_frame\":%u,"
|
||
"\"frames\":%u,\"ms\":%llu,\"sectors\":%u,"
|
||
"\"rate\":%u,\"divisor\":%u}",
|
||
i ? "," : "", b->start_frame, b->end_frame,
|
||
b->end_frame - b->start_frame + 1u,
|
||
(unsigned long long)(b->end_ms - b->start_ms),
|
||
b->sectors, b->rate, b->divisor);
|
||
}
|
||
send_fmt("]}\n");
|
||
}
|
||
|
||
/* autocompile_status: variant-capture automation state — autocapture
|
||
* enable/trigger counters + the background compile's state and output tail
|
||
* (in-memory ring; no log files). */
|
||
static void handle_autocompile_status(int id, const char *json)
|
||
{
|
||
extern int autocompile_status_json(char *out, int cap);
|
||
extern void overlay_autocapture_get_status(int *enabled,
|
||
uint32_t *triggers,
|
||
uint64_t *last_delta);
|
||
(void)json;
|
||
int ac_en = 0;
|
||
uint32_t trig = 0;
|
||
uint64_t delta = 0;
|
||
overlay_autocapture_get_status(&ac_en, &trig, &delta);
|
||
char comp[4096];
|
||
autocompile_status_json(comp, sizeof(comp));
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"autocapture_enabled\":%d,"
|
||
"\"triggers\":%u,\"last_pressure\":%llu,\"compile\":%s}\n",
|
||
id, ac_en, trig, (unsigned long long)delta, comp);
|
||
}
|
||
|
||
/* autocompile_run: manually kick the configured background compile. */
|
||
static void handle_autocompile_run(int id, const char *json)
|
||
{
|
||
extern int autocompile_request(void);
|
||
(void)json;
|
||
int started = autocompile_request();
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"started\":%d}\n", id, started);
|
||
}
|
||
|
||
/* overlay_rescan: re-scan the DLL cache and clear the checked-regions memo
|
||
* so newly compiled DLLs load on the next dispatch. */
|
||
static void handle_overlay_rescan(int id, const char *json)
|
||
{
|
||
extern void overlay_loader_rescan(void);
|
||
(void)json;
|
||
overlay_loader_rescan();
|
||
send_fmt("{\"id\":%d,\"ok\":true}\n", id);
|
||
}
|
||
|
||
/* overlay_capture_dump: flush overlay_captures.json on demand (does not require
|
||
* a clean window-close). Use after roaming through areas so a freeze/kill can't
|
||
* lose the captured overlays. Writes next to the runtime exe. */
|
||
static void handle_overlay_capture_dump(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
overlay_capture_write_json();
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"capture_entries\":%d}\n",
|
||
id, overlay_capture_count());
|
||
}
|
||
|
||
/* overlay_loader_status: report the dynamic overlay DLL loader state —
|
||
* whether active, how many functions are registered, which regions have
|
||
* been checked, and the most recent load event. Rule-3 inspection path for
|
||
* the loader (it does no stderr logging). */
|
||
static void handle_overlay_loader_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
int active = 0, registered = 0, nchecked = 0, nwritten = 0;
|
||
int file_found = 0;
|
||
uint32_t last_crc = 0;
|
||
char cache_dir[512] = {0}, game_id[64] = {0};
|
||
uint32_t checked[8] = {0};
|
||
overlay_loader_get_status(&active, ®istered, &nchecked,
|
||
cache_dir, sizeof(cache_dir),
|
||
game_id, sizeof(game_id),
|
||
checked, 8, &nwritten,
|
||
&last_crc, &file_found);
|
||
/* Escape backslashes in cache_dir for JSON */
|
||
char esc_dir[768] = {0};
|
||
for (int si = 0, di = 0; cache_dir[si] && di < (int)sizeof(esc_dir)-2; si++) {
|
||
if (cache_dir[si] == '\\') esc_dir[di++] = '\\';
|
||
esc_dir[di++] = cache_dir[si];
|
||
}
|
||
const char *msg = overlay_loader_last_msg();
|
||
char esc_msg[512] = {0};
|
||
for (int si = 0, di = 0; msg[si] && di < (int)sizeof(esc_msg)-2; si++) {
|
||
if (msg[si] == '\\' || msg[si] == '"') esc_msg[di++] = '\\';
|
||
esc_msg[di++] = msg[si];
|
||
}
|
||
char buf[1536];
|
||
int n = snprintf(buf, sizeof(buf),
|
||
"{\"id\":%d,\"ok\":true,\"active\":%d,\"registered\":%d,"
|
||
"\"regions_checked\":%d,\"last_crc\":\"0x%08X\",\"file_found\":%d,"
|
||
"\"cache_dir\":\"%s\",\"game_id\":\"%s\",\"last_msg\":\"%s\"",
|
||
id, active, registered, nchecked, last_crc, file_found,
|
||
esc_dir, game_id, esc_msg);
|
||
if (nwritten > 0) {
|
||
n += snprintf(buf + n, sizeof(buf) - n, ",\"checked\":[");
|
||
for (int i = 0; i < nwritten; i++)
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
"%s\"0x%08X\"", i ? "," : "", checked[i]);
|
||
n += snprintf(buf + n, sizeof(buf) - n, "]");
|
||
}
|
||
/* Inc1-D registration-lifetime counters. */
|
||
{
|
||
uint32_t loads=0, invs=0, unreg=0, lw_pc=0, lw_addr=0, lw_size=0, reval=0;
|
||
uint64_t dnat=0, dint=0, stale=0; int regions=0;
|
||
overlay_loader_get_counters(&loads, &invs, &unreg, &dnat, &dint,
|
||
&stale, &lw_pc, &lw_addr, &lw_size, ®ions,
|
||
&reval);
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
",\"regions\":%d,\"loads\":%u,\"invalidations\":%u,"
|
||
"\"revalidations\":%u,\"unregistered_funcs\":%u,\"dispatch_native\":%llu,"
|
||
"\"dispatch_interp_fallback\":%llu,\"stale_blocked\":%llu,"
|
||
"\"last_write_pc\":\"0x%08X\",\"last_write_addr\":\"0x%08X\","
|
||
"\"last_write_size\":%u",
|
||
regions, loads, invs, reval, unreg,
|
||
(unsigned long long)dnat, (unsigned long long)dint,
|
||
(unsigned long long)stale, lw_pc, lw_addr, lw_size);
|
||
int r0v=0; uint32_t r0w=0, r0lo=0, r0hi=0, r0crc=0, ratt=0, rmiss=0, rlast=0;
|
||
overlay_loader_get_reload_debug(&r0v, &r0w, &r0lo, &r0hi, &r0crc,
|
||
&ratt, &rmiss, &rlast);
|
||
n += snprintf(buf + n, sizeof(buf) - n,
|
||
",\"r0_valid\":%d,\"r0_writes_since_invalid\":%u,"
|
||
"\"r0_fn_lo\":\"0x%08X\",\"r0_fn_hi\":\"0x%08X\",\"r0_crc_live\":\"0x%08X\","
|
||
"\"reval_attempts\":%u,\"reval_crc_miss\":%u,\"last_reval_crc\":\"0x%08X\"",
|
||
r0v, r0w, r0lo, r0hi, r0crc, ratt, rmiss, rlast);
|
||
}
|
||
snprintf(buf + n, sizeof(buf) - n, "}\n");
|
||
send_fmt("%s", buf);
|
||
}
|
||
|
||
/* overlay_candidates: dump the per-entry candidate table (addr, state, stored
|
||
* vs live code hash, generation) so reload-on-return can be inspected directly. */
|
||
static void handle_overlay_candidates(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern int overlay_loader_dump_candidates(char *out, int cap);
|
||
static char cbuf[65536];
|
||
int len = overlay_loader_dump_candidates(cbuf, (int)sizeof(cbuf));
|
||
if (len < 0) len = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"candidates\":%s}\n", id, cbuf);
|
||
}
|
||
|
||
/* overlay_native_ring: dump the always-on ring of native overlay calls + the
|
||
* in-progress entry (freeze-inside-native detector). Measurement surface for
|
||
* the native↔interpreter parity investigation. */
|
||
static void handle_overlay_native_ring(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern int overlay_loader_dump_native_ring(char *out, int cap);
|
||
static char rbuf[16384];
|
||
int len = overlay_loader_dump_native_ring(rbuf, (int)sizeof(rbuf));
|
||
if (len < 0) len = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"ring\":%s}\n", id, rbuf);
|
||
}
|
||
|
||
/* overlay_diff_on/off: same-state native↔interp differential. With it on, each
|
||
* matched overlay function runs both ways from identical CPU+RAM state; the
|
||
* interp result is kept (game stays correct) and computation divergences are
|
||
* logged. overlay_shadow_dump returns the divergence records. */
|
||
static void handle_overlay_diff_on(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_diff_mode(int on);
|
||
overlay_loader_set_diff_mode(1);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"diff_mode\":1}\n", id);
|
||
}
|
||
static void handle_overlay_diff_off(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_diff_mode(int on);
|
||
overlay_loader_set_diff_mode(0);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"diff_mode\":0}\n", id);
|
||
}
|
||
static void handle_overlay_shadow_dump(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern int overlay_loader_dump_shadow(char *out, int cap);
|
||
static char sbuf[1 << 18];
|
||
int len = overlay_loader_dump_shadow(sbuf, (int)sizeof(sbuf));
|
||
if (len < 0) len = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"shadow\":%s}\n", id, sbuf);
|
||
}
|
||
static void handle_overlay_shadow_detail(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern int overlay_loader_dump_shadow_detail(char *out, int cap);
|
||
static char dbuf[8192];
|
||
int len = overlay_loader_dump_shadow_detail(dbuf, (int)sizeof(dbuf));
|
||
if (len < 0) len = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"detail\":%s}\n", id, dbuf);
|
||
}
|
||
|
||
/* overlay_irq_suppress_on/off: drop native overlay code's per-block interrupt
|
||
* checks (cadence ~ interpreter) to test whether the parity divergence is
|
||
* interrupt-delivery timing rather than codegen. */
|
||
static void handle_overlay_irq_suppress_on(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_irq_suppress(int mode, uint32_t ratelimit);
|
||
overlay_loader_set_irq_suppress(1, 0);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"irq_suppress\":1}\n", id);
|
||
}
|
||
static void handle_overlay_irq_suppress_off(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_irq_suppress(int mode, uint32_t ratelimit);
|
||
extern void overlay_loader_get_irq_suppress(int *mode, uint32_t *rl, uint64_t *supp);
|
||
int m=0; uint32_t rl=0; uint64_t supp=0;
|
||
overlay_loader_get_irq_suppress(&m,&rl,&supp);
|
||
overlay_loader_set_irq_suppress(0, 0);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"irq_suppress\":0,\"was_suppressed\":%llu}\n",
|
||
id, (unsigned long long)supp);
|
||
}
|
||
/* overlay_irq_ratelimit <n>: arm native per-block interrupt checks at a
|
||
* rate-limited cadence — the real check fires every Nth block, the rest are
|
||
* dropped. N=1 == normal native cadence (every block); larger N approaches the
|
||
* interpreter's coarse cadence. This is the decisive Priority-1 A/B knob; full
|
||
* suppression (overlay_irq_suppress_on) is too blunt to be conclusive. */
|
||
static void handle_overlay_irq_ratelimit(int id, const char *json)
|
||
{
|
||
extern void overlay_loader_set_irq_suppress(int mode, uint32_t ratelimit);
|
||
int n = json_get_int(json, "n", 1);
|
||
if (n < 1) n = 1;
|
||
overlay_loader_set_irq_suppress(1, (uint32_t)n);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"irq_suppress\":1,\"ratelimit\":%d}\n", id, n);
|
||
}
|
||
/* overlay_native_event_granularity <conservative|normal>: when conservative,
|
||
* psx_advance_cycles splits a batched (N>1) cycle advance into N single-cycle
|
||
* steps so device events fire at their true due-cycle in order — replicating
|
||
* the interpreter's fine event timeline for native execution. Decisive test:
|
||
* if the village->overworld blue screen clears with this on, the cause is
|
||
* per-block event-ordering (root #2/#3), and the real fix is a due-cycle event
|
||
* scheduler. */
|
||
static void handle_overlay_native_event_granularity(int id, const char *json)
|
||
{
|
||
extern int g_event_step_conservative;
|
||
char mode[32];
|
||
if (!json_get_str(json, "mode", mode, sizeof(mode))) {
|
||
send_err(id, "missing mode (conservative|normal)"); return;
|
||
}
|
||
int conservative = (strcmp(mode, "conservative") == 0);
|
||
g_event_step_conservative = conservative;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"event_granularity\":\"%s\"}\n",
|
||
id, conservative ? "conservative" : "normal");
|
||
}
|
||
|
||
/* overlay_fp_dump: dump the native↔interp execution-fingerprint log (per
|
||
* candidate function: in/out register CRC, native flag) for offline diffing. */
|
||
static void handle_overlay_fp_dump(int id, const char *json)
|
||
{
|
||
extern int overlay_loader_write_fp_file(const char *path);
|
||
char path[512];
|
||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||
snprintf(path, sizeof(path), "overlay_fp.json");
|
||
int count = overlay_loader_write_fp_file(path);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"file\":\"%s\",\"entries\":%d}\n",
|
||
id, path, count);
|
||
}
|
||
|
||
/* dirty_insn_gate <lo> <hi>: extra phys PC range the per-insn interp log
|
||
* records (on top of the hardwired kernel ranges). hi=0 disables. */
|
||
static void handle_dirty_insn_gate(int id, const char *json)
|
||
{
|
||
extern uint32_t g_insn_gate_lo, g_insn_gate_hi;
|
||
char buf[32];
|
||
if (json_get_str(json, "lo", buf, sizeof(buf))) g_insn_gate_lo = hex_to_u32(buf);
|
||
if (json_get_str(json, "hi", buf, sizeof(buf))) g_insn_gate_hi = hex_to_u32(buf);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"gate_lo\":\"0x%08X\",\"gate_hi\":\"0x%08X\"}\n",
|
||
id, g_insn_gate_lo, g_insn_gate_hi);
|
||
}
|
||
|
||
/* insn_freeze <addr> <nth>: freeze the insn ring immediately BEFORE the Nth
|
||
* candidate dispatch of phys <addr>, preserving the pre-divergence window.
|
||
* addr=0 disarms and unfreezes. */
|
||
static void handle_insn_freeze(int id, const char *json)
|
||
{
|
||
extern uint32_t g_insn_freeze_addr, g_insn_freeze_nth, g_insn_freeze_count;
|
||
extern int g_insn_log_frozen;
|
||
char buf[32];
|
||
if (json_get_str(json, "addr", buf, sizeof(buf)))
|
||
g_insn_freeze_addr = hex_to_u32(buf) & 0x1FFFFFFFu;
|
||
g_insn_freeze_nth = (uint32_t)json_get_int(json, "nth", 1);
|
||
g_insn_freeze_count = 0;
|
||
g_insn_log_frozen = 0;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"freeze_addr\":\"0x%08X\",\"nth\":%u}\n",
|
||
id, g_insn_freeze_addr, g_insn_freeze_nth);
|
||
}
|
||
|
||
static void handle_insn_freeze_status(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern uint32_t g_insn_freeze_addr, g_insn_freeze_nth, g_insn_freeze_count;
|
||
extern int g_insn_log_frozen;
|
||
extern uint64_t g_dirty_ram_insn_log_seq;
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"freeze_addr\":\"0x%08X\",\"nth\":%u,"
|
||
"\"count\":%u,\"frozen\":%d,\"insn_log_seq\":%llu}\n",
|
||
id, g_insn_freeze_addr, g_insn_freeze_nth, g_insn_freeze_count,
|
||
g_insn_log_frozen, (unsigned long long)g_dirty_ram_insn_log_seq);
|
||
}
|
||
|
||
/* event_ring_dump: write the whole live event-timeline ring (IRQ deliver/gate,
|
||
* I_STAT raises/changes, DMA kick/done, each tagged cycle/pc/func/mode/overlay)
|
||
* to a JSON file for offline diffing of native-OFF vs native-ON runs. Optional
|
||
* "path" param (default event_ring.json in CWD). */
|
||
static void handle_event_ring_dump(int id, const char *json)
|
||
{
|
||
extern int event_ring_dump_file(const char *path);
|
||
char path[256];
|
||
if (!json_get_str(json, "path", path, sizeof(path)))
|
||
snprintf(path, sizeof(path), "event_ring.json");
|
||
int count = event_ring_dump_file(path);
|
||
if (count < 0) { send_err(id, "event_ring file open failed"); return; }
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"file\":\"%s\",\"entries\":%d}\n",
|
||
id, path, count);
|
||
}
|
||
/* event_ring_tail: inline JSON tail of the most-recent N events for a quick
|
||
* eyeball / first-output validation (default 64). */
|
||
static void handle_event_ring_tail(int id, const char *json)
|
||
{
|
||
extern int event_ring_dump_json(char *out, int cap, int max_entries);
|
||
int n = json_get_int(json, "n", 64);
|
||
if (n < 1) n = 1;
|
||
if (n > 4000) n = 4000;
|
||
int cap = n * 256 + 512;
|
||
char *out = (char *)malloc((size_t)cap);
|
||
if (!out) { send_err(id, "alloc failed"); return; }
|
||
int w = snprintf(out, cap, "{\"id\":%d,\"ok\":true,\"ring\":", id);
|
||
w += event_ring_dump_json(out + w, cap - w, n);
|
||
snprintf(out + w, cap - w, "}");
|
||
debug_server_send_line(out);
|
||
free(out);
|
||
}
|
||
/* event_ring_clear: reset the ring to isolate a fresh capture window. */
|
||
static void handle_event_ring_clear(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void event_ring_clear(void);
|
||
event_ring_clear();
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"cleared\":true}\n", id);
|
||
}
|
||
|
||
/* overlay_native_on / overlay_native_off: toggle native overlay EXECUTION at
|
||
* runtime (validity tracking still runs). A/B to prove whether native execution
|
||
* is the cause without a rebuild. */
|
||
static void handle_overlay_native_on(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_native_exec(int on);
|
||
overlay_loader_set_native_exec(1);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"native_exec\":1}\n", id);
|
||
}
|
||
static void handle_overlay_native_off(int id, const char *json)
|
||
{
|
||
(void)json;
|
||
extern void overlay_loader_set_native_exec(int on);
|
||
overlay_loader_set_native_exec(0);
|
||
send_fmt("{\"id\":%d,\"ok\":true,\"native_exec\":0}\n", id);
|
||
}
|
||
|
||
/* overlay_dump: extract RAM regions that dirty_ram has marked executable
|
||
* above a threshold physical address. Writes <crc32>.bin files to a
|
||
* caller-supplied directory and returns a JSON manifest.
|
||
*
|
||
* Parameters (JSON):
|
||
* "lo" — low physical address threshold (default "0x98000")
|
||
* "dir" — output directory for .bin files (default "overlays")
|
||
*
|
||
* Returns:
|
||
* {"ok":true,"regions":[{"addr":"0x...","size":N,"crc32":"0x...","file":"<hex>.bin"},...]}
|
||
*/
|
||
static void handle_overlay_dump(int id, const char *json)
|
||
{
|
||
extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index);
|
||
extern uint32_t dirty_ram_get_bitmap_word_count(void);
|
||
extern uint8_t *memory_get_ram_ptr(void);
|
||
extern uint32_t crc32_compute(const uint8_t *data, size_t len);
|
||
|
||
char lo_buf[32] = {0};
|
||
char dir_buf[512] = {0};
|
||
json_get_str(json, "lo", lo_buf, sizeof(lo_buf));
|
||
json_get_str(json, "dir", dir_buf, sizeof(dir_buf));
|
||
|
||
uint32_t lo_phys = lo_buf[0] ? (uint32_t)strtoul(lo_buf, NULL, 0) : 0x00098000u;
|
||
if (!dir_buf[0]) strncpy(dir_buf, "overlays", sizeof(dir_buf) - 1);
|
||
|
||
/* Create output directory (best-effort). */
|
||
#ifdef _WIN32
|
||
{ char cmd[600]; snprintf(cmd, sizeof(cmd), "mkdir \"%s\" 2>nul", dir_buf); system(cmd); }
|
||
#else
|
||
{ char cmd[600]; snprintf(cmd, sizeof(cmd), "mkdir -p \"%s\"", dir_buf); system(cmd); }
|
||
#endif
|
||
|
||
uint8_t *ram = memory_get_ram_ptr();
|
||
uint32_t bw = dirty_ram_get_bitmap_word_count();
|
||
uint32_t page_sz = 4096u;
|
||
uint32_t lo_page = lo_phys / page_sz;
|
||
|
||
/* Scan bitmap for contiguous dirty-page runs above lo_phys. */
|
||
char out[65536];
|
||
int pos = 0;
|
||
int first = 1;
|
||
int in_run = 0;
|
||
uint32_t run_start = 0;
|
||
|
||
pos += snprintf(out + pos, sizeof(out) - pos,
|
||
"{\"id\":%d,\"ok\":true,\"regions\":[", id);
|
||
|
||
for (uint32_t page = lo_page; page < bw * 32u; page++) {
|
||
uint32_t word = dirty_ram_get_bitmap_word(page >> 5);
|
||
int dirty = (word >> (page & 31u)) & 1u;
|
||
|
||
if (dirty && !in_run) {
|
||
in_run = 1;
|
||
run_start = page;
|
||
} else if (!dirty && in_run) {
|
||
in_run = 0;
|
||
/* Emit region [run_start, page) */
|
||
uint32_t phys = run_start * page_sz;
|
||
uint32_t size = (page - run_start) * page_sz;
|
||
uint32_t crc = crc32_compute(ram + phys, size);
|
||
|
||
char fname[32];
|
||
snprintf(fname, sizeof(fname), "%08X.bin", crc);
|
||
char fpath[600];
|
||
snprintf(fpath, sizeof(fpath), "%s/%s", dir_buf, fname);
|
||
FILE *bf = fopen(fpath, "wb");
|
||
if (bf) { fwrite(ram + phys, 1, size, bf); fclose(bf); }
|
||
|
||
pos += snprintf(out + pos, sizeof(out) - pos,
|
||
"%s{\"addr\":\"0x%08X\",\"size\":%u,"
|
||
"\"crc32\":\"0x%08X\",\"file\":\"%s\"}",
|
||
first ? "" : ",", phys, size, crc, fname);
|
||
first = 0;
|
||
}
|
||
}
|
||
/* Close any open run at end of RAM. */
|
||
if (in_run) {
|
||
uint32_t page = bw * 32u;
|
||
uint32_t phys = run_start * page_sz;
|
||
uint32_t size = (page - run_start) * page_sz;
|
||
uint32_t crc = crc32_compute(ram + phys, size);
|
||
char fname[32];
|
||
snprintf(fname, sizeof(fname), "%08X.bin", crc);
|
||
char fpath[600];
|
||
snprintf(fpath, sizeof(fpath), "%s/%s", dir_buf, fname);
|
||
FILE *bf = fopen(fpath, "wb");
|
||
if (bf) { fwrite(ram + phys, 1, size, bf); fclose(bf); }
|
||
pos += snprintf(out + pos, sizeof(out) - pos,
|
||
"%s{\"addr\":\"0x%08X\",\"size\":%u,"
|
||
"\"crc32\":\"0x%08X\",\"file\":\"%s\"}",
|
||
first ? "" : ",", phys, size, crc, fname);
|
||
}
|
||
|
||
snprintf(out + pos, sizeof(out) - pos, "]}\n");
|
||
send_fmt("%s", out);
|
||
}
|
||
|
||
/* ====================================================================
|
||
* Freeze auto-dump accessors. Called from the freeze_heartbeat thread
|
||
* after it detects a main-thread stall. Each function writes a JSON
|
||
* array `[entry1,entry2,...]` of the newest entries directly to FILE*.
|
||
*
|
||
* Snapshot the ring's seq/head ONCE on entry so a concurrent writer
|
||
* (very rare at dump time) cannot make us walk off the end.
|
||
* ==================================================================== */
|
||
|
||
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
if (!s_wtrace_all) { fputs("[]", f); return; }
|
||
|
||
uint64_t total = s_wtrace_all_seq;
|
||
uint32_t head = s_wtrace_all_head;
|
||
uint32_t avail = (total < WRITE_TRACE_ALL_CAP)
|
||
? (uint32_t)total : WRITE_TRACE_ALL_CAP;
|
||
if (max_count > avail) max_count = avail;
|
||
if (max_count == 0) { fputs("[]", f); return; }
|
||
|
||
/* Walk oldest-first within the newest `max_count` window. */
|
||
uint32_t start = (total < WRITE_TRACE_ALL_CAP)
|
||
? (avail - max_count)
|
||
: (head + (WRITE_TRACE_ALL_CAP - max_count)) % WRITE_TRACE_ALL_CAP;
|
||
|
||
fputc('[', f);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
uint32_t idx = (start + i) % WRITE_TRACE_ALL_CAP;
|
||
const WriteTraceAllEntry *e = &s_wtrace_all[idx];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"new\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"frame\":%u,\"w\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->addr, e->new_val,
|
||
e->pc, e->ra, e->frame, (unsigned)e->w);
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
if (!s_wtrace) { fputs("[]", f); return; }
|
||
|
||
uint64_t total = s_wtrace_seq;
|
||
uint32_t head = s_wtrace_head;
|
||
uint32_t avail = (total < WRITE_TRACE_CAP) ? (uint32_t)total : WRITE_TRACE_CAP;
|
||
if (max_count > avail) max_count = avail;
|
||
if (max_count == 0) { fputs("[]", f); return; }
|
||
|
||
uint32_t start = (total < WRITE_TRACE_CAP)
|
||
? (avail - max_count)
|
||
: (head + (WRITE_TRACE_CAP - max_count)) % WRITE_TRACE_CAP;
|
||
|
||
fputc('[', f);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
uint32_t idx = (start + i) % WRITE_TRACE_CAP;
|
||
const WriteTraceEntry *e = &s_wtrace[idx];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"old\":\"0x%08X\","
|
||
"\"new\":\"0x%08X\",\"ra\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||
"\"frame\":%u,\"w\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->addr, e->old_val, e->new_val,
|
||
e->ra, e->func_addr, e->pc, e->cpu_pc, e->sp,
|
||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3, e->t0, e->t1,
|
||
e->frame, (unsigned)e->width);
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
if (!s_frame_history) { fputs("[]", f); return; }
|
||
|
||
uint64_t total = s_history_count;
|
||
if (total == 0 || max_count == 0) { fputs("[]", f); return; }
|
||
uint64_t oldest = (total > FRAME_HISTORY_CAP) ? total - FRAME_HISTORY_CAP : 0;
|
||
uint64_t start = (total > (uint64_t)max_count)
|
||
? total - (uint64_t)max_count : 0;
|
||
if (start < oldest) start = oldest;
|
||
|
||
fputc('[', f);
|
||
int first = 1;
|
||
for (uint64_t fr = start; fr < total; fr++) {
|
||
uint32_t idx = (uint32_t)(fr % FRAME_HISTORY_CAP);
|
||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||
if (r->frame_number != (uint32_t)fr) continue;
|
||
fprintf(f,
|
||
"%s{\"frame\":%u,\"verify\":%d,"
|
||
"\"sr\":\"0x%08X\",\"cause\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||
"\"pad\":\"0x%04X\",\"sio_stat\":\"0x%04X\",\"sio_ctrl\":\"0x%04X\","
|
||
"\"dispatch_count\":%u,\"total_dispatches\":%llu,"
|
||
"\"disp\":{\"x\":%u,\"y\":%u,\"w\":%u,\"h\":%u,\"off\":%u},"
|
||
"\"last_func\":\"%s\"}",
|
||
first ? "" : ",",
|
||
r->frame_number, r->verify_pass,
|
||
r->cop0_sr, r->cop0_cause, r->cop0_epc,
|
||
r->i_stat, r->i_mask,
|
||
(unsigned)r->pad_buttons,
|
||
(unsigned)r->sio_stat, (unsigned)r->sio_ctrl,
|
||
r->dispatch_count,
|
||
(unsigned long long)r->total_dispatches,
|
||
(unsigned)r->display_area_x, (unsigned)r->display_area_y,
|
||
(unsigned)r->display_w, (unsigned)r->display_h,
|
||
(unsigned)r->display_disabled,
|
||
r->last_func);
|
||
first = 0;
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_sio_pc_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
uint64_t total = s_sio_pc_trace_seq;
|
||
uint32_t avail = (total < SIO_PC_TRACE_CAP) ? (uint32_t)total : SIO_PC_TRACE_CAP;
|
||
if (max_count > avail) max_count = avail;
|
||
if (max_count == 0) { fputs("[]", f); return; }
|
||
|
||
uint64_t start = total - (uint64_t)max_count;
|
||
|
||
fputc('[', f);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
uint64_t s = start + i;
|
||
const SioPcTraceEntry *e = &s_sio_pc_trace[s % SIO_PC_TRACE_CAP];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"addr\":\"0x%08X\",\"value\":\"0x%08X\","
|
||
"\"byte_seq\":%u,\"width\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->pc, e->func,
|
||
e->addr, e->value, e->byte_seq, (unsigned)e->width);
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_thread_trace_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
uint64_t total = s_thread_trace_seq;
|
||
uint64_t avail = (total < THREAD_TRACE_CAP) ? total : THREAD_TRACE_CAP;
|
||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||
if (max_count == 0) { fputs("[]", f); return; }
|
||
|
||
uint64_t start = total - (uint64_t)max_count;
|
||
|
||
fputc('[', f);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
uint64_t s = start + i;
|
||
const ThreadTraceEntry *e = &s_thread_trace[s % THREAD_TRACE_CAP];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\","
|
||
"\"current_tcb\":\"0x%08X\",\"target_tcb\":\"0x%08X\","
|
||
"\"current_state\":\"0x%08X\",\"target_state\":\"0x%08X\","
|
||
"\"target_pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||
"\"store_pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"frame\":%u,\"in_exc\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->kind, thread_kind_name(e->kind),
|
||
e->current_tcb, e->target_tcb,
|
||
e->current_state, e->target_state,
|
||
e->target_pc, e->func,
|
||
e->last_store_pc, e->ra, e->sp,
|
||
e->a0, e->a1,
|
||
e->sr, e->epc, e->istat, e->imask,
|
||
e->frame, (unsigned)e->in_exception);
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_restore_trace_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
uint64_t total = s_restore_trace_seq;
|
||
uint64_t avail = (total < RESTORE_TRACE_CAP) ? total : RESTORE_TRACE_CAP;
|
||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||
if (max_count == 0) { fputs("[]", f); return; }
|
||
|
||
uint64_t start = total - (uint64_t)max_count;
|
||
|
||
fputc('[', f);
|
||
for (uint32_t i = 0; i < max_count; i++) {
|
||
uint64_t s = start + i;
|
||
const RestoreTraceEntry *e = &s_restore_trace[s % RESTORE_TRACE_CAP];
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\",\"jmp\":%u,"
|
||
"\"target\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||
"\"func\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||
"\"byte_seq\":%u,\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||
"\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||
"\"frame\":%u,\"in_exc\":%u}",
|
||
i ? "," : "",
|
||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||
e->jmp_val, e->target_pc, e->cpu_pc,
|
||
e->func, e->last_store_pc, e->byte_seq, e->ra, e->sp,
|
||
e->sr, e->epc, e->istat, e->imask,
|
||
e->frame, (unsigned)e->in_exception);
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_fn_entry_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) return;
|
||
if (!s_fn_entry) { fputs("[]", f); return; }
|
||
|
||
uint64_t total = s_fn_entry_seq;
|
||
uint64_t oldest = (total > FN_TRACE_CAP) ? total - FN_TRACE_CAP : 0;
|
||
uint64_t start = (total > (uint64_t)max_count)
|
||
? total - (uint64_t)max_count : 0;
|
||
if (start < oldest) start = oldest;
|
||
if (start >= total) { fputs("[]", f); return; }
|
||
|
||
fputc('[', f);
|
||
int first = 1;
|
||
for (uint64_t s = start; s < total; s++) {
|
||
const FnEntryEntry *e = &s_fn_entry[s % FN_TRACE_CAP];
|
||
if (e->seq != s) continue;
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"func\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\","
|
||
"\"a3\":\"0x%08X\",\"t1\":\"0x%08X\",\"frame\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->func_addr, e->ra,
|
||
e->a0, e->a1, e->a2, e->a3, e->t1, e->frame);
|
||
first = 0;
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
void debug_server_freeze_dump_dirty_block_json(FILE *f, uint32_t max_count)
|
||
{
|
||
if (!f) { return; }
|
||
|
||
uint64_t total = g_dirty_ram_block_log_seq;
|
||
uint64_t avail = (total < (uint64_t)DIRTY_RAM_BLOCK_LOG_CAP)
|
||
? total : (uint64_t)DIRTY_RAM_BLOCK_LOG_CAP;
|
||
uint64_t want = ((uint64_t)max_count < avail) ? (uint64_t)max_count : avail;
|
||
uint64_t start = total - want;
|
||
|
||
fputc('[', f);
|
||
int first = 1;
|
||
for (uint64_t s = start; s < total; s++) {
|
||
const DirtyRamBlockLogEntry *e =
|
||
&g_dirty_ram_block_log[s & (DIRTY_RAM_BLOCK_LOG_CAP - 1u)];
|
||
if (e->seq != s) { continue; }
|
||
fprintf(f,
|
||
"%s{\"seq\":%llu,\"target\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\","
|
||
"\"a3\":\"0x%08X\",\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||
"\"sp\":\"0x%08X\",\"frame\":%u}",
|
||
first ? "" : ",",
|
||
(unsigned long long)e->seq, e->target, e->ra,
|
||
e->a0, e->a1, e->a2, e->a3, e->t0, e->t1, e->sp, e->frame);
|
||
first = 0;
|
||
}
|
||
fputc(']', f);
|
||
}
|
||
|
||
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_read_ram }, /* alias: one request, one response */
|
||
{ "write_ram", handle_write_ram },
|
||
{ "gpu_state", handle_gpu_state },
|
||
{ "mem_words", handle_mem_words },
|
||
{ "vram_peek", handle_vram_peek },
|
||
{ "gl_coh_ring", handle_gl_coh_ring },
|
||
{ "gl_fbo_peek", handle_gl_fbo_peek },
|
||
{ "gl_vram_diff", handle_gl_vram_diff },
|
||
{ "irq_state", handle_irq_state },
|
||
{ "timers_state", handle_timers_state },
|
||
{ "cdrom_state", handle_cdrom_state },
|
||
{ "cdrom_sector_dump", handle_cdrom_sector_dump },
|
||
{ "cdrom_sector_history", handle_cdrom_sector_history },
|
||
{ "cdrom_sector_history_clear", handle_cdrom_sector_history_clear },
|
||
{ "cdrom_trace_dump", handle_cdrom_trace_dump },
|
||
{ "cdrom_trace_clear", handle_cdrom_trace_clear },
|
||
{ "dma_state", handle_dma_state },
|
||
{ "dma_trace_dump", handle_dma_trace_dump },
|
||
{ "dma_trace_clear", handle_dma_trace_clear },
|
||
{ "sio_state", handle_sio_state },
|
||
{ "mc_status", handle_mc_status },
|
||
{ "spu_status", handle_spu_status },
|
||
{ "spu_voices", handle_spu_voices },
|
||
{ "spu_events", handle_spu_events },
|
||
{ "spu_events_reset", handle_spu_events_reset },
|
||
{ "card_buffer_dump", handle_card_buffer_dump },
|
||
{ "sio_arm_audit", handle_sio_arm_audit },
|
||
{ "sio_burst_stats", handle_sio_burst_stats },
|
||
{ "pace_state", handle_pace_state },
|
||
{ "chain_trace", handle_chain_trace },
|
||
{ "sio_trace", handle_sio_trace },
|
||
{ "sio_trace_window", handle_sio_trace_window },
|
||
{ "sio_pc_trace", handle_sio_pc_trace },
|
||
{ "sio_pc_window", handle_sio_pc_window },
|
||
{ "sio_ctrl_reg_trace", handle_sio_ctrl_reg_trace },
|
||
{ "sio_ctrl_reg_window", handle_sio_ctrl_reg_window },
|
||
{ "sio_ctrl_reg_clear", handle_sio_ctrl_reg_clear },
|
||
{ "restore_trace", handle_restore_trace },
|
||
{ "restore_trace_window", handle_restore_trace_window },
|
||
{ "restore_trace_clear", handle_restore_trace_clear },
|
||
{ "thread_trace", handle_thread_trace },
|
||
{ "thread_trace_clear", handle_thread_trace_clear },
|
||
{ "thread_ctx_ring", handle_thread_ctx_ring },
|
||
{ "sreg_trace_dump", handle_sreg_trace_dump },
|
||
{ "sreg_trace_find", handle_sreg_trace_find },
|
||
{ "sreg_trace_stats", handle_sreg_trace_stats },
|
||
{ "sreg_trace_clear", handle_sreg_trace_clear },
|
||
{ "probe_trace", handle_probe_trace },
|
||
{ "probe_clear", handle_probe_clear },
|
||
{ "dirty_ram_stats", handle_dirty_ram_stats },
|
||
{ "dirty_ram_unsupported", handle_dirty_ram_unsupported },
|
||
{ "dirty_block_log", handle_dirty_block_log },
|
||
{ "dirty_flow_log", handle_dirty_flow_log },
|
||
{ "dirty_insn_log", handle_dirty_insn_log },
|
||
{ "dirty_insn_dump_file", handle_dirty_insn_dump_file },
|
||
{ "dirty_block_dump_file", handle_dirty_block_dump_file },
|
||
{ "fntrace_arm", handle_fntrace_arm },
|
||
{ "fntrace_arm_clear", handle_fntrace_arm_clear },
|
||
{ "fntrace_armed", handle_fntrace_armed },
|
||
{ "fntrace_clear", handle_fntrace_clear },
|
||
{ "fntrace_dump", handle_fntrace_dump },
|
||
{ "unknown_dispatch_log", handle_unknown_dispatch_log },
|
||
{ "bioscall_dump", handle_bioscall_dump },
|
||
{ "card_trace_dump", handle_card_trace_dump },
|
||
{ "card_txn_dump", handle_card_txn_dump },
|
||
{ "card_read_summary", handle_card_read_summary },
|
||
{ "card_read_summary_reset", handle_card_read_summary_reset },
|
||
{ "card_data_writes", handle_card_data_writes },
|
||
{ "card_data_writes_reset", handle_card_data_writes_reset },
|
||
{ "sio_irq_dump", handle_sio_irq_dump },
|
||
{ "sio_irq_window", handle_sio_irq_window },
|
||
{ "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 — normalized verb set (parity contract with psx-beetle).
|
||
* One slot per "lo,hi" pair. arm/disarm/disarm_all/reset/ranges/
|
||
* dump/stats. Both backends accept identical JSON shape. */
|
||
{ "wtrace_arm", handle_wtrace_add },
|
||
{ "wtrace_disarm", handle_wtrace_del },
|
||
{ "wtrace_disarm_all", handle_wtrace_disarm_all },
|
||
{ "wtrace_reset", handle_wtrace_clear },
|
||
{ "wtrace_ranges", handle_wtrace_ranges },
|
||
{ "wtrace_dump", handle_wtrace_dump },
|
||
{ "wtrace_stats", handle_wtrace_stats },
|
||
{ "wtrace_boot_dump", handle_wtrace_boot_dump },
|
||
{ "wtrace_boot_summary", handle_wtrace_boot_summary },
|
||
{ "wtrace_boot_stats", handle_wtrace_boot_stats },
|
||
{ "wtrace_boot_reset", handle_wtrace_boot_clear },
|
||
/* Always-on catch-all wtrace ring (parity with psx-beetle). */
|
||
{ "wtrace_all_dump", handle_wtrace_all_dump },
|
||
{ "wtrace_all_stats", handle_wtrace_all_stats },
|
||
{ "wtrace_all_reset", handle_wtrace_all_reset },
|
||
{ "wtrace_trans_dump", handle_wtrace_trans_dump },
|
||
{ "wtrace_trans_stats", handle_wtrace_trans_stats },
|
||
{ "wtrace_trans_reset", handle_wtrace_trans_reset },
|
||
{ "call_focus_dump", handle_call_focus_dump },
|
||
{ "call_focus_stats", handle_call_focus_stats },
|
||
{ "call_focus_reset", handle_call_focus_reset },
|
||
/* Legacy verbs retained for existing tools/ scripts that haven't
|
||
* been updated; they dispatch to the same handlers as the
|
||
* normalized verbs. Will retire once consumers are migrated. */
|
||
{ "wtrace_range", handle_wtrace_range },
|
||
{ "wtrace_add", handle_wtrace_add },
|
||
{ "wtrace_del", handle_wtrace_del },
|
||
{ "wtrace_clear", handle_wtrace_clear },
|
||
{ "freeze_check", handle_freeze_check },
|
||
{ "mmio_dump", handle_mmio_dump },
|
||
{ "mmio_clear", handle_mmio_clear },
|
||
{ "gp1_dump", handle_gp1_dump },
|
||
{ "mdec_state", handle_mdec_state },
|
||
{ "mdec_trace", handle_mdec_trace },
|
||
{ "mdec_trace_clear", handle_mdec_trace_clear },
|
||
{ "set_input", handle_set_input },
|
||
{ "press", handle_press },
|
||
{ "pad_status", handle_pad_status },
|
||
{ "clear_input", handle_clear_input },
|
||
{ "turbo", handle_turbo },
|
||
{ "turbo_state", handle_turbo_state },
|
||
{ "pause", handle_pause },
|
||
{ "continue", handle_continue },
|
||
{ "step", handle_step },
|
||
{ "run_to_frame", handle_run_to_frame },
|
||
{ "dirty_break_range", handle_dirty_break_range },
|
||
{ "dirty_break_clear", handle_dirty_break_clear },
|
||
{ "dirty_break_state", handle_dirty_break_state },
|
||
{ "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_file },
|
||
{ "screenshot_file", handle_screenshot_file }, /* alias */
|
||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||
{ "gpu_ring_stats", handle_gpu_ring_stats },
|
||
{ "gpu_frame_dump", handle_gpu_frame_dump },
|
||
{ "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_stats", handle_dispatch_stats },
|
||
{ "dispatch_check", handle_dispatch_check },
|
||
{ "dispatch_tail", handle_dispatch_tail },
|
||
{ "card_mgr_trace", handle_card_mgr_trace },
|
||
{ "card_mgr_clear", handle_card_mgr_clear },
|
||
{ "fn_filter", handle_fn_filter },
|
||
{ "fn_disable", handle_fn_disable },
|
||
{ "fn_clear", handle_fn_clear },
|
||
{ "fn_stats", handle_fn_stats },
|
||
{ "fn_entry_dump", handle_fn_entry_dump },
|
||
{ "fn_entry_tail", handle_fn_entry_tail },
|
||
{ "fn_exit_dump", handle_fn_exit_dump },
|
||
{ "overlay_dump", handle_overlay_dump },
|
||
{ "cd_read_log", handle_cd_read_log },
|
||
{ "overlay_loader_status", handle_overlay_loader_status },
|
||
{ "overlay_candidates", handle_overlay_candidates },
|
||
{ "overlay_native_ring", handle_overlay_native_ring },
|
||
{ "overlay_irq_suppress_on", handle_overlay_irq_suppress_on },
|
||
{ "overlay_irq_suppress_off", handle_overlay_irq_suppress_off },
|
||
{ "overlay_irq_ratelimit", handle_overlay_irq_ratelimit },
|
||
{ "overlay_native_event_granularity", handle_overlay_native_event_granularity },
|
||
{ "event_ring_dump", handle_event_ring_dump },
|
||
{ "event_ring_tail", handle_event_ring_tail },
|
||
{ "event_ring_clear", handle_event_ring_clear },
|
||
{ "overlay_diff_on", handle_overlay_diff_on },
|
||
{ "overlay_diff_off", handle_overlay_diff_off },
|
||
{ "overlay_shadow_dump", handle_overlay_shadow_dump },
|
||
{ "overlay_shadow_detail", handle_overlay_shadow_detail },
|
||
{ "overlay_fp_dump", handle_overlay_fp_dump },
|
||
{ "dirty_insn_gate", handle_dirty_insn_gate },
|
||
{ "insn_freeze", handle_insn_freeze },
|
||
{ "insn_freeze_status", handle_insn_freeze_status },
|
||
{ "overlay_native_on", handle_overlay_native_on },
|
||
{ "overlay_native_off", handle_overlay_native_off },
|
||
{ "overlay_capture_dump", handle_overlay_capture_dump },
|
||
{ "cdrom_instant_rate", handle_cdrom_instant_rate },
|
||
{ "cdrom_bursts", handle_cdrom_bursts },
|
||
{ "turbo_loads", handle_turbo_loads },
|
||
{ "autocompile_status", handle_autocompile_status },
|
||
{ "autocompile_run", handle_autocompile_run },
|
||
{ "overlay_rescan", handle_overlay_rescan },
|
||
{ 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;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* Backlog 16, not 1: when main thread stalls (the very state we want
|
||
* to probe during a freeze), backlog=1 means the first pending connect
|
||
* fills the queue and every subsequent probe gets RST/ConnectionRefused.
|
||
* 16 leaves room for a few probes to queue while we investigate. This
|
||
* is observability infrastructure, not a freeze fix. */
|
||
listen(s_listen, 16);
|
||
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;
|
||
|
||
if (!s_wtrace_boot) {
|
||
s_wtrace_boot =
|
||
(WriteTraceEntry *)calloc(WRITE_TRACE_BOOT_CAP, sizeof(WriteTraceEntry));
|
||
}
|
||
s_wtrace_boot_total = 0;
|
||
s_wtrace_boot_count = 0;
|
||
s_wtrace_boot_range_count = 0;
|
||
|
||
/* Always-on catch-all wtrace ring (8 MB). Records EVERY RAM write
|
||
* with lean fields (no register window). Sized for ~1 second of
|
||
* coverage at typical Tomba write rates. */
|
||
if (!s_wtrace_all) {
|
||
s_wtrace_all = (WriteTraceAllEntry *)calloc(WRITE_TRACE_ALL_CAP,
|
||
sizeof(WriteTraceAllEntry));
|
||
}
|
||
s_wtrace_all_seq = 0;
|
||
s_wtrace_all_head = 0;
|
||
|
||
if (!s_wtrace_trans) {
|
||
s_wtrace_trans = (WriteTraceTransEntry *)calloc(WRITE_TRACE_TRANS_CAP,
|
||
sizeof(WriteTraceTransEntry));
|
||
}
|
||
s_wtrace_trans_seq = 0;
|
||
s_wtrace_trans_head = 0;
|
||
s_wtrace_trans_range_count = 0;
|
||
|
||
fntrace_arm_from_env("PSX_FNTRACE_ARM");
|
||
|
||
/* 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_EXIT_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;
|
||
s_fn_tail_calls = 0;
|
||
s_fn_prev_ra = 0;
|
||
s_fn_direct_seen = 0;
|
||
s_fn_direct_no_cpu = 0;
|
||
s_fn_direct_filtered = 0;
|
||
s_fn_trace_active = 0;
|
||
s_fn_trace_filter_lo = 0u;
|
||
s_fn_trace_filter_hi = 0xFFFFFFFFu;
|
||
fn_trace_filter_from_env("PSX_FN_FILTER");
|
||
|
||
if (!s_call_focus) {
|
||
s_call_focus = (CallFocusEntry *)calloc(CALL_FOCUS_CAP,
|
||
sizeof(CallFocusEntry));
|
||
}
|
||
s_call_focus_seq = 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;
|
||
/* 0x75C0 = SIO data-mode flag (set 1 by func_6380, cleared 0 by 0xBFC15EBC).
|
||
* 0x75C4 = current buffer pointer. The data-byte handler at RAM 0x641C
|
||
* exits early via beq v0,zero if [0x75C0]==0; tracking these tells us
|
||
* whether the install-stub data path is engaging at all. */
|
||
s_wtrace_ranges[10].lo = 0x000075C0u;
|
||
s_wtrace_ranges[10].hi = 0x000075C8u;
|
||
/* 0x74B8 = pad-poll gate: if 0, pad poll function at BFC144BC SKIPS pad
|
||
* polling. 0x74BC = card-running gate: if non-zero, calls 0xBFC14B00
|
||
* (outer card coordinator). Tracking both tells us whether the BIOS is
|
||
* attempting the gate-flip serialization that prevents pad/card races. */
|
||
s_wtrace_ranges[11].lo = 0x000074B8u;
|
||
s_wtrace_ranges[11].hi = 0x000074C0u;
|
||
/* 0x72F0 = data-byte counter (halfword). Install handler at RAM 0x641C
|
||
* increments this per SIO IRQ during data phase; should reach 128 for a
|
||
* full sector read. Handoff says it stops at ~16. Track every write
|
||
* (including any abort-clear) to identify the byte-count regression PC. */
|
||
s_wtrace_ranges[12].lo = 0x000072F0u;
|
||
s_wtrace_ranges[12].hi = 0x000072F4u;
|
||
/* Card op result flags (kernel page 0xA000B9D0): B9D0 = success flag (set by
|
||
* SIO IRQ chain handler on op success); B9D4..B9E0 = error flags (timeout,
|
||
* checksum mismatch, etc.). FUN_bfc09144() in card_read returns the value
|
||
* at B9D0 — sector-≥1 read bail at FUN_bfc08b3c gates on this. Capture
|
||
* every writer to determine which writer fires for sector ≥1 (success vs
|
||
* error path) and which PC sets the failure flag. */
|
||
s_wtrace_ranges[13].lo = 0x0000B9D0u;
|
||
s_wtrace_ranges[13].hi = 0x0000B9F0u;
|
||
/* Shell COPY/DELETE/LOAD_DIR sub-state at 0x80066BC0. CIRCLE→7=LOAD_DIR,
|
||
* CROSS→4=COPY-related, TRIANGLE→0=cleared, 6=DELETE. After clicking
|
||
* COPY, BIOS got stuck at BC0=4 with NO further input transitions —
|
||
* likely waiting for a card op completion that never fires. Track every
|
||
* writer to identify the BIOS function driving this sub-state. */
|
||
s_wtrace_ranges[14].lo = 0x00066BC0u;
|
||
s_wtrace_ranges[14].hi = 0x00066BD0u;
|
||
s_wtrace_ranges[15].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_ranges[15].hi = 0x00097430u;
|
||
s_wtrace_range_count = 16;
|
||
|
||
s_wtrace_boot_ranges[0].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_boot_ranges[0].hi = 0x00097430u;
|
||
s_wtrace_boot_range_count = 1;
|
||
|
||
s_wtrace_trans_ranges[0].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_trans_ranges[0].hi = 0x00097430u;
|
||
s_wtrace_trans_range_count = 1;
|
||
|
||
#if DEFAULT_DEBUG_PORT == 4470
|
||
/* Tomba STR/FMVs: movie state, CD sector descriptor ring, and CD globals.
|
||
* These are passive traces for the game runtime only. */
|
||
s_wtrace_ranges[15].lo = 0x000D7188u;
|
||
s_wtrace_ranges[15].hi = 0x000D7588u;
|
||
s_wtrace_ranges[16].lo = 0x0009B010u;
|
||
s_wtrace_ranges[16].hi = 0x0009B050u;
|
||
s_wtrace_ranges[17].lo = 0x000A15C8u;
|
||
s_wtrace_ranges[17].hi = 0x000A3270u;
|
||
/* Tomba task scheduler descriptors and scratchpad state. The current
|
||
* black-screen blocker is a missing resume after the loader task closes,
|
||
* so keep the task table and scheduler scratch bytes in the reverse trace
|
||
* from process start. */
|
||
s_wtrace_ranges[18].lo = 0x001FD800u;
|
||
s_wtrace_ranges[18].hi = 0x001FD950u;
|
||
s_wtrace_ranges[19].lo = 0x1F8001CCu;
|
||
s_wtrace_ranges[19].hi = 0x1F800200u;
|
||
s_wtrace_ranges[20].lo = 0x1F800150u;
|
||
s_wtrace_ranges[20].hi = 0x1F800180u;
|
||
/* Runtime-loaded Tomba overlay text/data and the high-stack descriptors
|
||
* used by its primitive-list builders. This catches loader writes and any
|
||
* later self-modification before the frame-2500 overlay loop floods the
|
||
* trace. */
|
||
s_wtrace_ranges[21].lo = 0x000E0000u;
|
||
s_wtrace_ranges[21].hi = 0x000F0000u;
|
||
s_wtrace_ranges[22].lo = 0x001FE000u;
|
||
s_wtrace_ranges[22].hi = 0x001FE400u;
|
||
/* BIOS card-operation public state arrays. The Tomba Load menu waits on
|
||
* F4000001 events after _card_read; these cells reveal which BIOS path
|
||
* leaves the operation armed or clears it and delivers the public event. */
|
||
s_wtrace_ranges[23].lo = 0x00009F20u;
|
||
s_wtrace_ranges[23].hi = 0x00009F38u;
|
||
/* Tomba pad-poll buffer (game state +0x30 -> 0x8009EB58, second buffer at
|
||
* 0x8009EB7A). Display thread state machine gates on (*0x1F8001FC & 0x4008)
|
||
* which derives from `func_80028D70(0)` reading this buffer. Capture every
|
||
* write so we can attribute who initialises the layout — BIOS PadInit,
|
||
* runtime SIO, game-side memcpy, or none of the above. */
|
||
s_wtrace_ranges[24].lo = 0x0009EB40u;
|
||
s_wtrace_ranges[24].hi = 0x0009EB80u;
|
||
/* The gflag word the display thread polls (newly-set bits). */
|
||
s_wtrace_ranges[25].lo = 0x0009C9D0u;
|
||
s_wtrace_ranges[25].hi = 0x0009C9E0u;
|
||
/* OPTIONS/New Game render-state objects. The manager initializes two
|
||
* 0xF0-byte objects at 0x8009B3A0 and 0x8009B490. Gate func 0x8006B494
|
||
* reads object+0xE6/object+0x46, and func 0x8006A0C0 initializes the
|
||
* dispatch callbacks at object+0x14/+0x18. Keep both objects always-on
|
||
* from boot so the writers survive long menu/FMV runs. */
|
||
s_wtrace_ranges[26].lo = 0x0009B300u;
|
||
s_wtrace_ranges[26].hi = 0x0009B700u;
|
||
/* Function pointer globals used by func_8006A0C0 before it decides whether
|
||
* to initialize the object. */
|
||
s_wtrace_ranges[27].lo = 0x00097520u;
|
||
s_wtrace_ranges[27].hi = 0x00097538u;
|
||
/* DRAWENV/GPU command queue globals. At the black-screen state,
|
||
* 0x80090CAC continues to be copied every frame while queue indices
|
||
* 0x80090DA0/0x80090DA4 stop advancing. Keep register context for the
|
||
* copy/enqueue writers so we can identify the caller-supplied env. */
|
||
s_wtrace_ranges[28].lo = 0x00090C80u;
|
||
s_wtrace_ranges[28].hi = 0x00090DE0u;
|
||
s_wtrace_ranges[29].lo = 0x000B3200u; /* title/load GPU packet buffer */
|
||
s_wtrace_ranges[29].hi = 0x000B3800u;
|
||
s_wtrace_ranges[30].lo = 0x000EA000u; /* BIOS licensed-screen GPU packet buffer */
|
||
s_wtrace_ranges[30].hi = 0x000ED000u;
|
||
s_wtrace_ranges[31].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_ranges[31].hi = 0x00097430u;
|
||
s_wtrace_range_count = 32;
|
||
|
||
s_wtrace_boot_ranges[0].lo = 0x0009B3B0u; /* slot0 callbacks */
|
||
s_wtrace_boot_ranges[0].hi = 0x0009B3C4u;
|
||
s_wtrace_boot_ranges[1].lo = 0x0009B3E0u; /* slot0 state bytes */
|
||
s_wtrace_boot_ranges[1].hi = 0x0009B3F0u;
|
||
s_wtrace_boot_ranges[2].lo = 0x0009B480u; /* slot0 gate/status */
|
||
s_wtrace_boot_ranges[2].hi = 0x0009B490u;
|
||
s_wtrace_boot_ranges[3].lo = 0x0009B4A0u; /* slot1 callbacks */
|
||
s_wtrace_boot_ranges[3].hi = 0x0009B4B4u;
|
||
s_wtrace_boot_ranges[4].lo = 0x0009B4D0u; /* slot1 state bytes */
|
||
s_wtrace_boot_ranges[4].hi = 0x0009B4E0u;
|
||
s_wtrace_boot_ranges[5].lo = 0x0009B570u; /* slot1 gate/status */
|
||
s_wtrace_boot_ranges[5].hi = 0x0009B580u;
|
||
s_wtrace_boot_ranges[6].lo = 0x00097520u; /* manager callbacks */
|
||
s_wtrace_boot_ranges[6].hi = 0x00097538u;
|
||
s_wtrace_boot_ranges[7].lo = 0x0000E1F4u; /* BIOS TCB save areas */
|
||
s_wtrace_boot_ranges[7].hi = 0x0000E400u;
|
||
s_wtrace_boot_ranges[8].lo = 0x0009C970u; /* title/menu state variables */
|
||
s_wtrace_boot_ranges[8].hi = 0x0009C9A0u;
|
||
s_wtrace_boot_ranges[9].lo = 0x000B3200u; /* title/load GPU packet buffer */
|
||
s_wtrace_boot_ranges[9].hi = 0x000B3800u;
|
||
s_wtrace_boot_ranges[10].lo = 0x000EA000u; /* BIOS licensed-screen GPU packet buffer */
|
||
s_wtrace_boot_ranges[10].hi = 0x000ED000u;
|
||
s_wtrace_boot_ranges[11].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_boot_ranges[11].hi = 0x00097430u;
|
||
s_wtrace_boot_range_count = 12;
|
||
|
||
s_wtrace_trans_ranges[0].lo = 0x0000E1F4u; /* BIOS TCB save areas */
|
||
s_wtrace_trans_ranges[0].hi = 0x0000E400u;
|
||
s_wtrace_trans_ranges[1].lo = 0x001FD800u; /* Tomba task table */
|
||
s_wtrace_trans_ranges[1].hi = 0x001FD950u;
|
||
s_wtrace_trans_ranges[2].lo = 0x1F8001CCu; /* scheduler scratch */
|
||
s_wtrace_trans_ranges[2].hi = 0x1F800200u;
|
||
s_wtrace_trans_ranges[3].lo = 0x0009B300u; /* render/menu state objects */
|
||
s_wtrace_trans_ranges[3].hi = 0x0009B700u;
|
||
s_wtrace_trans_ranges[4].lo = 0x00097520u; /* manager callbacks */
|
||
s_wtrace_trans_ranges[4].hi = 0x00097538u;
|
||
s_wtrace_trans_ranges[5].lo = 0x00090C80u; /* DRAWENV/GPU queue globals */
|
||
s_wtrace_trans_ranges[5].hi = 0x00090DE0u;
|
||
s_wtrace_trans_ranges[6].lo = 0x0009EB40u; /* pad buffers */
|
||
s_wtrace_trans_ranges[6].hi = 0x0009EB80u;
|
||
s_wtrace_trans_ranges[7].lo = 0x0009C9D0u; /* display gflag word */
|
||
s_wtrace_trans_ranges[7].hi = 0x0009C9E0u;
|
||
s_wtrace_trans_ranges[8].lo = 0x0009C970u; /* title/menu state variables */
|
||
s_wtrace_trans_ranges[8].hi = 0x0009C9A0u;
|
||
s_wtrace_trans_ranges[9].lo = 0x00097420u; /* movie/frame handoff state */
|
||
s_wtrace_trans_ranges[9].hi = 0x00097430u;
|
||
s_wtrace_trans_range_count = 10;
|
||
#endif
|
||
|
||
/* 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;
|
||
|
||
/* Dedicated GP1 display-control ring (long retention). */
|
||
if (!s_gp1_trace) {
|
||
s_gp1_trace = (MmioTraceEntry *)calloc(GP1_TRACE_CAP, sizeof(MmioTraceEntry));
|
||
}
|
||
s_gp1_trace_seq = 0;
|
||
s_gp1_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)
|
||
{
|
||
/* Phase 1.0e-e2 starvation watchdog heartbeat. Refreshes the
|
||
* "last poll wall-clock" timestamp; if too much time passes
|
||
* between calls, the ring is dumped and the runtime aborts. */
|
||
extern void starvation_watchdog_heartbeat(void);
|
||
starvation_watchdog_heartbeat();
|
||
|
||
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;
|
||
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;
|
||
/* Don't reset s_input_override here. A scheduled press
|
||
* (s_input_frames > 0) must continue to hold across the
|
||
* client disconnect — Python clients open a fresh socket
|
||
* per command, so clearing on every connection close
|
||
* would truncate every press to a 1-frame edge. */
|
||
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 / run_to_frame post-frame hooks: removed with the rest of
|
||
* pause/step machinery. s_step_count and s_run_to stay at zero. */
|
||
if (0) {
|
||
send_fmt("{\"event\":\"unreachable\",\"frame\":%llu}",
|
||
(unsigned long long)(s_frame_count - 1));
|
||
}
|
||
}
|
||
|
||
void debug_server_wait_if_paused(void)
|
||
{
|
||
/* No-op: pause/step removed (see handle_pause). Kept exported so
|
||
* main.cpp's vblank callback continues to compile without
|
||
* conditional defines. s_paused is now permanently zero, so the
|
||
* old `while (s_paused)` loop would have been a no-op anyway. */
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
int debug_server_turbo_enabled(void)
|
||
{
|
||
return s_turbo_enabled != 0;
|
||
}
|