Files
psxrecomp/runtime/src/device_trace.c
T
Matthew Stanley 18c94a1394 device_trace: general two-process device-event (IRQ) cycle ring
New always-on ring (device_trace.{c,h}, compiles into both binaries like
parity_trace) recording every hardware IRQ-raise edge with its guest cycle,
source (I_STAT bit) and detail. All native device sources funnel their raise
through one choke point psx_irq_raise(bit,detail) in interrupts.c (VBLANK/GPU/
CDROM/DMA/timers/SIO routed; DMA passes DICR channel flags, timers pass index),
so the device-timing audit sees every event from one place. devtrace_dump/ctl
debug commands (cycle-window + source filters) + tools/devtrace_diff.py align
the two device-event timelines by source ordinal and flag where cycles drift.
The Beetle oracle side (mednafen IRQ_Assert rising-edge hook) lives in the main
checkout.

This is the level below parity_trace: parity names the divergent control-flow /
gate input; device_trace names the divergent HARDWARE EVENT behind it.

First result (MMX6 cutscene asset-load over-fast wedge): the load's CD+DMA
activity is compressed into ~232M cycles on native vs ~384M on Beetle (~1.65x),
completing the func_8001CB3C scene setup ~100M cycles / ~176 frames early. The
producer is CD-read / DMA-completion timing (DMA channels 2/3/4 = GPU/CD/SPU
asset streaming) -- confirming the device-completion-latency hypothesis and
ruling in the CDROM/DMA cycle model as the over-fast subsystem. (VBlank verified
correct: native VBlank edges are exactly 564480 cyc apart.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:53:27 -07:00

67 lines
2.5 KiB
C

/* device_trace.c — general two-process device-event cycle ring.
* See device_trace.h. Self-contained (no runtime internals) so the SAME TU
* compiles into both psx-runtime and psx-beetle and they emit identical rows. */
#include "device_trace.h"
#include <string.h>
#define DEVTRACE_RING_CAP (1u << 20) /* 1M events (~24 MB); covers the whole
* boot->wedge window so the divergent
* event is in-window, never evicted. */
static DevEvent s_ring[DEVTRACE_RING_CAP];
static uint64_t s_seq = 0;
static uint32_t s_armed = 0;
/* Guest-cycle + frame accessors — the SAME the parity ring uses, defined once
* per host process (native: main.cpp; Beetle: beetle_libretro.cpp). Reused so
* there is exactly one notion of "guest cycle" / "frame" per process. */
extern uint32_t parity_host_frame(void);
extern uint64_t parity_host_cycle(void);
void device_trace_arm(int on) { s_armed = on ? 1u : 0u; }
void device_trace_reset(void) { s_seq = 0; memset(s_ring, 0, sizeof(s_ring)); }
int device_trace_is_armed(void) { return (int)s_armed; }
uint64_t device_trace_total(void) { return s_seq; }
const char* device_source_str(uint32_t source)
{
switch (source) {
case DEV_IRQ_VBLANK: return "vblank";
case DEV_IRQ_GPU: return "gpu";
case DEV_IRQ_CDROM: return "cdrom";
case DEV_IRQ_DMA: return "dma";
case DEV_IRQ_TIMER0: return "timer0";
case DEV_IRQ_TIMER1: return "timer1";
case DEV_IRQ_TIMER2: return "timer2";
case DEV_IRQ_SIO0: return "sio0";
case DEV_IRQ_SIO1: return "sio1";
case DEV_IRQ_SPU: return "spu";
case DEV_IRQ_PIO: return "pio";
default: return "?";
}
}
void device_trace_note(uint32_t source, uint32_t detail)
{
if (!s_armed) return;
DevEvent* e = &s_ring[s_seq & (DEVTRACE_RING_CAP - 1u)];
e->seq = s_seq;
e->cycle = parity_host_cycle();
e->frame = parity_host_frame();
e->source = source;
e->detail = detail;
s_seq++;
}
uint32_t device_trace_get(DevEvent* out, uint32_t max_rows)
{
if (!out || max_rows == 0) return 0;
uint64_t total = s_seq;
uint32_t avail = (total < DEVTRACE_RING_CAP) ? (uint32_t)total : DEVTRACE_RING_CAP;
uint32_t n = (avail < max_rows) ? avail : max_rows;
uint64_t start = total - n;
for (uint32_t i = 0; i < n; i++)
out[i] = s_ring[(start + i) & (DEVTRACE_RING_CAP - 1u)];
return n;
}