diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 473d7c52..53aec15b 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -48,6 +48,27 @@ typedef struct { void gpu_arm_shaded_quad_capture(void); int gpu_get_shaded_quad_capture(const GpuSqCapEntry** out); +/* Per-frame GP0 command ring (always-on; query via debug server). + * Each entry records the GP0 command header + up to 6 payload words + * (longer commands like 0x3C shaded textured quad are truncated to 6). + * Stamped with the s_frame_count value at issue time so a debug + * client can pull all commands for any frame in the ring window. */ +#define GPU_GP0_RING_MAX_WORDS 6 +typedef struct { + uint32_t frame; + uint32_t seq; + uint8_t opcode; + uint8_t n_words; /* total command length; >MAX means truncated */ + uint16_t pad; + uint32_t cmd[GPU_GP0_RING_MAX_WORDS]; +} GpuGp0RingEntry; + +uint64_t gpu_gp0_ring_total(void); +uint32_t gpu_gp0_ring_capacity(void); +uint32_t gpu_gp0_ring_max_words(void); +int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out); +void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest); + /* Vblank presentation callback — called from gpu_vblank_tick(). */ typedef void (*gpu_vblank_cb)(void); void gpu_set_vblank_callback(gpu_vblank_cb cb); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 623169f6..5c47bb44 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2711,6 +2711,62 @@ static void handle_gpu_opcodes(int id, const char *json) 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); + + /* ~140 bytes per entry in JSON; budget conservatively. */ + size_t buf_sz = 256 + (size_t)n * 200u; + 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,\"w\":[", + i ? "," : "", e->seq, e->opcode, e->n_words); + 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; @@ -5217,6 +5273,8 @@ static const CmdEntry s_commands[] = { { "screenshot", handle_screenshot }, { "screenshot_file", handle_screenshot_file }, { "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 }, diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b8a4f1c2..14b95d21 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1180,10 +1180,82 @@ static uint32_t gp0_opcode_count[256]; uint32_t gpu_get_opcode_count(uint8_t op) { return gp0_opcode_count[op]; } +/* ---- Per-frame GP0 command ring (always-on, queried via debug server) ---- */ +/* We record every GP0 command (header + up to 6 payload words) with the + * frame number it was issued in. Per CLAUDE.md ring-buffer rule: capture + * is continuous and observers query a window of interest later, not arm- + * then-record. ~34 MB at 1M entries. Polyline / long commands get the + * first 6 payload words; that's enough for the header + first vertex pair + * + first uv/color word for diagnosing per-primitive state. */ + +extern uint64_t s_frame_count; /* defined in debug_server.c */ + +#define GP0_RING_CAP (1u << 20) /* 1 048 576 entries */ +/* GpuGp0RingEntry + GPU_GP0_RING_MAX_WORDS are public types in gpu.h. */ + +static GpuGp0RingEntry *gp0_ring = NULL; /* lazy-alloc on first record */ +static uint64_t gp0_ring_seq = 0; /* total commands recorded */ +static uint32_t gp0_ring_head = 0; /* next write slot */ + +static void gp0_ring_record(const uint32_t *words, int n) { + if (!gp0_ring) { + gp0_ring = (GpuGp0RingEntry *)calloc(GP0_RING_CAP, sizeof(*gp0_ring)); + if (!gp0_ring) return; /* OOM — capture disabled, no other effect */ + } + GpuGp0RingEntry *e = &gp0_ring[gp0_ring_head]; + e->frame = (uint32_t)s_frame_count; + e->seq = (uint32_t)gp0_ring_seq; + e->opcode = (uint8_t)((words[0] >> 24) & 0xFF); + e->n_words = (uint8_t)(n > 255 ? 255 : (n < 0 ? 1 : n)); + e->pad = 0; + int copy_n = e->n_words > GPU_GP0_RING_MAX_WORDS ? GPU_GP0_RING_MAX_WORDS : e->n_words; + for (int i = 0; i < copy_n; i++) e->cmd[i] = words[i]; + for (int i = copy_n; i < GPU_GP0_RING_MAX_WORDS; i++) e->cmd[i] = 0; + gp0_ring_head = (gp0_ring_head + 1) % GP0_RING_CAP; + gp0_ring_seq++; +} + +/* Public accessors for debug_server.c */ +uint64_t gpu_gp0_ring_total(void) { return gp0_ring_seq; } +uint32_t gpu_gp0_ring_capacity(void) { return GP0_RING_CAP; } +uint32_t gpu_gp0_ring_max_words(void){ return GPU_GP0_RING_MAX_WORDS; } + +/* Fill `out[0..max_out-1]` with entries from the requested frame; returns + * count. Walks from oldest in-buffer to newest so iteration order matches + * draw order within a frame. */ +int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out) { + if (!gp0_ring || max_out <= 0) return 0; + uint32_t avail = (gp0_ring_seq < GP0_RING_CAP) + ? (uint32_t)gp0_ring_seq : GP0_RING_CAP; + uint32_t start = (gp0_ring_seq < GP0_RING_CAP) ? 0 : gp0_ring_head; + int n_out = 0; + for (uint32_t i = 0; i < avail && n_out < max_out; i++) { + uint32_t idx = (start + i) % GP0_RING_CAP; + if (gp0_ring[idx].frame == frame) { + out[n_out++] = gp0_ring[idx]; + } + } + return n_out; +} + +/* Report the frame range currently held in the ring (oldest..newest). */ +void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) { + if (out_oldest) *out_oldest = 0; + if (out_newest) *out_newest = 0; + if (!gp0_ring || gp0_ring_seq == 0) return; + uint32_t avail = (gp0_ring_seq < GP0_RING_CAP) + ? (uint32_t)gp0_ring_seq : GP0_RING_CAP; + uint32_t start = (gp0_ring_seq < GP0_RING_CAP) ? 0 : gp0_ring_head; + uint32_t newest_idx = (start + avail - 1) % GP0_RING_CAP; + if (out_oldest) *out_oldest = gp0_ring[start].frame; + if (out_newest) *out_newest = gp0_ring[newest_idx].frame; +} + /* Execute a fully-collected GP0 command */ static void gp0_execute_command(void) { uint8_t opcode = (gp0_cmd_buf[0] >> 24) & 0xFF; gp0_opcode_count[opcode]++; + gp0_ring_record(gp0_cmd_buf, gp0_words_needed); /* Categorize for diagnostics */ if (opcode <= 0x01) gp0_nop_count++; @@ -1538,6 +1610,11 @@ void gpu_write_gp0(uint32_t val) { sw_set_semi_transparency(polyline_semi_trans, (int)semi_transparency); gp0_state = shaded ? GP0_POLYLINE_SHADED : GP0_POLYLINE_MONO; gp0_draw_count++; + /* Record polyline header (variable-length body not captured; + * just enough so per-frame stream shows the polyline existed). */ + gp0_opcode_count[opcode]++; + uint32_t hdr_only[1] = { val }; + gp0_ring_record(hdr_only, 1); return; }