Files
psxrecomp/runtime/include/fntrace.h
T
Matthew Stanley ae47e69902 recursion(oracle): oracle disambiguation DONE + SP-fntrace instruments; verdict = §11 universal fix (docs §23–§24)
Oracle disambiguation (RECURSION_BUG.md §23–§24). Traced one func_8001A954
invocation on real HW (psx-beetle) vs the recomp in the IDENTICAL Tomba idle
state (New Game village dialogue; object 0x801FD950, state/scene/jumptable=0).

VERDICT: real HW's per-frame chain is BALANCED — func_8001A954 is entered once
per frame from main_loop (JAL 0x8001A630, sp=0x801FE3E0) and RETURNS once per
frame (jr ra @0x8001A9E8 -> 0x8001A638, SP fully restored); no re-entry, the
overlay 0x80063864 never calls back (refutes §21/§22's "call-cycle"). The recomp
runs the same guest flow and the guest SP unwinds identically, but the HOST stack
leaks +1.17 KB/frame (ce_profile, live-reconfirmed: 20099->22215 KB / 1807 frames)
because the per-frame compiled chain (func_8001A954->AC00->B2B4->B5A8->func_80046264,
emitted as DIRECT func_X(cpu) host calls) re-enters itself via func_80046264's
CD-DMA-dirty hop and never unwinds.

native_stack early-trip (PSX_STACK_GUARD_KB=6144, frame 7592, 6 MB) pins the
leaking host cycle. Cross-checked: xprobe mixed_depth bounded at 6, dirty-insn/
dirty-block show NO literal func_8001A954 transfer, and a954=1/frame is a SURFACED
guest tail-transfer (j/jr) to func_8001A954 that the oracle proves real HW never
makes — i.e. there is both (A) a correctness divergence (spurious per-frame
re-entry) and (B) the architectural host-mirroring limitation that amplifies it.

Fix scope CORRECTED from §23's "quick contained runtime patch": the UNIVERSAL fix
is §11 — route in-range guest calls through the depth-managed psx_dispatch_impl
trampoline instead of mirroring them as direct host calls, so the host stack stays
bounded regardless of guest re-entrancy (real or fabricated). §12 (CD-DMA dirty
precision) and fixing the specific divergence are surgical mitigations, not
universal. Performance loss accepted for now (static recomp: right before fast).

Instruments added (runtime-only, no regen):
- SP capture in the fntrace control-flow ring on BOTH backends (GPR[29] read live):
  psx-beetle (beetle_libretro.cpp + beetle_debug_server.c) and the recomp
  (fntrace.h/.c + debug_server.c) — enables the func_8001A954 SP/RA-lifecycle oracle.
- psx-beetle default debug port 4380 -> 4382 (+ --port N override): 4380 collided
  with psxref and cdirecomp/CdiRuntime.exe; two LISTENers on one port silently
  misroute connections (the phantom "oracle keeps dying" early in the session).

Oracle tooling: the control-flow oracle is psx-beetle (Beetle interprets -> records
every J/JAL/JR/JALR), NOT psxref (no PC trace; the assumed emu_step/emu_trace_addr
never existed). Loads Tomba via: psx-beetle.exe --disc tomba.cue <bios> --port 4382.

Next: implement the §11 universal fix (codegen + full regen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 08:03:04 -07:00

75 lines
2.9 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* fntrace.h — always-on call ring for recomp psx_dispatch.
*
* Records every entry into psx_dispatch with the caller's argument
* registers and return address. Mirrors beetle_libretro.cpp's fntrace
* ring (same wire commands: fntrace_arm / fntrace_dump / fntrace_clear)
* so cross-process tools that already speak Beetle's protocol work
* unchanged against psx-runtime.
*
* Why "always-on" rather than arm-then-record: see CLAUDE.md global
* rule "Never time/attach for observability". The ring captures every
* dispatch from boot; arming only narrows what the dump command
* reports, never what is recorded. To investigate a window of
* interest, fntrace_dump it after the fact.
*
* Coverage: every psx_dispatch entry — both static-recompiled targets
* (ROM functions, shell-relocated functions) and dirty-RAM dispatches.
* dirty_ram_block_log overlaps with the dirty-RAM subset; this ring is
* the superset.
*/
#ifndef PSXRECOMP_FNTRACE_H
#define PSXRECOMP_FNTRACE_H
#include <stdint.h>
#include "cpu_state.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 4M entries × 32 bytes = 128 MB. At ~580K dispatches/s peak (boot)
* that's ~7s; at ~10K/s during modal idle, ~400s. Sized for press-
* window retrospectives without per-second eviction. */
#define FNTRACE_RING_CAP (1u << 22)
typedef struct {
uint32_t frame; /* s_frame_count at dispatch */
uint32_t target; /* psx_dispatch addr argument (virtual) */
uint32_t ra; /* cpu->gpr[31] at dispatch — caller's return PC */
uint32_t a0; /* cpu->gpr[4] */
uint32_t a1; /* cpu->gpr[5] */
uint32_t a2; /* cpu->gpr[6] */
uint32_t a3; /* cpu->gpr[7] */
uint32_t s3; /* cpu->gpr[19] — callee-saved, useful for arg-passing chains */
uint32_t sp; /* cpu->gpr[29] — for func_8001A954 SP/RA-lifecycle oracle */
} FntraceEntry;
extern FntraceEntry g_fntrace_ring[FNTRACE_RING_CAP];
extern uint64_t g_fntrace_seq; /* monotonic; index into ring = seq % CAP */
/* Hot path. Called from psx_dispatch on every entry. Inlinable. */
void fntrace_record(CPUState* cpu, uint32_t target);
/* Register the game's text range for one-shot game-start detection.
* First dispatch into [lo, hi) calls cdrom_notify_game_started(). */
void fntrace_set_game_range(uint32_t lo, uint32_t hi);
int fntrace_is_game_started(void);
/* Arm a target filter. arm_count == 0 means "record all" (default).
* When arm_count > 0, only dispatches whose target matches one of the
* armed entries are recorded. Use for noise reduction on a hot ring. */
#define FNTRACE_ARM_MAX 32
void fntrace_arm(uint32_t target); /* add to arm list, or 0 to clear */
void fntrace_arm_clear(void);
uint32_t fntrace_arm_count(void);
uint32_t fntrace_arm_get(uint32_t i);
void fntrace_arm_from_env(const char *env_name);
/* Reset ring (clears seq, leaves storage in place). */
void fntrace_clear(void);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_FNTRACE_H */