mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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:
+10
-4
@@ -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` | ✓ | | |
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user