mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
ae47e69902
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>
75 lines
2.9 KiB
C
75 lines
2.9 KiB
C
/* 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 */
|