mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
cyc_watch: arm boot anchors from power-on on both backends
Boot anchors (reset, the kernel copy, the first A0/B0/C0 calls, the shell)
pass within the first frames, before a TCP cyc_watch arm can land, so a
native-vs-Beetle boot comparison could not be run from the tree.
psx-runtime (debug_server.c) and psx-beetle (beetle_main.cpp) now read
PSX_CYC_WATCH ("<pc>" or "<pc>-<end>" for region mode) and PSX_CYC_WATCH_N
(default 16) at start-up and arm the watch before the first guest
instruction. The native arm shares the TCP handler's code path
(cyc_watch_arm); psx-beetle calls beetle_cyc_watch_arm. With neither
variable set nothing changes. tools/cycle_compare.py --no-arm then reads
both rings.
PSX_BEETLE_UNPACED=1 skips psx-beetle's 59.94 Hz wall-clock pacing for
headless oracle runs. Guest timing is unaffected.
docs/beetle-macos.md step 7 documents the variables and the anchor-key
difference for BIOS copy windows declared dispatch_key = "rom": the shell
is keyed natively at its ROM address (OpenBIOS 0xBFC0A500, SCPH-1001
0xBFC18000) while Beetle sees 0x80030000. Checked on macOS: the documented
example records the OpenBIOS shell entry at 5,015,670 in Beetle, the value
of the #429 land gate, and the native watch arms from boot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -156,3 +156,49 @@ These are the recorded oracle values. `icache_miss` is 19 (+16) on the current
|
||||
ROM and 17 (+14) on the 13-loop ROM of 2026-06-27, because the loop moved
|
||||
within its cache line (see the README). If a build disagrees, fix the oracle
|
||||
before comparing anything against it.
|
||||
|
||||
## 7. Boot anchors: arm cyc_watch from power-on
|
||||
|
||||
Reset, the kernel copy, the first A0/B0/C0 calls and the shell pass within the
|
||||
first frames, before a TCP `cyc_watch` arm can land. Both backends therefore
|
||||
read the watch from their environment at start-up:
|
||||
|
||||
| variable | meaning |
|
||||
|---|---|
|
||||
| `PSX_CYC_WATCH` | anchor PC, or `<pc>-<end>` for region mode (each hit is one pass's cycle count) |
|
||||
| `PSX_CYC_WATCH_N` | hits to record (default 16) |
|
||||
| `PSX_BEETLE_UNPACED=1` | psx-beetle only: skip the 59.94 Hz wall-clock pacing. Guest timing is unchanged; headless boots just finish sooner |
|
||||
|
||||
Read the rings with `tools/cycle_compare.py <pc> --no-arm` (or `cyc_watch_dump`
|
||||
on each port). For an LLE boot on the native side, set `PSX_BIOS_HLE=0`.
|
||||
|
||||
Anchor keys can differ between the two sides. A BIOS copy window declared with
|
||||
`dispatch_key = "rom"` keeps its ROM address as the native key; Beetle sees the
|
||||
PC the code runs at. Both `bios/OpenBIOS.toml` and `bios/SCPH1001.toml` declare
|
||||
the shell that way:
|
||||
|
||||
| anchor | Beetle | native |
|
||||
|---|---|---|
|
||||
| OpenBIOS shell entry | `0x80030000` | `0xBFC0A500` |
|
||||
| SCPH-1001 shell entry | `0x80030000` | `0xBFC18000` |
|
||||
|
||||
A native watch on `0x80030000` records nothing. Kernel windows
|
||||
(`dispatch_key = "ram"`) and ROM code use the same PC on both sides.
|
||||
|
||||
Example, OpenBIOS shell entry (the cycle-test disc from step 6 inserted):
|
||||
|
||||
```bash
|
||||
PSX_CYC_WATCH=0x80030000 PSX_CYC_WATCH_N=1 PSX_BEETLE_UNPACED=1 \
|
||||
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software \
|
||||
"$PSXRECOMP/runtime/build-beetle/psx-beetle" ~/psx-oracle/bios/scph5501.bin \
|
||||
--port 4382 --disc disc/cyctest.cue &
|
||||
PSX_CYC_WATCH=0xBFC0A500 PSX_CYC_WATCH_N=1 PSX_BIOS_HLE=0 \
|
||||
build/Cycle_Test_ROM --no-launcher --headless --game game.toml \
|
||||
--bios ../../bios/openbios.bin --disc disc/cyctest.cue --debug-port 4600 &
|
||||
python3 ../cycle_compare.py 0x80030000 --no-arm --hits 1 --native-port 4600
|
||||
```
|
||||
|
||||
(run from `tools/cycle_testrom`). Beetle records its first hit at guest cycle
|
||||
5,015,670 with `bios/openbios.bin` (SHA-1 `95419841…`). `cycle_compare.py`
|
||||
warns that `anchor_phys` differs between the backends; for a ROM-keyed window
|
||||
that is expected.
|
||||
|
||||
@@ -15,6 +15,7 @@ int beetle_init(const char* bios_path);
|
||||
int beetle_init_with_disc(const char* bios_path, const char* disc_path);
|
||||
int beetle_wtrace_arm(uint32_t lo, uint32_t hi);
|
||||
int beetle_rtrace_arm(uint32_t lo, uint32_t hi);
|
||||
void beetle_cyc_watch_arm(uint32_t anchor_raw, uint32_t end_raw, int n);
|
||||
void beetle_shutdown(void);
|
||||
void beetle_run_frame(uint16_t pad1_buttons);
|
||||
int beetle_get_framebuffer(uint32_t **out_pixels, unsigned *out_w, unsigned *out_h);
|
||||
@@ -151,6 +152,20 @@ int main(int argc, char** argv) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
beetle_debug_server_init(dbg_port);
|
||||
std::fprintf(stdout, "psx-beetle: debug server on port %d\n", dbg_port);
|
||||
|
||||
/* PSX_CYC_WATCH="<pc>" or "<pc>-<end>" + PSX_CYC_WATCH_N (default 16):
|
||||
* arm cyc_watch before the first frame, as psx-runtime does, so boot
|
||||
* anchors are recorded from power-on on both backends. */
|
||||
if (const char* cw = std::getenv("PSX_CYC_WATCH"); cw && cw[0]) {
|
||||
char* e = nullptr;
|
||||
uint32_t pc = (uint32_t)std::strtoul(cw, &e, 0);
|
||||
uint32_t end = (e && *e == '-') ? (uint32_t)std::strtoul(e + 1, nullptr, 0) : 0u;
|
||||
const char* ns = std::getenv("PSX_CYC_WATCH_N");
|
||||
int n = (ns && ns[0]) ? (int)std::strtol(ns, nullptr, 0) : 16;
|
||||
beetle_cyc_watch_arm(pc, end, n);
|
||||
std::fprintf(stdout, "psx-beetle: cyc_watch armed from boot pc=0x%08X end=0x%08X n=%d\n",
|
||||
pc, end, n);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* General control-flow parity trace (oracle producer). Armed from boot via
|
||||
@@ -250,6 +265,13 @@ int main(int argc, char** argv) {
|
||||
* sustain unlocked rate (turbo). */
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
if (keys && keys[SDL_SCANCODE_TAB]) continue;
|
||||
/* PSX_BEETLE_UNPACED=1: no wall-clock pacing, for headless oracle
|
||||
* runs (guest timing is unaffected; only the host waits less). */
|
||||
static const int unpaced = [] {
|
||||
const char* u = std::getenv("PSX_BEETLE_UNPACED");
|
||||
return (u && u[0] && u[0] != '0') ? 1 : 0;
|
||||
}();
|
||||
if (unpaced) continue;
|
||||
constexpr double FRAME_MS = 1000.0 / 59.94;
|
||||
static FramePacer pacer = { 0 };
|
||||
frame_pacer_wait(&pacer, FRAME_MS);
|
||||
|
||||
+37
-15
@@ -9807,24 +9807,12 @@ static void handle_wtrace_ranges(int id, const char *json)
|
||||
|
||||
/* ---- cyc_watch command handlers (see cyc_watch_observe above) ---- */
|
||||
|
||||
/* cyc_watch — arm an anchor PC. {"pc":"0x...","n":16}. Clears the ring,
|
||||
* masks the anchor to physical, and starts recording. The Beetle side
|
||||
* (psx-beetle, added parent-side) implements the SAME command/spec. */
|
||||
static void handle_cyc_watch(int id, const char *json)
|
||||
/* Arm cyc_watch: clear the ring, mask the anchor(s) to physical, start
|
||||
* recording. end_raw != 0 selects REGION mode (each entry = Δcycles A->B). */
|
||||
static void cyc_watch_arm(uint32_t raw, uint32_t end_raw, int n)
|
||||
{
|
||||
char pcbuf[64];
|
||||
if (!json_get_str(json, "pc", pcbuf, sizeof(pcbuf))) {
|
||||
send_err(id, "cyc_watch requires pc");
|
||||
return;
|
||||
}
|
||||
uint32_t raw = hex_to_u32(pcbuf);
|
||||
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;
|
||||
@@ -9839,6 +9827,22 @@ static void handle_cyc_watch(int id, const char *json)
|
||||
s_cyc_watch_last_cycle = 0xFFFFFFFFFFFFFFFFull;
|
||||
memset(s_cyc_watch_ring, 0, sizeof(s_cyc_watch_ring));
|
||||
s_cyc_watch_armed = 1;
|
||||
}
|
||||
|
||||
/* cyc_watch — arm an anchor PC. {"pc":"0x...","n":16}. Clears the ring,
|
||||
* masks the anchor to physical, and starts recording. The Beetle side
|
||||
* (psx-beetle, added parent-side) implements the SAME command/spec. */
|
||||
static void handle_cyc_watch(int id, const char *json)
|
||||
{
|
||||
char pcbuf[64];
|
||||
if (!json_get_str(json, "pc", pcbuf, sizeof(pcbuf))) {
|
||||
send_err(id, "cyc_watch requires pc");
|
||||
return;
|
||||
}
|
||||
/* 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;
|
||||
cyc_watch_arm(hex_to_u32(pcbuf), end_raw, json_get_int(json, "n", 16));
|
||||
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"anchor\":\"0x%08X\","
|
||||
"\"anchor_phys\":\"0x%08X\",\"end\":\"0x%08X\",\"end_phys\":\"0x%08X\","
|
||||
@@ -14275,6 +14279,24 @@ void debug_server_init(int port)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* PSX_CYC_WATCH="<pc>" or "<pc>-<end>" (region mode) arms cyc_watch
|
||||
* before the first guest instruction, with PSX_CYC_WATCH_N hits
|
||||
* (default 16). Boot anchors (reset, the kernel copy, the first
|
||||
* syscalls, the shell) pass before any TCP arm can land. psx-beetle
|
||||
* reads the same variables, so one boot of each backend gives the
|
||||
* same anchor from cycle 0 (tools/cycle_compare.py --no-arm). */
|
||||
{
|
||||
const char *cw = getenv("PSX_CYC_WATCH");
|
||||
if (cw && *cw) {
|
||||
char *e = NULL;
|
||||
uint32_t pc = (uint32_t)strtoul(cw, &e, 0);
|
||||
uint32_t end = (e && *e == '-') ? (uint32_t)strtoul(e + 1, NULL, 0) : 0u;
|
||||
const char *ns = getenv("PSX_CYC_WATCH_N");
|
||||
cyc_watch_arm(pc, end, (ns && *ns) ? (int)strtol(ns, NULL, 0) : 16);
|
||||
fprintf(stdout, "psxrecomp: cyc_watch armed from boot pc=0x%08X end=0x%08X n=%u\n",
|
||||
pc, end, s_cyc_watch_max_hits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -19,6 +19,16 @@ The native side (psx-runtime) implements `cyc_watch` / `cyc_watch_dump` /
|
||||
`cyc_watch_clear` in runtime/src/debug_server.c. The Beetle side
|
||||
(psx-beetle) implements the SAME commands to the SAME spec (added parent-side).
|
||||
|
||||
BOOT ANCHORS: reset, the kernel copy, the first syscalls and the shell pass in
|
||||
the first frames, before a TCP arm can land. Arm them from power-on instead:
|
||||
start each backend with PSX_CYC_WATCH=<pc> (or <pc>-<end> for REGION mode) and
|
||||
PSX_CYC_WATCH_N=<hits> (default 16) in its environment, then run this tool with
|
||||
--no-arm to dump and diff. The anchor key can differ per side: a BIOS copy
|
||||
window with dispatch_key = "rom" (the shell, in both bios/OpenBIOS.toml and
|
||||
bios/SCPH1001.toml) is keyed natively at its ROM address, so the OpenBIOS shell
|
||||
entry is 0x80030000 in Beetle but 0xBFC0A500 natively (SCPH-1001: 0xBFC18000).
|
||||
Windows with dispatch_key = "ram" (the kernels) use the RAM PC on both sides.
|
||||
|
||||
Wire format (request):
|
||||
{"cmd":"cyc_watch","pc":"0x80012345","n":16}
|
||||
{"cmd":"cyc_watch_dump"}
|
||||
|
||||
Reference in New Issue
Block a user