From ca6f423edbaaee2ec80f705945c33891ecfbdca3 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 12 Jun 2026 05:19:17 -0700 Subject: [PATCH] debug: gl_coh_ring/gl_fbo_peek/gl_vram_diff commands; richer SEH dumps 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 --- runtime/src/crash_trace.c | 53 +++++++++++++++- runtime/src/debug_server.c | 124 +++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 66038af9..fc9a062f 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -149,9 +149,58 @@ void psx_crash_trace_dump(const char *reason, void *seh_info) { " \"code\": \"0x%08lX\",\n" " \"address\": \"%p\",\n" " \"access\": \"%s\",\n" - " \"fault_addr\": \"0x%p\"\n" - " },\n", + " \"fault_addr\": \"0x%p\",\n", code, addr, kind, (void *)fault_addr); + + /* Module-relative location of the faulting instruction, so the + * address survives ASLR and feeds straight into addr2line. */ + { + MEMORY_BASIC_INFORMATION mbi; + char mod_name[MAX_PATH] = "?"; + void *mod_base = NULL; + if (VirtualQuery(addr, &mbi, sizeof(mbi))) { + mod_base = mbi.AllocationBase; + GetModuleFileNameA((HMODULE)mod_base, mod_name, sizeof(mod_name)); + } + append_fmt(buf, sizeof(buf), &pos, + " \"module\": \"%s\",\n" + " \"module_base\": \"%p\",\n" + " \"module_offset\": \"0x%llX\",\n", + mod_name, mod_base, + (unsigned long long)((char *)addr - (char *)mod_base)); + } + + /* Poor-man's backtrace: scan the faulting thread's stack for values + * that point into executable module regions and report them + * module-relative. Noisy but enough to pin the faulting call path. */ + append_str(buf, sizeof(buf), &pos, " \"stack_scan\": ["); + { + CONTEXT *ctx = info->ContextRecord; + ULONG_PTR *sp = (ULONG_PTR *)ctx->Rsp; + int emitted = 0; + for (int i = 0; i < 512 && emitted < 24; i++) { + ULONG_PTR v; + MEMORY_BASIC_INFORMATION mbi; + if (!VirtualQuery(&sp[i], &mbi, sizeof(mbi)) || + !(mbi.State & MEM_COMMIT)) break; + v = sp[i]; + if (v < 0x10000) continue; + if (!VirtualQuery((void *)v, &mbi, sizeof(mbi))) continue; + if (mbi.Type != MEM_IMAGE) continue; + if (!(mbi.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | + PAGE_EXECUTE_READWRITE))) continue; + char mod_name[MAX_PATH] = "?"; + GetModuleFileNameA((HMODULE)mbi.AllocationBase, mod_name, + sizeof(mod_name)); + const char *base = strrchr(mod_name, '\\'); + append_fmt(buf, sizeof(buf), &pos, + "%s\n {\"module\": \"%s\", \"offset\": \"0x%llX\"}", + emitted ? "," : "", base ? base + 1 : mod_name, + (unsigned long long)(v - (ULONG_PTR)mbi.AllocationBase)); + emitted++; + } + } + append_str(buf, sizeof(buf), &pos, "\n ]\n },\n"); } #else (void)seh_info; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index a5127cd3..360be2f7 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -7,6 +7,12 @@ * 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 #include "debug_server.h" #include "overlay_loader.h" #include "overlay_capture.h" @@ -25,6 +31,7 @@ #include "card_read_summary.h" #include "card_data_writes.h" #include "crash_trace.h" +#include "gpu_gl_renderer.h" #include #include @@ -48,6 +55,7 @@ #else # include # include +# include /* struct timeval — socket send/recv timeouts */ # include # include # include @@ -5720,6 +5728,119 @@ static void handle_vram_peek(int id, const char *json) 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; @@ -8305,6 +8426,9 @@ static const CmdEntry s_commands[] = { { "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 },