mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
feat(timing): ruler #2 Beetle half live — synthetic boot disc + oracle component costs
The test ROM now boots on a real BIOS. The license region (required or the BIOS drops to its shell) is extracted from a disc the developer OWNS via the in-tree dumpsxiso and embedded by mkpsxiso — LOCAL ONLY, gitignored, never redistributed. Both backends boot disc/cyctest.cue identically (real BIOS -> SYSTEM.CNF -> EXE), so CPU/cache state matches. - disc/cyctest.xml + disc/SYSTEM.CNF: mkpsxiso project (tracked). license_data.dat + built .bin/.cue gitignored (copyrighted Sony data, local only). - measure.py: per-anchor consecutive-hit Δ = per-iteration cycle cost; reports per-component cost via baseline subtraction. - README: boot-disc build + license-extraction recipe + the oracle results. Beetle ORACLE component costs (the HW targets for the cost model): baseline 3 | alu +1 | load +5 | load2 +11 (2nd load +1 = ReadFudge) | div +38 (~36 stall) | div_spaced +38 (fillers ABSORBED) | mult +15 (~13 stall) Native currently charges 0 for div/mult stall — the dominant gaps. div_spaced==div proves the stall must be a completion-timestamp + stall-on-mflo-read (absorbable), NOT a flat charge on the divu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,12 @@
|
||||
cycle_testrom.exe
|
||||
*.anchors.json
|
||||
generated/
|
||||
|
||||
# Synthetic boot disc — LOCAL ONLY, never commit/push.
|
||||
# disc/license_data.dat is the PlayStation license region extracted from a disc
|
||||
# the developer OWNS (via dumpsxiso). It is copyrighted Sony data: do NOT commit,
|
||||
# do NOT redistribute. Each developer extracts their own from a disc they own
|
||||
# (see README "Boot disc"). The built .bin/.cue embed it, so they are ignored too.
|
||||
disc/license_data.dat
|
||||
disc/*.bin
|
||||
disc/*.cue
|
||||
|
||||
@@ -43,17 +43,59 @@ Each component is then transcribed/calibrated into `psx_instr_base_cycles`
|
||||
(execute latencies: mult/div, GTE) or the memory-path wait-state (`memory.c`),
|
||||
Δ-gated against Beetle on these loops + FMV-no-regression, one at a time.
|
||||
|
||||
## Regenerate
|
||||
## Boot disc (how the EXE reaches both backends)
|
||||
|
||||
The real BIOS only boots a disc that carries the PlayStation license region; a
|
||||
license-less disc drops to the BIOS shell (Memory-Card/CD-Player menu). So the
|
||||
test EXE ships on a synthetic disc built with the in-tree `tools/mkpsxiso`:
|
||||
|
||||
```bash
|
||||
cd tools/cycle_testrom
|
||||
python gen_testrom.py cycle_testrom.exe # writes the EXE + .anchors.json
|
||||
# recompile for native:
|
||||
../../recompiler/build-t2/psxrecomp-game.exe --config game.toml
|
||||
python gen_testrom.py cycle_testrom.exe # EXE + .anchors.json
|
||||
../../recompiler/build-t2/psxrecomp-game.exe --config game.toml # recompile (native)
|
||||
|
||||
# ONE-TIME: extract the license region from a disc YOU OWN (local only, never
|
||||
# redistributed). dumpsxiso writes license_data.dat from the disc system area:
|
||||
DUMP=../mkpsxiso/mkpsxiso-2.20-win64/dumpsxiso.exe
|
||||
"$DUMP" -x /tmp/own_disc -s /tmp/own_disc/x.xml "<path to a PS1 disc you own>.cue"
|
||||
cp /tmp/own_disc/license_data.dat disc/license_data.dat # gitignored, stays local
|
||||
|
||||
# build the disc (embeds license + SYSTEM.CNF + cyctest.exe):
|
||||
cd disc && ../../mkpsxiso/mkpsxiso-2.20-win64/mkpsxiso.exe -y cyctest.xml
|
||||
```
|
||||
|
||||
Tracked: `gen_testrom.py`, `game.toml`, `seeds.txt`, this README.
|
||||
Ignored (regenerable): `cycle_testrom.exe`, `*.anchors.json`, `generated/`.
|
||||
`disc/license_data.dat` and the built `disc/*.bin`/`*.cue` are **gitignored** —
|
||||
copyrighted Sony data, local only. Everything else (gen, xml, SYSTEM.CNF) is
|
||||
tracked, so each developer reproduces the disc from a disc they own.
|
||||
|
||||
## Measure
|
||||
|
||||
```bash
|
||||
# both backends boot the same disc/cyctest.cue:
|
||||
# beetle: psx-beetle <bios> --disc disc/cyctest.cue --port 4382
|
||||
# native: <test-runtime> ... --disc disc/cyctest.cue (TODO: test-runtime)
|
||||
python measure.py --port 4382 # Beetle oracle: per-iter + per-component delta
|
||||
python measure.py --port 4500 # native cost model (once the test-runtime exists)
|
||||
```
|
||||
|
||||
## Beetle ORACLE results (2026-06-26 — the HW cost targets)
|
||||
|
||||
Per-iteration cycle delta, and component cost (minus baseline=3):
|
||||
|
||||
| loop | per-iter | component | reads as |
|
||||
|-------------|----------|-----------|----------|
|
||||
| baseline | 3 | — | addiu+bne+nop, base 1 each |
|
||||
| alu | 4 | +1 | one addu (pure execute) |
|
||||
| load | 8 | +5 | one main-RAM lw (result unused → no absorb) |
|
||||
| load2 | 14 | +11 | 2 lw: 5 + 6 (2nd load's +1 = ReadFudge) |
|
||||
| div | 41 | +38 | divu+mflo base 2 + ~36 div stall |
|
||||
| div_spaced | 41 | +38 | divu+2addu+mflo — fillers ABSORBED by stall |
|
||||
| mult | 18 | +15 | multu+mflo base 2 + ~13 mult stall |
|
||||
|
||||
Key targets for the native cost model: **div stall ~36, mult stall ~13** (native
|
||||
charges 0). Load ~5/6 (native memory.c flat +6 — close). **`div_spaced`==`div`
|
||||
proves the stall must be modeled as a completion-timestamp + stall-on-mflo-read
|
||||
(fillers absorb it), NOT a flat charge on the divu.**
|
||||
|
||||
## Status / TODO
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
BOOT = cdrom:\CYCTEST.EXE;1
|
||||
TCB = 4
|
||||
EVENT = 10
|
||||
STACK = 801FFF00
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<iso_project image_name="cyctest.bin" cue_sheet="cyctest.cue" no_xa="0">
|
||||
<track type="data" xa_edc="true" new_type="true">
|
||||
<identifiers
|
||||
system = "PLAYSTATION"
|
||||
application = "PLAYSTATION"
|
||||
volume = "CYCTEST"
|
||||
volume_set = "CYCTEST"
|
||||
publisher = "PSXRECOMP"
|
||||
data_preparer= "MKPSXISO"
|
||||
copyright = "SYSTEM.CNF;1"
|
||||
/>
|
||||
<license file="license_data.dat"/>
|
||||
<directory_tree>
|
||||
<file name="system.cnf" type="data" source="SYSTEM.CNF"/>
|
||||
<file name="cyctest.exe" type="data" source="../cycle_testrom.exe"/>
|
||||
</directory_tree>
|
||||
</track>
|
||||
</iso_project>
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""measure.py — per-iteration cycle delta for each test-ROM anchor on one backend.
|
||||
|
||||
Arms cyc_watch single-anchor on each loop top, dumps, and reports the modal
|
||||
consecutive-hit delta = one iteration's cycle cost. Run against Beetle (4382,
|
||||
the oracle) and native (the cost model) and compare.
|
||||
|
||||
Usage: python measure.py [--port 4382] [--json cycle_testrom.exe.anchors.json]
|
||||
"""
|
||||
import argparse, collections, json, socket, sys, time
|
||||
|
||||
def query(port, cmd, host="127.0.0.1", timeout=10.0):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout); s.connect((host, port))
|
||||
try:
|
||||
s.sendall((json.dumps(cmd) + "\n").encode())
|
||||
buf = bytearray(); depth = 0; instr = False; esc = False; started = False
|
||||
while True:
|
||||
ch = s.recv(1 << 20)
|
||||
if not ch: break
|
||||
buf.extend(ch)
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
c = buf[i]
|
||||
if instr:
|
||||
if esc: esc = False
|
||||
elif c == 0x5C: esc = True
|
||||
elif c == 0x22: instr = False
|
||||
elif c == 0x22: instr = True
|
||||
elif c == 0x7B: depth += 1; started = True
|
||||
elif c == 0x7D:
|
||||
depth -= 1
|
||||
if started and depth == 0:
|
||||
return json.loads(buf[:i+1].decode())
|
||||
i += 1
|
||||
finally:
|
||||
s.close()
|
||||
return json.loads(bytes(buf).decode())
|
||||
|
||||
def modal_delta(entries):
|
||||
cy = [int(e["cycles"]) for e in entries]
|
||||
ds = [cy[i+1]-cy[i] for i in range(len(cy)-1)]
|
||||
if not ds: return None, ds
|
||||
return collections.Counter(ds).most_common(1)[0][0], ds
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--port", type=int, default=4382)
|
||||
p.add_argument("--json", default="cycle_testrom.exe.anchors.json")
|
||||
p.add_argument("--hits", type=int, default=8)
|
||||
p.add_argument("--wait", type=float, default=1.5)
|
||||
a = p.parse_args()
|
||||
meta = json.load(open(a.json))
|
||||
anchors = meta["anchors"]
|
||||
label = "beetle" if a.port == 4382 else ("native" if a.port == 4500 else f"port{a.port}")
|
||||
print(f"=== per-iteration cycle delta on {label} (port {a.port}) ===")
|
||||
results = {}
|
||||
base = None
|
||||
for name, addr in anchors.items():
|
||||
query(a.port, {"cmd": "cyc_watch", "pc": addr, "n": a.hits})
|
||||
time.sleep(a.wait)
|
||||
d = query(a.port, {"cmd": "cyc_watch_dump"})
|
||||
md, ds = modal_delta(d.get("entries", []))
|
||||
results[name] = md
|
||||
if name == "baseline": base = md
|
||||
comp = f" (component = {md - base:+d})" if (base is not None and md is not None and name != "baseline") else ""
|
||||
print(f" {name:12s} {addr} per-iter={md}{comp} hits={d.get('hits')}")
|
||||
print()
|
||||
if base is not None:
|
||||
print("Component costs (per-iter minus baseline):")
|
||||
for name, md in results.items():
|
||||
if name == "baseline" or md is None: continue
|
||||
print(f" {name:12s} {md - base:+d}")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user