Record GPU list CPU holds, per-source exception cycles and recent C0 readbacks in always-on rings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-05 20:30:25 -07:00
parent 66482a7cd8
commit a734cd9034
7 changed files with 286 additions and 19 deletions
+10 -4
View File
@@ -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":"<name>"}` 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":"<name>"}` 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` | ✓ | | |
+24
View File
@@ -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
+105 -8
View File
@@ -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 },
+72
View File
@@ -31,6 +31,16 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#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) {
+24 -7
View File
@@ -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;
+23
View File
@@ -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];
@@ -21,6 +21,8 @@
#include <stdio.h>
#include <string.h>
#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();