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:
Matthew Stanley
2026-06-26 21:18:26 -07:00
parent ee3588bc5c
commit 2b5e69fe8f
2 changed files with 70 additions and 14 deletions
+56 -9
View File
@@ -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
View File
@@ -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 ...")