mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
70c1eb8a51
Brings the ape-escape framework (real-EPC interrupt delivery,
g_psx_call_depth safe-point gating, continuation-entry codegen, DualShock
controller config, exception/entryint/irq-check rings, interp
dispatch_until + exit logging + static-text fallback) up to master
035a9fa (frame-pacing TOCTOU fix, vsync self-heal, watchdog classifier,
overlay JIT cache + autocapture/autocompile, Bug A interp call contract,
prod build hygiene).
Key reconciliations (ambiguity resolved toward master per plan):
- interrupts.c: real-EPC delivery (psx_check_interrupts_at) is the
primary entry; the legacy psx_check_interrupts wrapper keeps the
0x80000048 sentinel fallback (ape tip restored it), so old-flavor
generated code keeps working interrupt delivery. Master's bottom
compat shim removed (would have recursed). Both observability paths
kept (event-ring gate outcomes + ape irq-check ring; new
GATE_CALL_DEPTH/GATE_BAD_RESUME kinds).
- Claimed-IRQ cooldown: master's per-source CLAIMED_PROGRESS_QUANTUM=8
(SIO exempt) over ape's unconditional 0; ape's 2026-06-05 starvation
note kept as a MERGE NOTE — first knob to A/B if title-LOAD regresses.
(User-validated: Tomba title-LOAD slot1/slot2 renders and plays on
this merge — the historical ape-fw blackout is gone.)
- dirty_ram_interp.c: ape's dispatch_until_impl structure (stop_addr,
static-text fallback, exit-log ring) with master's Bug A call contract
(psx_dispatch_game_compiled + overlay_loader_call_native at interp
jal/jalr), overlay loader/fingerprint entry checks, freeze-gated flow
ring, chain-target attribution, and interp-active bracketing on the
public wrappers. Ape tip's final local-flow rule kept: dirty game-text
NEVER chains locally (the tip reversed its own earlier widening).
- freeze_heartbeat.c: classifiers composed — per-frame storm threshold
(master) + progress-counter gating and 6s window (ape) + slow_frames
kind (master), thresholds rescaled to the window.
- code_generator.cpp: ape's continuation-entry switch + master's
refactored scan_jr_tables.
- debug_server.c: ape originals win over master's ported copies
(bioscall ring, card trace with the richer address set); master-only
tools kept (dirty_*_dump_file, card_trace filter handler).
- dma.c/cdrom.c: master's overlay-capture path + ape's DMA/command
history diagnostics, both wired.
Builds: recompiler + Tomba runtime (legacy gen x merged runtime).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
6.3 KiB
C
131 lines
6.3 KiB
C
/*
|
|
* event_ring.h — Always-on unified event-timeline ring.
|
|
*
|
|
* Purpose: capture the interleaved timeline of interrupt-delivery decisions,
|
|
* I_STAT transitions, and DMA kick/complete events, each tagged with the
|
|
* current guest cycle, PC, enclosing recompiled function, current execution
|
|
* mode (native-overlay / dirty-RAM-interp / static), and the in-progress
|
|
* native overlay function. The goal is to diff two runs of the SAME route
|
|
* (native-overlay OFF vs ON) and find the FIRST interrupt/DMA ordering
|
|
* divergence relative to game code (PRINCIPLES.md #3, handoff Priority 2).
|
|
*
|
|
* Design: ALWAYS-ON (Release too), eviction-bounded ring. Records EDGES /
|
|
* EVENTS, never per-poll samples — native code calls psx_check_interrupts at
|
|
* every block, so polling would evict the window instantly. The interrupt
|
|
* decision path emits an event only when the decision OUTCOME changes
|
|
* (idle -> gated(reason) -> delivered), and raises/DMA are emitted at their
|
|
* source sites. Dump-on-demand; the ring survives a blue screen because the
|
|
* debug-server thread stays alive and queries it.
|
|
*
|
|
* This is OBSERVABILITY, not a fix. It does not alter timing.
|
|
*/
|
|
#ifndef PSXRECOMP_EVENT_RING_H
|
|
#define PSXRECOMP_EVENT_RING_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* The event ring is a dev diagnostic: it is only readable over the TCP
|
|
* debug server, which production builds strip (PSX_NO_DEBUG_TOOLS). It is
|
|
* not part of the freeze dump, so recording in production is pure
|
|
* hot-path cost — disable it there. */
|
|
#ifndef EVENT_RING_ENABLED
|
|
#ifdef PSX_NO_DEBUG_TOOLS
|
|
#define EVENT_RING_ENABLED 0
|
|
#else
|
|
#define EVENT_RING_ENABLED 1
|
|
#endif
|
|
#endif
|
|
|
|
/* 64K entries * 48 bytes ~= 3 MB. Covers many frames of transition activity. */
|
|
#define EVENT_RING_CAP (1u << 16)
|
|
|
|
typedef enum {
|
|
EV_NONE = 0,
|
|
EV_IRQ_DELIVER = 1, /* interrupt actually delivered (in_exception <- 1) */
|
|
EV_IRQ_GATE = 2, /* pending & masked-in but blocked; detail = gate code */
|
|
EV_ISTAT_RAISE = 3, /* known raise site fired; detail = IRQ bit */
|
|
EV_ISTAT_CHANGE= 4, /* generic backstop: i_stat changed since last check */
|
|
EV_DMA_KICK = 5, /* DMA transfer start; detail = channel, aux = chcr */
|
|
EV_DMA_DONE = 6, /* DMA transfer complete; detail = channel, aux = chcr */
|
|
EV_MODE = 7, /* execution-mode transition; mode = new mode */
|
|
EV_DMA_SCHED = 8, /* delayed (async) completion scheduled; detail=chan, aux=chcr */
|
|
EV_ENQ = 9, /* event SCHEDULED/queued; detail = EventSource, aux = due_cycle (or payload) */
|
|
EV_DEQ = 10, /* event FIRED/consumed; detail = EventSource, aux = payload (type/scheduled-due) */
|
|
} EventKind;
|
|
|
|
/* Source id for EV_ENQ / EV_DEQ (the `detail` field). Lets us independently
|
|
* track when each event is scheduled vs when it fires — a MISSING enqueue (an
|
|
* event scheduled in the working run but never in the broken run) is the prime
|
|
* suspect for "content never loads", and is invisible if you only watch fires. */
|
|
typedef enum {
|
|
SRC_NONE = 0,
|
|
SRC_DMA0 = 1, /* SRC_DMA0+ch for DMA channels 0..6 (1..7) */
|
|
SRC_VBLANK = 16, /* enqueue=next VBlank scheduled, aux=due_cycle */
|
|
SRC_CD_CMD = 17, /* enqueue=CD command issued, aux=cmd byte */
|
|
SRC_CD_READ = 18, /* enqueue=sector read scheduled, aux=read_delay cycles */
|
|
SRC_CD_IRQ = 19, /* dequeue=CD response/data IRQ fired, aux=cd irq type */
|
|
SRC_CD_PEND = 20, /* enqueue=delayed CD command scheduled, aux=delay cycles */
|
|
SRC_TIMER0 = 24, /* SRC_TIMER0+n for timers 0..2 (enqueue=armed, dequeue=fired) */
|
|
SRC_SIO = 28, /* enqueue=shift/ack countdown armed, dequeue=SIO IRQ fired */
|
|
} EventSource;
|
|
|
|
/* detail codes for EV_IRQ_GATE (why a pending+masked IRQ was NOT delivered) */
|
|
typedef enum {
|
|
GATE_NONE = 0,
|
|
GATE_IN_EXCEPTION = 1, /* already inside an exception */
|
|
GATE_COOLDOWN = 2, /* post-exception cooldown block */
|
|
GATE_SR_IE = 3, /* COP0 SR IEc clear (interrupts globally off) */
|
|
GATE_SR_IM2 = 4, /* COP0 SR IM2 clear (HW int line masked) */
|
|
GATE_CALL_DEPTH = 5, /* ape-fw: inside nested generated call, not a safe point */
|
|
GATE_BAD_RESUME = 6, /* ape-fw: no valid guest resume PC at this check */
|
|
} EventGateReason;
|
|
|
|
typedef enum {
|
|
MODE_UNKNOWN = 0,
|
|
MODE_STATIC = 1, /* statically-recompiled ROM/EXE code */
|
|
MODE_NATIVE_OVERLAY= 2, /* native overlay DLL function */
|
|
MODE_INTERP = 3, /* dirty-RAM interpreter */
|
|
} EventExecMode;
|
|
|
|
typedef struct {
|
|
uint64_t seq; /* monotonic sequence */
|
|
uint64_t cycle; /* psx_get_cycle_count() at event */
|
|
uint32_t pc; /* g_debug_last_store_pc (best "where" signal) */
|
|
uint32_t func; /* g_debug_current_func_addr */
|
|
uint32_t overlay_fn; /* in-progress native overlay fn (0 if none) */
|
|
uint32_t i_stat;
|
|
uint32_t i_mask;
|
|
uint32_t aux; /* per-kind payload: DMA events -> chcr value; else 0 */
|
|
uint16_t kind; /* EventKind */
|
|
uint8_t mode; /* EventExecMode at event time */
|
|
uint8_t detail; /* channel / gate reason / IRQ bit, per kind */
|
|
} EventEntry;
|
|
|
|
/* Record one event. Captures cycle/pc/func/overlay_fn/i_stat/i_mask/mode from
|
|
* runtime globals internally, so call sites are a single line. No-op when the
|
|
* ring is disabled. Safe to call from any runtime path. */
|
|
void event_ring_record(uint16_t kind, uint8_t detail);
|
|
/* As above, plus a per-kind aux payload (DMA events pass the CHCR value). */
|
|
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux);
|
|
|
|
/* Write the whole live window (oldest->newest) as a JSON array to `path`.
|
|
* Returns entries written, or -1 on open failure. No TCP size limit. */
|
|
int event_ring_dump_file(const char *path);
|
|
|
|
/* Emit a bounded JSON tail (most-recent `max_entries`, oldest->newest) into
|
|
* `out`. Returns bytes written. For quick TCP inspection. */
|
|
int event_ring_dump_json(char *out, int cap, int max_entries);
|
|
|
|
/* Clear the ring (e.g. to isolate a fresh window before a run). */
|
|
void event_ring_clear(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_EVENT_RING_H */
|