mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/* device_trace.h — general two-process DEVICE-EVENT cycle ring.
|
||||
*
|
||||
* Purpose: name the device/IRQ event whose guest-cycle timing diverges between
|
||||
* psx-runtime (code under test) and psx-beetle (independent oracle). Where
|
||||
* parity_trace answers "which control-flow / gate-input diverged", this answers
|
||||
* the level below it: "which hardware event (CD sector IRQ / DMA completion /
|
||||
* RootCounter / VBlank / SPU) fired at a different guest cycle" — the producer
|
||||
* of an over-fast or over-slow wait.
|
||||
*
|
||||
* Model (CLAUDE.md ring-buffer doctrine): ALWAYS-ON from boot, records one row
|
||||
* per hardware IRQ-raise edge (the single choke point each backend funnels its
|
||||
* device interrupts through — native psx_irq_raise(); Beetle mednafen
|
||||
* IRQ_Assert rising edge), stamped with the deterministic guest-cycle ruler.
|
||||
* A probe QUERIES the ring for a cycle window; it never arms-then-times.
|
||||
*
|
||||
* Generality: nothing here is title-specific. The SAME TU compiles into both
|
||||
* binaries and emits IDENTICAL rows, so tools/devtrace_diff.py pulls both
|
||||
* timelines and aligns them by guest cycle to find the first divergent event.
|
||||
* This is the reusable device-timing audit substrate for the whole ecosystem
|
||||
* (see recomp-template); the MMX6 cutscene asset-load over-fast wedge is its
|
||||
* first user.
|
||||
*/
|
||||
#ifndef DEVICE_TRACE_H
|
||||
#define DEVICE_TRACE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* IRQ source = the PSX I_STAT bit index. Identical on both backends (mednafen
|
||||
* irq.h uses the same numbering). */
|
||||
typedef enum {
|
||||
DEV_IRQ_VBLANK = 0,
|
||||
DEV_IRQ_GPU = 1,
|
||||
DEV_IRQ_CDROM = 2,
|
||||
DEV_IRQ_DMA = 3,
|
||||
DEV_IRQ_TIMER0 = 4,
|
||||
DEV_IRQ_TIMER1 = 5,
|
||||
DEV_IRQ_TIMER2 = 6,
|
||||
DEV_IRQ_SIO0 = 7,
|
||||
DEV_IRQ_SIO1 = 8,
|
||||
DEV_IRQ_SPU = 9,
|
||||
DEV_IRQ_PIO = 10,
|
||||
} dev_irq_source_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t seq; /* monotonic event index since boot */
|
||||
uint64_t cycle; /* absolute guest CPU cycles at the raise (the ruler) */
|
||||
uint32_t frame; /* host video frame at the raise */
|
||||
uint32_t source; /* dev_irq_source_t — the I_STAT bit raised */
|
||||
uint32_t detail; /* source-specific: DMA channel / timer index / 0 */
|
||||
} DevEvent;
|
||||
|
||||
void device_trace_arm(int on); /* on=1 begins recording (idempotent) */
|
||||
void device_trace_reset(void); /* clear ring (keeps armed state) */
|
||||
int device_trace_is_armed(void); /* hot-path gate: cheap one-branch check */
|
||||
|
||||
/* Record one device-IRQ raise edge. No-op unless armed. Stamps cycle+frame via
|
||||
* the process's guest-cycle/frame accessors (shared with parity_trace). */
|
||||
void device_trace_note(uint32_t source, uint32_t detail);
|
||||
|
||||
/* Readback: copies up to max_rows NEWEST events, oldest-first, into out[]. */
|
||||
uint32_t device_trace_get(DevEvent* out, uint32_t max_rows);
|
||||
uint64_t device_trace_total(void);
|
||||
const char* device_source_str(uint32_t source);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* DEVICE_TRACE_H */
|
||||
@@ -24,6 +24,13 @@ struct CPUState;
|
||||
|
||||
void interrupts_init(void);
|
||||
|
||||
/* Central IRQ-raise choke point: sets the I_STAT bit AND records the raise edge
|
||||
* into the always-on device-event ring (device_trace) with the guest cycle.
|
||||
* Every hardware source (VBLANK/GPU/CDROM/DMA/timers/SIO/SPU) funnels its raise
|
||||
* through here so the device-timing audit sees every event from one place.
|
||||
* `detail` is source-specific (DMA channel, timer index) or 0. */
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
|
||||
/* Called from the dispatch loop after each function returns.
|
||||
* Fires vblank on schedule, checks (i_stat & i_mask), and if
|
||||
* pending + COP0 allows, dispatches the exception handler. */
|
||||
|
||||
@@ -133,6 +133,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/device_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/traps.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
|
||||
|
||||
+3
-1
@@ -36,6 +36,8 @@ extern int iso_track_is_audio(void* handle, int track);
|
||||
|
||||
/* I_STAT owned by memory.c — set bit 2 for CDROM IRQ */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
extern uint32_t g_debug_current_func_addr;
|
||||
extern uint32_t g_debug_last_store_pc;
|
||||
extern uint64_t s_frame_count;
|
||||
@@ -536,7 +538,7 @@ static void present_cdrom_irq(void) {
|
||||
* own trailing INTC ack. */
|
||||
if (cdrom_irq_present_delay > 0) return;
|
||||
if (irq_flag && (irq_enable & (1 << (irq_flag - 1))) && !cdrom_intc_request_latched) {
|
||||
i_stat |= (1u << 2); /* IRQ_CDROM */
|
||||
psx_irq_raise(2, irq_flag); /* IRQ_CDROM; detail = CD response/IRQ type */
|
||||
cdrom_intc_request_latched = 1;
|
||||
cdrom_intc_latched_generation = cdrom_irq_generation;
|
||||
event_ring_record(EV_ISTAT_RAISE, 2 /* IRQ_CDROM bit */);
|
||||
|
||||
@@ -2606,6 +2606,7 @@ static void handle_dirty_insn_dump_file(int id, const char *json)
|
||||
* across processes (psx-runtime port 4370, psx-beetle port 4380). */
|
||||
#include "fntrace.h"
|
||||
#include "parity_trace.h"
|
||||
#include "device_trace.h"
|
||||
|
||||
/* ---- parity_dump / parity_ctl: general two-process control-flow parity ring.
|
||||
* Mirrors the IDENTICAL command on psx-beetle so tools/parity_diff.py can pull
|
||||
@@ -2691,6 +2692,61 @@ static void handle_parity_ctl(int id, const char *json)
|
||||
debug_server_send_line(buf);
|
||||
}
|
||||
|
||||
/* ---- devtrace_dump / devtrace_ctl: general two-process device-event ring.
|
||||
* Identical command + JSON on psx-beetle so tools/devtrace_diff.py pulls both
|
||||
* device-IRQ timelines and aligns them by guest cycle. Optional filters:
|
||||
* count max newest events to return (default 65536, cap = ring)
|
||||
* cyc_lo / cyc_hi half-open guest-cycle window (decimal) — slice the load
|
||||
* src only this I_STAT source bit (0..10); omit/negative = all */
|
||||
static void handle_devtrace_dump(int id, const char *json)
|
||||
{
|
||||
int count = json_get_int(json, "count", 65536);
|
||||
if (count < 1) count = 1;
|
||||
if (count > (1 << 20)) count = (1 << 20);
|
||||
char buf[32];
|
||||
uint64_t cyc_lo = 0, cyc_hi = ~0ull;
|
||||
if (json_get_str(json, "cyc_lo", buf, sizeof buf)) cyc_lo = strtoull(buf, NULL, 0);
|
||||
if (json_get_str(json, "cyc_hi", buf, sizeof buf)) cyc_hi = strtoull(buf, NULL, 0);
|
||||
int src = json_get_int(json, "src", -1);
|
||||
|
||||
DevEvent *e = (DevEvent *)malloc(sizeof(DevEvent) * (size_t)count);
|
||||
if (!e) { send_err(id, "oom"); return; }
|
||||
uint32_t got = device_trace_get(e, (uint32_t)count);
|
||||
const size_t BUF_SZ = 12 * 1024 * 1024;
|
||||
char *out = (char *)malloc(BUF_SZ);
|
||||
if (!out) { free(e); send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"armed\":%d,\"count\":%u,\"events\":[",
|
||||
id, (unsigned long long)device_trace_total(), device_trace_is_armed(), got);
|
||||
uint32_t emitted = 0;
|
||||
for (uint32_t i = 0; i < got; i++) {
|
||||
if (pos > BUF_SZ - 256) break;
|
||||
DevEvent *r = &e[i];
|
||||
if (r->cycle < cyc_lo || r->cycle >= cyc_hi) continue;
|
||||
if (src >= 0 && (int)r->source != src) continue;
|
||||
pos += snprintf(out + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"srcn\":%u,\"src\":\"%s\",\"detail\":%u}",
|
||||
emitted ? "," : "", (unsigned long long)r->seq, (unsigned long long)r->cycle,
|
||||
r->frame, r->source, device_source_str(r->source), r->detail);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(out + pos, BUF_SZ - pos, "],\"emitted\":%u}\n", emitted);
|
||||
debug_server_send_line(out); free(out); free(e);
|
||||
}
|
||||
|
||||
static void handle_devtrace_ctl(int id, const char *json)
|
||||
{
|
||||
if (json_get_int(json, "reset", 0)) device_trace_reset();
|
||||
long armv = json_get_int(json, "arm", -1);
|
||||
if (armv == 0 || armv == 1) device_trace_arm((int)armv);
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof buf,
|
||||
"{\"id\":%d,\"ok\":true,\"armed\":%d,\"total\":%llu}\n",
|
||||
id, device_trace_is_armed(), (unsigned long long)device_trace_total());
|
||||
debug_server_send_line(buf);
|
||||
}
|
||||
|
||||
static void handle_fntrace_arm(int id, const char *json)
|
||||
{
|
||||
char buf[32];
|
||||
@@ -10338,6 +10394,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "ping", handle_ping },
|
||||
{ "parity_dump", handle_parity_dump },
|
||||
{ "parity_ctl", handle_parity_ctl },
|
||||
{ "devtrace_dump", handle_devtrace_dump },
|
||||
{ "devtrace_ctl", handle_devtrace_ctl },
|
||||
{ "latency", handle_latency },
|
||||
{ "vk_perf", handle_vk_perf },
|
||||
{ "game_options", handle_game_options },
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/* 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;
|
||||
}
|
||||
+3
-1
@@ -32,6 +32,8 @@ extern void psx_write_word(uint32_t addr, uint32_t val);
|
||||
|
||||
/* Interrupt status — defined in memory.c */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
extern uint32_t g_debug_current_func_addr;
|
||||
extern uint32_t g_debug_last_store_pc;
|
||||
extern uint64_t s_frame_count;
|
||||
@@ -215,7 +217,7 @@ static void raise_dma_irq_on_master_edge(uint32_t before) {
|
||||
* from 0 to 1. Pending channel flags keep bit 31 high, but do not
|
||||
* continuously re-latch I_STAT after software acknowledges IRQ3. */
|
||||
if (!dicr_master_flag(before) && dicr_master_flag(dicr)) {
|
||||
i_stat |= (1u << 3);
|
||||
psx_irq_raise(3, (dicr >> 24) & 0x7Fu); /* detail = DICR per-channel IRQ flags */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1074,6 +1074,8 @@ static uint32_t lcf; /* bit 31 */
|
||||
* BIOS-shell GPUSTAT poll loop, without going through the
|
||||
* vblank-callback path (which would call SDL_Delay/Sleep). */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
|
||||
/* Display area start (GP1(05h)) */
|
||||
static uint32_t display_area_x;
|
||||
@@ -1229,7 +1231,7 @@ uint32_t gpu_read_gpustat(void) {
|
||||
if (gpustat_poll_count >= GPUSTAT_POLL_VBLANK_THRESHOLD) {
|
||||
gpustat_poll_count = 0;
|
||||
lcf ^= 1;
|
||||
i_stat |= (1u << 0); /* IRQ_VBLANK */
|
||||
psx_irq_raise(0, 0); /* IRQ_VBLANK (GPUSTAT-poll fallback path) */
|
||||
/* DEQUEUE: VBlank fired via the GPUSTAT-poll fallback path (distinct
|
||||
* from the cycle-paced VBlank in psx_check_interrupts). */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, 0xFFFFFFFFu);
|
||||
@@ -1343,7 +1345,7 @@ static uint16_t rgb888_to_rgb555(uint32_t color24) {
|
||||
void gpu_vblank_tick(void) {
|
||||
lcf ^= 1;
|
||||
gpustat_poll_count = 0;
|
||||
i_stat |= (1u << 0); /* IRQ_VBLANK */
|
||||
psx_irq_raise(0, 0); /* IRQ_VBLANK (gpu_vblank_tick) */
|
||||
if (vblank_callback) vblank_callback();
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,20 @@ static void irq_record_outcome(uint8_t kind, uint8_t detail, uint32_t aux) {
|
||||
extern uint32_t i_stat;
|
||||
extern uint32_t i_mask;
|
||||
|
||||
/* Device-event cycle ring (device_trace.c): every hardware IRQ-raise edge is
|
||||
* recorded here with its guest cycle. */
|
||||
#include "device_trace.h"
|
||||
|
||||
/* Central IRQ-raise choke point. All device sources call this to set their
|
||||
* I_STAT bit so the device-event ring sees every raise from one place with the
|
||||
* exact guest cycle. Pure addition over `i_stat |= (1<<bit)` — identical effect
|
||||
* on i_stat, plus the trace note (no-op unless the ring is armed). */
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail)
|
||||
{
|
||||
i_stat |= (1u << bit);
|
||||
device_trace_note(bit, detail);
|
||||
}
|
||||
|
||||
/* Dispatch counter for vblank scheduling. */
|
||||
#define VBLANK_INTERVAL 50000 /* legacy: dispatch-count fallback (unused for VBlank gating now) */
|
||||
#define VBLANK_CYCLES 564480u /* 33.8688 MHz / 60 Hz — real PSX NTSC VBlank period */
|
||||
@@ -391,7 +405,7 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
(uint32_t)psx_get_cycle_count());
|
||||
event_ring_record_aux(EV_ENQ, (uint8_t)SRC_VBLANK,
|
||||
(uint32_t)(psx_get_cycle_count() + VBLANK_CYCLES));
|
||||
i_stat |= (1 << IRQ_VBLANK);
|
||||
psx_irq_raise(IRQ_VBLANK, 0);
|
||||
g_vblank_raise_count++;
|
||||
event_ring_record(EV_ISTAT_RAISE, IRQ_VBLANK);
|
||||
gpu_vblank_tick(); /* Toggle LCF (GPUSTAT bit 31) */
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "cpu_state.h"
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
#include "parity_trace.h" /* general two-process control-flow parity ring */
|
||||
#include "device_trace.h" /* general two-process device-event cycle ring */
|
||||
#include "psx_interpreter.h"
|
||||
#include "cdrom.h"
|
||||
#include "fntrace.h"
|
||||
@@ -2828,6 +2829,20 @@ int main(int argc, char** argv) {
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
/* Device-event cycle ring (device_trace.h): armed from boot under the same
|
||||
* PSX_PARITY_TRACE gate, or PSX_DEVTRACE=1 standalone. Captures every
|
||||
* CD/DMA/timer/VBlank/SIO/SPU IRQ raise with its guest cycle for the
|
||||
* cross-process device-timing diff (tools/devtrace_diff.py). */
|
||||
{
|
||||
const char* pt = std::getenv("PSX_PARITY_TRACE");
|
||||
const char* dt = std::getenv("PSX_DEVTRACE");
|
||||
if ((pt && pt[0] && pt[0] != '0') || (dt && dt[0] && dt[0] != '0')) {
|
||||
device_trace_arm(1);
|
||||
std::fprintf(stdout, "psxrecomp: device-event trace ARMED\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
psx_scheduler_run(&cpu);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
/* I_STAT and I_MASK are owned by memory.c */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
extern uint32_t i_mask;
|
||||
|
||||
#define COP0_SR 12
|
||||
@@ -521,7 +523,7 @@ static void interp_check_interrupts(CPUState* cpu) {
|
||||
s_vblank_count++;
|
||||
if (s_vblank_count >= VBLANK_INTERVAL) {
|
||||
s_vblank_count = 0;
|
||||
i_stat |= (1u << IRQ_VBLANK);
|
||||
psx_irq_raise(IRQ_VBLANK, 0);
|
||||
gpu_vblank_tick();
|
||||
timers_tick(33868);
|
||||
cdrom_tick();
|
||||
|
||||
+4
-2
@@ -20,6 +20,8 @@
|
||||
|
||||
/* I_STAT is owned by memory.c */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
|
||||
/* IRQ bit for SIO0 */
|
||||
#define IRQ_SIO0 7
|
||||
@@ -1872,7 +1874,7 @@ static void sio_fire_ack_irq(void) {
|
||||
sio_stat |= SIO_STAT_IRQ;
|
||||
|
||||
uint32_t i_stat_before = i_stat;
|
||||
i_stat |= (1 << IRQ_SIO0);
|
||||
psx_irq_raise(IRQ_SIO0, 0); /* SIO ACK IRQ */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_SIO, 0u); /* SIO ACK IRQ fired */
|
||||
|
||||
extern uint32_t g_debug_current_func_addr;
|
||||
@@ -2070,7 +2072,7 @@ void sio_tick(int cycles) {
|
||||
* on TX_RDY before writing the next byte. */
|
||||
sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY;
|
||||
uint32_t i_stat_before = i_stat;
|
||||
i_stat |= (1 << IRQ_SIO0);
|
||||
psx_irq_raise(IRQ_SIO0, 1); /* SIO shift IRQ */
|
||||
event_ring_record_aux(EV_DEQ, (uint8_t)SRC_SIO, 1u); /* SIO shift IRQ fired */
|
||||
|
||||
/* SIO IRQ ring capture. */
|
||||
|
||||
@@ -67,6 +67,8 @@ void timers_get_debug(int t, uint16_t *counter, uint16_t *target,
|
||||
|
||||
/* I_STAT is owned by memory.c — we poke it directly via this extern. */
|
||||
extern uint32_t i_stat;
|
||||
/* Central IRQ-raise choke point (interrupts.c) — also records the device ring. */
|
||||
extern void psx_irq_raise(uint32_t bit, uint32_t detail);
|
||||
|
||||
void timers_init(void) {
|
||||
memset(timers, 0, sizeof(timers));
|
||||
@@ -119,14 +121,14 @@ static void timer_fire_irq(int t) {
|
||||
tm->irq_line ^= 1;
|
||||
if (tm->irq_line) {
|
||||
tm->mode &= ~MODE_IRQ_REQUEST;
|
||||
i_stat |= (1 << timer_irq[t]);
|
||||
psx_irq_raise(timer_irq[t], t); /* detail = timer index */
|
||||
} else {
|
||||
tm->mode |= MODE_IRQ_REQUEST;
|
||||
}
|
||||
} else {
|
||||
/* Pulse mode */
|
||||
tm->mode &= ~MODE_IRQ_REQUEST;
|
||||
i_stat |= (1 << timer_irq[t]);
|
||||
psx_irq_raise(timer_irq[t], t); /* detail = timer index */
|
||||
tm->mode |= MODE_IRQ_REQUEST;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""devtrace_diff.py — two-process DEVICE-EVENT cycle-timeline diff.
|
||||
|
||||
Pulls the always-on device-event ring (devtrace_dump) from psx-runtime (code
|
||||
under test) and psx-beetle (independent oracle) and compares, per IRQ source
|
||||
(VBLANK / GPU / CDROM / DMA / TIMER0-2 / SIO / SPU), the GUEST CYCLE at which the
|
||||
Nth event of that source fired. VBlank is cycle-paced so it should track between
|
||||
the two; a source that drifts (its Nth event at a very different cycle) is the
|
||||
device whose completion timing diverges — the producer of an over-fast/over-slow
|
||||
wait. This is the level below parity_diff (control flow): it names the hardware
|
||||
event, not the branch.
|
||||
|
||||
Both backends emit the IDENTICAL row schema (device_trace.h), so one tool reads
|
||||
both ports by switching the port number.
|
||||
|
||||
Usage:
|
||||
python tools/devtrace_diff.py [--native PORT] [--beetle PORT]
|
||||
[--cyc-lo C] [--cyc-hi C] # restrict to a guest-cycle window
|
||||
[--drift N] # flag first ordinal whose |Δcycle| > N (default 2_000_000)
|
||||
[--show K] # also print first K events per source per side
|
||||
"""
|
||||
import argparse, json, socket, sys
|
||||
|
||||
SRC = {0:"vblank",1:"gpu",2:"cdrom",3:"dma",4:"timer0",5:"timer1",6:"timer2",
|
||||
7:"sio0",8:"sio1",9:"spu",10:"pio"}
|
||||
|
||||
def query(port, cmd, **kw):
|
||||
obj = {"cmd": cmd}; obj.update(kw)
|
||||
s = socket.create_connection(("127.0.0.1", port), timeout=15)
|
||||
try:
|
||||
s.sendall((json.dumps(obj) + "\n").encode())
|
||||
buf = b""
|
||||
while b"\n" not in buf:
|
||||
chunk = s.recv(1 << 20)
|
||||
if not chunk: break
|
||||
buf += chunk
|
||||
return json.loads(buf.decode(errors="replace").split("\n", 1)[0])
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
def pull(port, cyc_lo, cyc_hi):
|
||||
kw = {"count": 1 << 20}
|
||||
if cyc_lo is not None: kw["cyc_lo"] = str(cyc_lo)
|
||||
if cyc_hi is not None: kw["cyc_hi"] = str(cyc_hi)
|
||||
r = query(port, "devtrace_dump", **kw)
|
||||
if not r.get("ok"):
|
||||
print(f"[port {port}] devtrace_dump failed: {r}", file=sys.stderr); sys.exit(2)
|
||||
return r
|
||||
|
||||
def by_source(events):
|
||||
d = {}
|
||||
for e in events:
|
||||
d.setdefault(e["srcn"], []).append(e)
|
||||
return d
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--native", type=int, default=4490)
|
||||
ap.add_argument("--beetle", type=int, default=4382)
|
||||
ap.add_argument("--cyc-lo", type=int, default=None)
|
||||
ap.add_argument("--cyc-hi", type=int, default=None)
|
||||
ap.add_argument("--drift", type=int, default=2_000_000)
|
||||
ap.add_argument("--show", type=int, default=0)
|
||||
args = ap.parse_args()
|
||||
|
||||
nat = pull(args.native, args.cyc_lo, args.cyc_hi)
|
||||
bet = pull(args.beetle, args.cyc_lo, args.cyc_hi)
|
||||
ne, be = nat["events"], bet["events"]
|
||||
print(f"native(:{args.native}): {len(ne)} events (ring total={nat['total']}, armed={nat['armed']})")
|
||||
print(f"beetle(:{args.beetle}): {len(be)} events (ring total={bet['total']}, armed={bet['armed']})")
|
||||
|
||||
ns, bs = by_source(ne), by_source(be)
|
||||
sources = sorted(set(ns) | set(bs))
|
||||
print(f"\n{'source':<8} {'native#':>8} {'beetle#':>8} {'first divergent ordinal (cycle delta)':<40}")
|
||||
print("-" * 78)
|
||||
for s in sources:
|
||||
n, b = ns.get(s, []), bs.get(s, [])
|
||||
name = SRC.get(s, f"src{s}")
|
||||
# align by ordinal, find first where the cycle gap exceeds --drift
|
||||
flag = ""
|
||||
m = min(len(n), len(b))
|
||||
for i in range(m):
|
||||
dc = n[i]["cycle"] - b[i]["cycle"]
|
||||
if abs(dc) > args.drift:
|
||||
flag = f"#{i}: native cyc={n[i]['cycle']} beetle cyc={b[i]['cycle']} (delta={dc:+d})"
|
||||
break
|
||||
if not flag and len(n) != len(b):
|
||||
flag = f"count differs ({len(n)} vs {len(b)}) — no big per-ordinal drift in first {m}"
|
||||
if not flag:
|
||||
flag = "tracks (no drift > threshold)"
|
||||
print(f"{name:<8} {len(n):>8} {len(b):>8} {flag}")
|
||||
|
||||
if args.show:
|
||||
for s in sources:
|
||||
name = SRC.get(s, f"src{s}")
|
||||
print(f"\n--- {name} first {args.show} (native | beetle) ---")
|
||||
n, b = ns.get(s, []), bs.get(s, [])
|
||||
for i in range(min(args.show, max(len(n), len(b)))):
|
||||
nc = f"cyc={n[i]['cycle']} f={n[i]['frame']} d={n[i]['detail']}" if i < len(n) else "-"
|
||||
bc = f"cyc={b[i]['cycle']} f={b[i]['frame']} d={b[i]['detail']}" if i < len(b) else "-"
|
||||
print(f" #{i:<4} {nc:<34} | {bc}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user