mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
feat(timing): two-anchor REGION mode for cyc_watch — validated stable gate
cyc_watch + cycle_compare.py now take an optional END anchor: each recorded value is Δcycles of one START->END pass (cost of the known code path between two dispatch points). [A,A] = Δ between consecutive A hits (call-to-call cost). Mirrored EXACTLY on both backends (native debug_server.c + Beetle libretro.cpp/beetle_debug_server.c, the latter uncommitted in main checkout, additive). VALIDATED: region [0x80017FC4, 0x80017FC4] = native Δ 56 vs Beetle Δ 91, gap -35, ZERO variance across 8 passes — a stable, offset-independent per-region gate to drive Stage-2 components to convergence (faithful components shrink the gap monotonically; can't overshoot). Note: [A,B] with distinct B needs B to reliably follow A (a dispatch point on the deterministic path); B=0x8001EFFC failed (func has multiple exits) — pick wrapper/ single-exit pairs or use [A,A]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2057,19 +2057,50 @@ typedef struct {
|
||||
} CycWatchEntry;
|
||||
static CycWatchEntry s_cyc_watch_ring[CYC_WATCH_RING_CAP];
|
||||
static volatile int s_cyc_watch_armed = 0; /* 1 = recording active */
|
||||
static uint32_t s_cyc_watch_anchor_phys = 0; /* armed anchor, masked to phys */
|
||||
static uint32_t s_cyc_watch_anchor_phys = 0; /* armed anchor (A / start), masked to phys */
|
||||
static uint32_t s_cyc_watch_anchor_raw = 0; /* armed anchor as supplied (for echo) */
|
||||
static uint32_t s_cyc_watch_max_hits = 16; /* stop after this many hits */
|
||||
static uint32_t s_cyc_watch_hits = 0; /* hits recorded so far */
|
||||
/* Two-anchor REGION mode (FAITHFUL_TIMING_PLAN.md §3c): when end_phys != 0, each
|
||||
* recorded entry is the Δcycles of one A->B pass (cycles at B minus cycles at A),
|
||||
* i.e. the cost of the KNOWN code path between two dispatch points (e.g. a function
|
||||
* entry -> its exit-transfer target). end_phys == 0 keeps the single-anchor mode
|
||||
* (entry stores absolute cycles at A). Both anchors must be dispatch points (the
|
||||
* native observer only fires at function entries / compiled-dispatch / dirty
|
||||
* blocks). */
|
||||
static uint32_t s_cyc_watch_end_phys = 0; /* B / end anchor (0 = single-anchor) */
|
||||
static uint32_t s_cyc_watch_end_raw = 0;
|
||||
static int s_cyc_watch_in_region = 0; /* 1 = saw A, awaiting B */
|
||||
static uint64_t s_cyc_watch_region_start = 0;
|
||||
|
||||
/* Hot-path block-entry observer. Called from BOTH the compiled-dispatch
|
||||
* path (debug_server_trace_dispatch, arg already physical) and the
|
||||
* dirty-RAM interpreter path (debug_server_dirty_break_maybe_pause, arg
|
||||
* virtual). `block_leader_phys` must be the physical block-leader PC. */
|
||||
/* Hot-path block-entry observer. Called from the compiled-dispatch path
|
||||
* (debug_server_trace_dispatch), the universal function-entry hook
|
||||
* (debug_server_log_call_entry), and the dirty-RAM path
|
||||
* (debug_server_dirty_break_maybe_pause). `block_leader_phys` is physical. */
|
||||
static inline void cyc_watch_observe(uint32_t block_leader_phys)
|
||||
{
|
||||
if (!s_cyc_watch_armed) return; /* disarmed: no cost */
|
||||
if (block_leader_phys != s_cyc_watch_anchor_phys) return;
|
||||
|
||||
if (s_cyc_watch_end_phys != 0u) { /* ── REGION mode (A..B Δ) ── */
|
||||
if (!s_cyc_watch_in_region) {
|
||||
if (block_leader_phys == s_cyc_watch_anchor_phys) {
|
||||
s_cyc_watch_region_start = psx_get_cycle_count();
|
||||
s_cyc_watch_in_region = 1;
|
||||
}
|
||||
} else if (block_leader_phys == s_cyc_watch_end_phys) {
|
||||
uint64_t now = psx_get_cycle_count();
|
||||
CycWatchEntry *e = &s_cyc_watch_ring[s_cyc_watch_hits];
|
||||
e->hit_index = s_cyc_watch_hits;
|
||||
e->pc = block_leader_phys;
|
||||
e->psx_cycle_count = now - s_cyc_watch_region_start; /* Δ(B-A) */
|
||||
s_cyc_watch_hits++;
|
||||
s_cyc_watch_in_region = 0;
|
||||
if (s_cyc_watch_hits >= s_cyc_watch_max_hits) s_cyc_watch_armed = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (block_leader_phys != s_cyc_watch_anchor_phys) return; /* ── single-anchor ── */
|
||||
if (s_cyc_watch_hits >= s_cyc_watch_max_hits) {
|
||||
s_cyc_watch_armed = 0; /* full: stop sampling */
|
||||
return;
|
||||
@@ -7221,20 +7252,29 @@ static void handle_cyc_watch(int id, const char *json)
|
||||
int n = json_get_int(json, "n", 16);
|
||||
if (n < 1) n = 1;
|
||||
if (n > CYC_WATCH_RING_CAP) n = CYC_WATCH_RING_CAP;
|
||||
/* Optional second anchor -> REGION mode: each entry = Δcycles of one A->B pass. */
|
||||
char endbuf[64];
|
||||
uint32_t end_raw = json_get_str(json, "end", endbuf, sizeof(endbuf)) ? hex_to_u32(endbuf) : 0u;
|
||||
|
||||
/* Disarm first so the hot path can't sample mid-reset. */
|
||||
s_cyc_watch_armed = 0;
|
||||
s_cyc_watch_anchor_raw = raw;
|
||||
s_cyc_watch_anchor_phys = raw & 0x1FFFFFFFu;
|
||||
s_cyc_watch_end_raw = end_raw;
|
||||
s_cyc_watch_end_phys = end_raw & 0x1FFFFFFFu;
|
||||
s_cyc_watch_in_region = 0;
|
||||
s_cyc_watch_region_start= 0;
|
||||
s_cyc_watch_max_hits = (uint32_t)n;
|
||||
s_cyc_watch_hits = 0;
|
||||
memset(s_cyc_watch_ring, 0, sizeof(s_cyc_watch_ring));
|
||||
s_cyc_watch_armed = 1;
|
||||
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"anchor\":\"0x%08X\","
|
||||
"\"anchor_phys\":\"0x%08X\",\"max_hits\":%u}",
|
||||
"\"anchor_phys\":\"0x%08X\",\"end\":\"0x%08X\",\"end_phys\":\"0x%08X\","
|
||||
"\"region\":%d,\"max_hits\":%u}",
|
||||
id, s_cyc_watch_anchor_raw, s_cyc_watch_anchor_phys,
|
||||
s_cyc_watch_max_hits);
|
||||
s_cyc_watch_end_raw, s_cyc_watch_end_phys,
|
||||
(s_cyc_watch_end_phys != 0u) ? 1 : 0, s_cyc_watch_max_hits);
|
||||
}
|
||||
|
||||
/* cyc_watch_dump — return the recorded ring as JSON. */
|
||||
@@ -7244,9 +7284,12 @@ static void handle_cyc_watch_dump(int id, const char *json)
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"id\":%d,\"ok\":true,\"anchor\":\"0x%08X\","
|
||||
"\"anchor_phys\":\"0x%08X\",\"armed\":%d,\"max_hits\":%u,"
|
||||
"\"anchor_phys\":\"0x%08X\",\"end\":\"0x%08X\",\"end_phys\":\"0x%08X\","
|
||||
"\"region\":%d,\"armed\":%d,\"max_hits\":%u,"
|
||||
"\"hits\":%u,\"entries\":[",
|
||||
id, s_cyc_watch_anchor_raw, s_cyc_watch_anchor_phys,
|
||||
s_cyc_watch_end_raw, s_cyc_watch_end_phys,
|
||||
(s_cyc_watch_end_phys != 0u) ? 1 : 0,
|
||||
s_cyc_watch_armed ? 1 : 0, s_cyc_watch_max_hits,
|
||||
s_cyc_watch_hits);
|
||||
send_line(buf);
|
||||
@@ -7270,6 +7313,10 @@ static void handle_cyc_watch_clear(int id, const char *json)
|
||||
s_cyc_watch_hits = 0;
|
||||
s_cyc_watch_anchor_phys = 0;
|
||||
s_cyc_watch_anchor_raw = 0;
|
||||
s_cyc_watch_end_phys = 0;
|
||||
s_cyc_watch_end_raw = 0;
|
||||
s_cyc_watch_in_region = 0;
|
||||
s_cyc_watch_region_start = 0;
|
||||
memset(s_cyc_watch_ring, 0, sizeof(s_cyc_watch_ring));
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
+14
-5
@@ -153,9 +153,12 @@ def _is_unknown_command(resp):
|
||||
"no such" in err or "not found" in err or err == "")
|
||||
|
||||
|
||||
def arm(host, port, label, anchor, hits):
|
||||
def arm(host, port, label, anchor, hits, end=None):
|
||||
cmd = {"cmd": "cyc_watch", "pc": anchor, "n": hits}
|
||||
if end:
|
||||
cmd["end"] = end # two-anchor REGION mode: each entry = Δcycles of one A->B pass
|
||||
try:
|
||||
resp = query(host, port, {"cmd": "cyc_watch", "pc": anchor, "n": hits})
|
||||
resp = query(host, port, cmd)
|
||||
except (ConnectionRefusedError, TimeoutError, OSError, ConnectionError) as e:
|
||||
raise BackendError(f"{label} ({host}:{port}) unreachable: {e}")
|
||||
if _is_unknown_command(resp):
|
||||
@@ -281,7 +284,13 @@ def report(anchor, native_resp, beetle_resp):
|
||||
def main():
|
||||
p = argparse.ArgumentParser(add_help=True, description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("anchor", nargs="?", help="anchor guest PC, hex (e.g. 0x80012345)")
|
||||
p.add_argument("anchor", nargs="?", help="anchor guest PC (region START), hex (e.g. 0x80012345)")
|
||||
p.add_argument("--end", default=None,
|
||||
help="region END PC, hex. Enables two-anchor REGION mode: each "
|
||||
"recorded value is Δcycles of one START->END pass (the cost of "
|
||||
"the known code path between two dispatch points). Both anchors "
|
||||
"must be dispatch points (function entry / CPS continuation / "
|
||||
"dirty block). Omit for single-anchor (absolute-cycles-at-anchor).")
|
||||
p.add_argument("--hits", type=int, default=16)
|
||||
p.add_argument("--wait", type=float, default=5.0)
|
||||
p.add_argument("--native-port", type=int, default=DEFAULT_NATIVE_PORT)
|
||||
@@ -315,8 +324,8 @@ def main():
|
||||
|
||||
try:
|
||||
if not opts.no_arm:
|
||||
r_n = arm(host, np_, "native", anchor, opts.hits)
|
||||
r_b = arm(host, bp_, "beetle", anchor, opts.hits)
|
||||
r_n = arm(host, np_, "native", anchor, opts.hits, end=opts.end)
|
||||
r_b = arm(host, bp_, "beetle", anchor, opts.hits, end=opts.end)
|
||||
print(f"Armed native: {json.dumps(r_n)}")
|
||||
print(f"Armed beetle: {json.dumps(r_b)}")
|
||||
print(f"Letting both run for {opts.wait:.1f}s ...")
|
||||
|
||||
Reference in New Issue
Block a user