diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 28774837..a51f37bb 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -65,6 +65,9 @@ Columns: **N** = native, **D** = DuckStation oracle. | `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | | `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | | `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | +| `irq_exc_stats` | ✓ | | — | Cumulative per-source exception accounting since boot: deliveries and guest cycles from entry to exit, charged to every `I_STAT & I_MASK` bit pending at entry (`sw` = software interrupt only). Diff two snapshots for a window. `irqctx_ring` entries carry the matching `exit_cycle` | +| `dma_hold_stats` | ✓ | | — | Cumulative CPU holds for kicked GPU linked-list walks (the kicking store returns only when the walk ends): count, guest `held_cycles`, host `host_us`, words, advance steps, and the `exc_*` share for kicks made inside an exception handler (that host time is what `phase_profile` reports as `exc_share`) | +| `dma_hold_ring` | ✓ | | `count`, `frame_lo`, `frame_hi` | Newest held kicks (ring of 4096): cycle, frame, held cycles, words, steps, host µs, kick PC, MADR, `in_exc`, `still_active` | | `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | | `overlay_state` | | ✓ | — | Current overlay info | | `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | @@ -431,9 +434,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i ## Complete command index (generated) -**345 commands registered** — 332 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**348 commands registered** — 335 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -63 of 345 have prose above; **282 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. +68 of 348 have prose above; **280 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. @@ -517,6 +520,8 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `display_ring_get` | ✓ | | | | `display_ring_stats` | ✓ | | | | `dma_cdrom_history` | ✓ | | | +| `dma_hold_ring` | ✓ | | ✓ | +| `dma_hold_stats` | ✓ | | ✓ | | `dma_state` | ✓ | | ✓ | | `dma_trace_clear` | ✓ | | | | `dma_trace_dump` | ✓ | | | @@ -592,8 +597,9 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `insn_freeze_snapshot` | ✓ | | | | `insn_freeze_status` | ✓ | | | | `insn_freeze_target` | ✓ | | | +| `irq_exc_stats` | ✓ | | ✓ | | `irq_state` | ✓ | | ✓ | -| `irqctx_ring` | ✓ | | | +| `irqctx_ring` | ✓ | | ✓ | | `kernel_bless` | ✓ | | | | `latency` | ✓ | | | | `load_transitions` | ✓ | | | @@ -649,7 +655,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `pgxp_miss_ring` | ✓ | | | | `pgxp_shadow` | ✓ | | | | `phase_hot` | ✓ | | | -| `phase_profile` | ✓ | | | +| `phase_profile` | ✓ | | ✓ | | `ping` | ✓ | ✓ | ✓ | | `present_image_ring_get` | ✓ | | | | `present_image_ring_stats` | ✓ | | | diff --git a/runtime/include/dma.h b/runtime/include/dma.h index a681de9f..d2f189cc 100644 --- a/runtime/include/dma.h +++ b/runtime/include/dma.h @@ -168,6 +168,30 @@ void dma_debug_get_gpu_ot_stats(DMAGpuOtStats* out); uint64_t dma_debug_get_cdrom_history(const DMACDROMHistoryEntry** out_entries); void dma_debug_clear_cdrom_history(void); +/* Always-on record of every CPU hold for a kicked GPU linked-list walk + * (the kicking store returns only when the walk ends). Ring is the newest + * DMA_GPU_HOLD_RING_CAP holds; stats are cumulative since boot. */ +#define DMA_GPU_HOLD_RING_CAP 4096u +typedef struct { + uint64_t seq; + uint64_t cycle; /* guest cycle at the kick */ + uint32_t held_cycles; /* guest cycles the CPU was held */ + uint32_t words; /* list words the walk moved during the hold */ + uint32_t steps; /* advance+service iterations the hold ran */ + uint32_t host_us; /* host wall time spent inside the hold */ + uint32_t frame; + uint32_t kick_pc; /* guest store PC that kicked the walk */ + uint32_t madr; + uint8_t in_exception; /* kick came from inside an exception handler */ + uint8_t still_active; /* hold ended with the walk still running */ +} DMAGpuHoldEntry; +typedef struct { + uint64_t count, held_cycles, host_us, words, steps; + uint64_t exc_count, exc_held_cycles, exc_host_us; +} DMAGpuHoldStats; +void dma_debug_get_gpu_hold(DMAGpuHoldStats *out_stats, + const DMAGpuHoldEntry **out_ring); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 95a3a5b0..b5c2ffcc 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -6371,6 +6371,8 @@ static void handle_a0_history(int id, const char *json) } extern int gpu_get_c0_count(void); +extern uint64_t gpu_get_c0_total(void); +extern uint32_t gpu_get_c0_frame(int index); 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); @@ -6379,17 +6381,18 @@ 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); + char buf[65536]; + int pos = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"count\":%d,\"total\":%llu,\"reads\":[", + id, count, (unsigned long long)gpu_get_c0_total()); 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," + "%s{\"i\":%d,\"frame\":%u,\"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); + i ? "," : "", i, gpu_get_c0_frame(i), x, y, w, h, func, sp, s1, fw0, fw1, rcount); } pos += snprintf(buf + pos, sizeof(buf) - pos, "]}"); send_fmt("%s", buf); @@ -11016,13 +11019,103 @@ static void handle_d44_ring(int id, const char *json) * callback word [0x80079D44], CD-DMA-active + dma-depth, and COP0/IRQ state. Shows * whether VBlank was delivered while 0x80079D44 was the clobbered 0x016F0110 AND a * CD DMA was mid-transfer (the VSync-in-DMA-window bug). */ +/* irq_exc_stats: cumulative per-source exception accounting since boot + * (interrupts.c). Each delivery is charged to every I_STAT&I_MASK bit pending + * at entry ("sw" = software interrupt only), with the guest cycles from entry + * to the exit restore decision. Diff two snapshots for a window. */ +static void handle_irq_exc_stats(int id, const char *json) +{ + (void)json; + extern uint64_t g_exc_src_deliveries[12], g_exc_src_cycles[12]; + extern uint64_t g_exc_cycles_total, g_exc_exits_total, g_irqctx_seq; + static const char *names[12] = { "vblank", "gpu", "cdrom", "dma", "tmr0", + "tmr1", "tmr2", "sio0", "sio1", "spu", "lightpen", "sw" }; + char buf[2048]; + size_t pos = 0; + pos += snprintf(buf + pos, sizeof(buf) - pos, + "{\"id\":%d,\"ok\":true,\"cycle\":%llu,\"frame\":%llu," + "\"deliveries\":%llu,\"exits\":%llu,\"exc_cycles\":%llu,\"src\":{", + id, (unsigned long long)psx_get_cycle_count(), + (unsigned long long)s_frame_count, + (unsigned long long)g_irqctx_seq, + (unsigned long long)g_exc_exits_total, + (unsigned long long)g_exc_cycles_total); + for (int b = 0; b < 12; b++) + pos += snprintf(buf + pos, sizeof(buf) - pos, + "%s\"%s\":{\"n\":%llu,\"cyc\":%llu}", b ? "," : "", names[b], + (unsigned long long)g_exc_src_deliveries[b], + (unsigned long long)g_exc_src_cycles[b]); + snprintf(buf + pos, sizeof(buf) - pos, "}}"); + debug_server_send_line(buf); +} + +/* dma_hold_stats: cumulative totals for CPU holds on kicked GPU linked-list + * walks (dma.c), split by kicks made inside an exception handler. */ +static void handle_dma_hold_stats(int id, const char *json) +{ + (void)json; + DMAGpuHoldStats st; + dma_debug_get_gpu_hold(&st, NULL); + send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%llu,\"count\":%llu,\"held_cycles\":%llu," + "\"host_us\":%llu,\"words\":%llu,\"steps\":%llu,\"exc_count\":%llu," + "\"exc_held_cycles\":%llu,\"exc_host_us\":%llu}", + id, (unsigned long long)s_frame_count, + (unsigned long long)st.count, (unsigned long long)st.held_cycles, + (unsigned long long)st.host_us, (unsigned long long)st.words, + (unsigned long long)st.steps, (unsigned long long)st.exc_count, + (unsigned long long)st.exc_held_cycles, (unsigned long long)st.exc_host_us); +} + +/* dma_hold_ring: newest `count` held kicks (default 256, max ring cap), + * optionally windowed by frame_lo/frame_hi. */ +static void handle_dma_hold_ring(int id, const char *json) +{ + int count = json_get_int(json, "count", 256); + int flo = json_get_int(json, "frame_lo", -1); + int fhi = json_get_int(json, "frame_hi", -1); + if (count < 1) count = 1; + if (count > (int)DMA_GPU_HOLD_RING_CAP) count = (int)DMA_GPU_HOLD_RING_CAP; + DMAGpuHoldStats st; + const DMAGpuHoldEntry *ring = NULL; + dma_debug_get_gpu_hold(&st, &ring); + uint64_t total = st.count; + uint32_t avail = total < DMA_GPU_HOLD_RING_CAP ? (uint32_t)total : DMA_GPU_HOLD_RING_CAP; + uint32_t n = (uint32_t)count < avail ? (uint32_t)count : avail; + size_t BUF_SZ = 256u + (size_t)n * 260u; + 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,\"entries\":[", + id, (unsigned long long)total); + for (uint32_t i = 0; i < n && pos < BUF_SZ - 260; i++) { + const DMAGpuHoldEntry *e = &ring[(total - n + i) & (DMA_GPU_HOLD_RING_CAP - 1u)]; + if (flo >= 0 && (int)e->frame < flo) continue; + if (fhi >= 0 && (int)e->frame > fhi) continue; + pos += snprintf(buf + pos, BUF_SZ - pos, + "%s{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"held_cycles\":%u," + "\"words\":%u,\"steps\":%u,\"host_us\":%u,\"kick_pc\":\"0x%08X\"," + "\"madr\":\"0x%08X\",\"in_exc\":%u,\"still_active\":%u}", + emitted ? "," : "", (unsigned long long)e->seq, + (unsigned long long)e->cycle, e->frame, e->held_cycles, e->words, + e->steps, e->host_us, e->kick_pc, e->madr, + (unsigned)e->in_exception, (unsigned)e->still_active); + emitted++; + } + pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted); + debug_server_send_line(buf); + free(buf); +} + static void handle_irqctx_ring(int id, const char *json) { typedef struct { uint64_t seq, cycle; uint32_t frame, istat, imask, sr, d44, cdrom_active, is_vblank; int dma_depth; uint32_t take_pc, real_epc, exit_pc, exit_reason, same_thread, restored, v0_exit, v0_saved, v1_exit, v1_saved, ra_exit, - ra_saved, redirects, entry_sp, pump_site; } E; + ra_saved, redirects, entry_sp, pump_site; + uint64_t exit_cycle; } E; /* must match IrqCtxEntry */ extern E g_irqctx_ring[]; extern uint64_t g_irqctx_seq; /* Ring cap must track IRQCTX_RING_CAP in interrupts.c. */ uint32_t cap = 4096u; @@ -11036,7 +11129,7 @@ static void handle_irqctx_ring(int id, const char *json) uint64_t total = g_irqctx_seq; uint32_t avail = total < cap ? (uint32_t)total : cap; uint32_t n = (uint32_t)count < avail ? (uint32_t)count : avail; - size_t BUF_SZ = 512u + (size_t)n * 410u; + size_t BUF_SZ = 512u + (size_t)n * 480u; char *buf = (char *)malloc(BUF_SZ); if (!buf) { send_err(id, "oom"); return; } size_t pos = 0; pos += snprintf(buf + pos, BUF_SZ - pos, @@ -11057,13 +11150,14 @@ static void handle_irqctx_ring(int id, const char *json) "\"v0_exit\":\"0x%08X\",\"v0_saved\":\"0x%08X\"," "\"v1_exit\":\"0x%08X\",\"v1_saved\":\"0x%08X\"," "\"ra_exit\":\"0x%08X\",\"ra_saved\":\"0x%08X\",\"redirects\":%u," - "\"entry_sp\":\"0x%08X\",\"pump_site\":%u}", + "\"entry_sp\":\"0x%08X\",\"pump_site\":%u,\"exit_cycle\":%llu}", emitted ? "," : "", (unsigned long long)e->seq, (unsigned long long)e->cycle, e->frame, e->is_vblank, e->d44, e->cdrom_active, e->dma_depth, e->sr, e->istat, e->imask, e->take_pc, e->real_epc, e->exit_pc, e->exit_reason, e->same_thread, e->restored, e->v0_exit, e->v0_saved, e->v1_exit, e->v1_saved, - e->ra_exit, e->ra_saved, e->redirects, e->entry_sp, e->pump_site); + e->ra_exit, e->ra_saved, e->redirects, e->entry_sp, e->pump_site, + (unsigned long long)e->exit_cycle); emitted++; } pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%d}", emitted); @@ -15083,6 +15177,9 @@ static const CmdEntry s_commands[] = { { "freeze_check", handle_freeze_check }, { "d44_ring", handle_d44_ring }, { "irqctx_ring", handle_irqctx_ring }, + { "irq_exc_stats", handle_irq_exc_stats }, + { "dma_hold_stats", handle_dma_hold_stats }, + { "dma_hold_ring", handle_dma_hold_ring }, { "sp_ring", handle_sp_ring }, { "disp_ring", handle_disp_ring }, { "cyc_watch", handle_cyc_watch }, diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 70ade5a9..4690a8f1 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -31,6 +31,16 @@ #include #include #include +#include +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include "gpu_timeline.h" /* Word-aligned main-RAM offset of a DMA address (MADR / stepped cursor) @@ -807,16 +817,78 @@ static void start_async_gpu_linked_list(void) { * its text packets from a template, clearing the link) cannot reach it before * the walk does. A cyclic list still ends at the walker's node limit. The hold * also ends if an advance moves no list word. */ +/* Always-on hold observability (dma_hold_stats / dma_hold_ring). Every held + * kick is recorded with its guest cost, its host wall time and whether it was + * made from inside an exception handler: a kick from an IRQ callback runs the + * whole walk (and the GPU work it feeds) with in_exception set, which is what + * the phase profiler's exc_share then measures. */ +extern int psx_get_in_exception(void); +static DMAGpuHoldEntry s_hold_ring[DMA_GPU_HOLD_RING_CAP]; +static DMAGpuHoldStats s_hold_stats; + +static uint64_t hold_host_ticks(void) { +#ifdef _WIN32 + LARGE_INTEGER c; QueryPerformanceCounter(&c); return (uint64_t)c.QuadPart; +#else + struct timespec ts; timespec_get(&ts, TIME_UTC); /* C11; diagnostics only */ + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; +#endif +} + +static uint64_t hold_host_ticks_per_sec(void) { +#ifdef _WIN32 + LARGE_INTEGER f; QueryPerformanceFrequency(&f); return (uint64_t)f.QuadPart; +#else + return 1000000000ull; +#endif +} + static void hold_cpu_for_gpu_linked_list(void) { if (psx_in_device_service || g_ls_replay_active || g_psx_render_pass_active) return; + const uint64_t cyc0 = psx_cycle_count; + const uint64_t host0 = hold_host_ticks(); + const uint32_t words0 = gpu_linked_list.total_words; + uint32_t steps = 0; while (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) && channel_enabled(2)) { const uint32_t moved = gpu_linked_list.total_words; psx_advance_cycles(dma_gpu_ll_cycles_to_event(&gpu_linked_list)); psx_devices_service_to_now(); + steps++; if (gpu_linked_list.active && gpu_linked_list.total_words == moved) break; } + const uint64_t host_us = (hold_host_ticks() - host0) * 1000000ull / + hold_host_ticks_per_sec(); + const int in_exc = psx_get_in_exception() != 0; + DMAGpuHoldEntry *e = &s_hold_ring[s_hold_stats.count & (DMA_GPU_HOLD_RING_CAP - 1u)]; + e->seq = s_hold_stats.count; + e->cycle = cyc0; + e->held_cycles = (uint32_t)(psx_cycle_count - cyc0); + e->words = gpu_linked_list.total_words - words0; + e->steps = steps; + e->host_us = (uint32_t)(host_us > 0xFFFFFFFFull ? 0xFFFFFFFFull : host_us); + e->frame = (uint32_t)s_frame_count; + e->kick_pc = g_debug_last_store_pc; + e->madr = channels[2].madr; + e->in_exception = (uint8_t)in_exc; + e->still_active = (uint8_t)(gpu_linked_list.active != 0); + s_hold_stats.count++; + s_hold_stats.held_cycles += e->held_cycles; + s_hold_stats.host_us += host_us; + s_hold_stats.words += e->words; + s_hold_stats.steps += steps; + if (in_exc) { + s_hold_stats.exc_count++; + s_hold_stats.exc_held_cycles += e->held_cycles; + s_hold_stats.exc_host_us += host_us; + } +} + +void dma_debug_get_gpu_hold(DMAGpuHoldStats *out_stats, + const DMAGpuHoldEntry **out_ring) { + if (out_stats) *out_stats = s_hold_stats; + if (out_ring) *out_ring = s_hold_ring; } static uint32_t execute_ch2_gpu(void) { diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 450b1f17..008555b5 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3484,15 +3484,18 @@ static uint32_t v_display_y2; /* GPUREAD latch (GP1(10h) get-info result, or VRAM read data) */ static uint32_t gpuread_latch; -/* C0 (VRAM→CPU) capture slot — forward declaration for gpu_read_gpuread */ -#define C0_HISTORY_CAP_FWD 32 +/* C0 (VRAM→CPU) history: an always-on ring of the most recent reads + * (it used to keep only the first 32 since boot, so gameplay readbacks were + * invisible). Forward declaration for gpu_read_gpuread. */ +#define C0_HISTORY_CAP_FWD 256 static struct C0HistEntry { uint16_t x, y, w, h; uint32_t func_addr, sp_val, s1_val; uint32_t first_words[4]; int read_count; + uint32_t frame; } c0_history_fwd[C0_HISTORY_CAP_FWD]; -static int c0_history_count_fwd = 0; +static uint64_t c0_history_count_fwd = 0; /* total reads since boot */ static int c0_capture_slot_fwd = -1; /* Vblank presentation callback */ @@ -5747,11 +5750,18 @@ static void gp0_exec_cpu_to_vram(void) { #define c0_history_count c0_history_count_fwd #define c0_capture_slot c0_capture_slot_fwd -int gpu_get_c0_count(void) { return c0_history_count; } +/* Total GP0 C0 reads since boot; the ring holds the newest C0_HISTORY_CAP. */ +uint64_t gpu_get_c0_total(void) { return c0_history_count; } +int gpu_get_c0_count(void) { + return c0_history_count < C0_HISTORY_CAP ? (int)c0_history_count : C0_HISTORY_CAP; +} +/* index 0 = oldest entry still in the ring. */ 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) { - if (index < 0 || index >= c0_history_count) return 0; + if (index < 0 || index >= gpu_get_c0_count()) return 0; + if (c0_history_count > C0_HISTORY_CAP) + index = (int)((c0_history_count + (uint64_t)index) % C0_HISTORY_CAP); *x = c0_history[index].x; *y = c0_history[index].y; *w = c0_history[index].w; *h = c0_history[index].h; *func = c0_history[index].func_addr; @@ -5762,6 +5772,12 @@ int gpu_get_c0_history(int index, int *x, int *y, int *w, int *h, *rcount = c0_history[index].read_count; return 1; } +uint32_t gpu_get_c0_frame(int index) { + if (index < 0 || index >= gpu_get_c0_count()) return 0; + if (c0_history_count > C0_HISTORY_CAP) + index = (int)((c0_history_count + (uint64_t)index) % C0_HISTORY_CAP); + return c0_history[index].frame; +} static void gp0_exec_vram_to_cpu(void) { /* 0xC0 — VRAM→CPU Copy (3 words) @@ -5777,8 +5793,9 @@ static void gp0_exec_vram_to_cpu(void) { vram_read_h = (h == 0) ? 0x200 : (uint16_t)h; /* Record for debug */ - if (c0_history_count < C0_HISTORY_CAP) { - int slot = c0_history_count++; + { + int slot = (int)(c0_history_count++ % C0_HISTORY_CAP); + c0_history[slot].frame = (uint32_t)s_frame_count; c0_history[slot].x = vram_read_x; c0_history[slot].y = vram_read_y; c0_history[slot].w = vram_read_w; diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index b5f99efc..0ae27908 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -82,9 +82,18 @@ typedef struct { * corrupt snapshot (0x801384AC paired with 0x801EFE80). */ uint32_t entry_sp; /* cpu->gpr[29] at exception entry */ uint32_t pump_site; /* g_cosim_dirty_pump_site at entry (which delivery path) */ + uint64_t exit_cycle; /* guest cycle at the exit restore decision (0 = not exited) */ } IrqCtxEntry; IrqCtxEntry g_irqctx_ring[IRQCTX_RING_CAP]; uint64_t g_irqctx_seq = 0; +/* Always-on per-source exception accounting (irq_exc_stats). A delivery is + * charged to every I_STAT&I_MASK bit pending at entry (index 11 = software + * interrupt only); cycles are guest cycles from entry to the exit restore + * decision, so a handler that stalls (e.g. a held DMA kick) shows up here. */ +uint64_t g_exc_src_deliveries[12]; +uint64_t g_exc_src_cycles[12]; +uint64_t g_exc_cycles_total; +uint64_t g_exc_exits_total; extern uint64_t psx_get_cycle_count(void); /* Record an interrupt-delivery decision into the event ring. GATE outcomes are @@ -2000,6 +2009,20 @@ irq_deliver_eval: e->v1_saved = saved_gpr[3]; e->ra_exit = cpu->gpr[31]; e->ra_saved = saved_gpr[31]; + e->exit_cycle = psx_get_cycle_count(); + { + const uint64_t spent = e->exit_cycle - e->cycle; + const uint32_t pend = (e->istat & e->imask) & 0x7FFu; + g_exc_cycles_total += spent; + g_exc_exits_total++; + if (!pend) { g_exc_src_deliveries[11]++; g_exc_src_cycles[11] += spent; } + for (int b = 0; b < 11; b++) { + if (pend & (1u << b)) { + g_exc_src_deliveries[b]++; + g_exc_src_cycles[b] += spent; + } + } + } } if (do_restore) { for (int i = 0; i < 32; i++) cpu->gpr[i] = saved_gpr[i]; diff --git a/runtime/tests/test_dma_gpu_list_cpu_hold.c b/runtime/tests/test_dma_gpu_list_cpu_hold.c index 656d0153..d3a70a15 100644 --- a/runtime/tests/test_dma_gpu_list_cpu_hold.c +++ b/runtime/tests/test_dma_gpu_list_cpu_hold.c @@ -21,6 +21,8 @@ #include #include +#include "dma.h" + void dma_init(void); void dma_write(uint32_t addr, uint32_t val); void dma_advance(uint32_t cycles); @@ -87,6 +89,8 @@ void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux) { (void)kind; (void)detail; (void)aux; } void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; } +static int s_in_exception; +int psx_get_in_exception(void) { return s_in_exception; } /* ---- scenario ------------------------------------------------------------ */ #define NODE_A 0x00001000u @@ -153,6 +157,30 @@ int main(void) { "GPU received %u words (%08X %08X %08X), expected A's and B's payload", s_gp0_n, s_gp0[0], s_gp0[1], s_gp0[2]); + /* The hold is recorded in the always-on hold ring (dma_hold_stats / + * dma_hold_ring): A's header + 2 words, B's header + 1 word, C's header. */ + { + DMAGpuHoldStats st; const DMAGpuHoldEntry *ring = NULL; + dma_debug_get_gpu_hold(&st, &ring); + CHECK(st.count == 1u && st.exc_count == 0u, "hold count %llu exc %llu", + (unsigned long long)st.count, (unsigned long long)st.exc_count); + CHECK(ring && ring[0].words == 6u && ring[0].held_cycles == psx_cycle_count - before + && ring[0].cycle == before && ring[0].in_exception == 0u + && ring[0].still_active == 0u, + "hold entry words %u held %u cycle %llu", ring ? ring[0].words : 0u, + ring ? ring[0].held_cycles : 0u, + ring ? (unsigned long long)ring[0].cycle : 0ull); + /* A kick from inside an exception handler is attributed to it. */ + s_in_exception = 1; + kick(0); + s_in_exception = 0; + dma_debug_get_gpu_hold(&st, &ring); + CHECK(st.count == 2u && st.exc_count == 1u && ring[1].in_exception == 1u && + st.exc_held_cycles == ring[1].held_cycles, + "exception-kick attribution: count %llu exc %llu", + (unsigned long long)st.count, (unsigned long long)st.exc_count); + } + /* Control: a kick from inside the device service is not held. */ CHECK(kick(1), "a kick from inside the device service was held"); finish_walk();