diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp index 14509ca..faf89e3 100644 --- a/XenonRecomp/recompiler.cpp +++ b/XenonRecomp/recompiler.cpp @@ -1653,6 +1653,18 @@ bool Recompiler::Recompile( println("\t{}.compare({}.s32, 0, {});", cr(0), r(insn.operands[0]), xer()); break; + // PPC 405/Xenon: multiply-accumulate halfwords into RT (low 32 bits). + // macchwu: high halfwords unsigned; maclhwu: low halfwords unsigned. + case PPC_INST_MACCHWU: + println("\t{}.u64 = uint32_t({}.u32 + uint32_t(uint16_t({}.u32 >> 16)) * uint32_t(uint16_t({}.u32 >> 16)));", + r(insn.operands[0]), r(insn.operands[0]), r(insn.operands[1]), r(insn.operands[2])); + break; + + case PPC_INST_MACLHWU: + println("\t{}.u64 = uint32_t({}.u32 + uint32_t(uint16_t({}.u32)) * uint32_t(uint16_t({}.u32)));", + r(insn.operands[0]), r(insn.operands[0]), r(insn.operands[1]), r(insn.operands[2])); + break; + case PPC_INST_MULLD: println("\t{}.s64 = {}.s64 * {}.s64;", r(insn.operands[0]), r(insn.operands[1]), r(insn.operands[2])); break; @@ -2111,7 +2123,9 @@ bool Recompiler::Recompile( println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_and_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2])); break; + case PPC_INST_VANDC: case PPC_INST_VANDC128: + // andnot(a,b) == b & ~a; PPC vandc VD,VA,VB is VA & ~VB → args ordered VB, VA for andnot println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_andnot_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1])); break; @@ -2389,6 +2403,12 @@ bool Recompiler::Recompile( println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_packus_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1])); break; + case PPC_INST_VPKSWSS: + case PPC_INST_VPKSWSS128: + // Pack signed 32-bit → signed 16-bit with signed saturation (vector endian: VB then VA). + println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_packs_epi32(simde_mm_load_si128((simde__m128i*){}.s32), simde_mm_load_si128((simde__m128i*){}.s32)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1])); + break; + case PPC_INST_VREFP: case PPC_INST_VREFP128: // TODO: see if we can use rcp safely diff --git a/rb2_test/VERIFY_REPORT.md b/rb2_test/VERIFY_REPORT.md new file mode 100644 index 0000000..aa0b107 --- /dev/null +++ b/rb2_test/VERIFY_REPORT.md @@ -0,0 +1,151 @@ +# Prove RB2 — verification report + +**Goal:** Prove Raging Blast 2 recomp quality with the same *evidence bar* as Burst Limit +(not “RB2 is as clean as Burst Limit”). + +| | | +|--|--| +| **Current tip** | `pr/burst-limit-opcodes` @ `49bd935` | +| **Main** | `ddd128b` (`XenonRecomp-main` build) | +| **XEX** | `rb2_test/extracted/default.xex` (from Europe ISO) | +| **Shipped verifier** | `tools/verify_recomp_metrics.py` (+ `tools/testdata/*`) | + +**Non-claims:** No runtime/boot/playable proof. + +--- + +## Acceptance criteria + +| # | Criterion | Result | +|---|-----------|--------| +| 1 | Same XEX/config A/B main vs current; exit 0; metrics for switch_outside, unrecognized, switch count | **PASS** | +| 2 | Minimal config AUTO rest/save: 8 helpers on current | **PASS** (8/8) | +| 3 | Jump-table status settled: empty Analyse → switch KPI **N/A** | **PASS** (N/A documented) | +| 4 | This write-up with table; no boot claim | **PASS** | + +--- + +## 1. Same-config A/B (full TOML, same rest/save) + +Config: `rb2_test/post/config.toml` and `rb2_test/pre/config.toml` (identical addresses + empty jumptable). + +| Metric | **main** | **current** | +|--------|----------|-------------| +| Exit code | **0** | **0** | +| `ppc` files | 87 | 87 | +| `switch_table_count` | **0** | **0** | +| `switch_outside_function` | **0** (log) | **0** (report) | +| Unrecognized instruction **lines** | **9956** | **9819** (−137) | +| `function_count` | — | **21124** | +| Recomp time | ~1.9 s | ~2.0 s | + +**Current report** (`recomp_report.json`): + +```json +function_count: 21124 +switch_table_count: 0 +unrecognized_instructions: 9819 +switch_outside_function: 0 +``` + +**Main** has no report file; metrics from log: +- `Unrecognized instruction` line count = 9956 +- `jump outside function` line count = 0 + +Scratch copies: `{SCRATCH}/rb2_current_report.json`, `{SCRATCH}/rb2_main.log`, `{SCRATCH}/rb2_main_metrics.json`. + +### Interpretation + +- **Finish A/B:** both complete successfully. +- **Switch KPI:** **N/A** for quality comparison (see §3) — both 0 because there are **no** switch tables. +- **Unrecognized:** current slightly better (−137 lines, e.g. `vpkswss*` from opcode PR); **still a large cliff** (~9.8k). Not “proven clean.” + +--- + +## 2. Auto rest/save (minimal TOML, current only) + +Config: `rb2_test/post/config_auto.toml` (no rest/save keys). + +| Helper | Detected | +|--------|----------| +| `__restgprlr_14` | 0x8247DD50 | +| `__savegprlr_14` | 0x8247DD00 | +| `__restfpr_14` | 0x8247EE8C | +| `__savefpr_14` | 0x8247EE40 | +| `__restvmx_14` | 0x8247F318 | +| `__savevmx_14` | 0x8247F080 | +| `__restvmx_64` | 0x8247F3AC | +| `__savevmx_64` | 0x8247F114 | + +**8/8 AUTO, exit 0.** Log: `{SCRATCH}/rb2_auto_rest.log`. + +**Verdict:** Auto rest/save is **proven on a second title** (not Burst Limit–only). + +--- + +## 3. Jump-table / switch KPI status + +XenonAnalyse on the same XEX → `{SCRATCH}/rb2_jumptable.toml`: + +```text +# Generated by XenonAnalyse +# ---- ABSOLUTE JUMPTABLE ---- +# ---- COMPUTED JUMPTABLE ---- +# ---- OFFSETED JUMPTABLE ---- +``` + +| `[[switch]]` count | **0** | +|--------------------|------:| + +**Switch KPI: N/A** — not a silent pass. +Switch-aware analysis cannot improve RB2 until Analyse finds tables (pattern work is out of this goal’s non-goals). + +--- + +## 4. Unrecognized histogram (optional evidence) + +Top opcodes (line counts): + +| Opcode | main | current | +|--------|-----:|--------:| +| vslh | 2420 | 2420 | +| bdzf | 2097 | 2097 | +| vsrah | 1024 | 1024 | +| vsubshs | 928 | 928 | +| vspltish | 609 | 609 | +| … | … | … | +| vpkswss128 | 121 (main top-15) | reduced/absent in top (current covers) | + +Files: `{SCRATCH}/rb2_unrec_hist_main.txt`, `rb2_unrec_hist_current.txt`. + +--- + +## 5. Committed verifier (drives real artifact format) + +```text +python tools/verify_recomp_metrics.py \ + --report \ + --auto-log \ + --jumptable \ + --expect-switch-na \ + --min-unrecognized 1000 +``` + +Run on live RB2 scratch artifacts: **ALL CHECKS PASSED** +Fixture self-check: `tools/testdata/*` also passes the same tool. + +--- + +## Bottom line — “has RB2 been proven?” + +| Claim | Proven? | +|-------|---------| +| Current recompiles RB2 end-to-end | **Yes** | +| Main vs current A/B same config | **Yes** | +| Auto rest/save on RB2 | **Yes (8/8)** | +| Switch quality better on RB2 like Burst Limit | **N/A** (0 tables) | +| Opcode coverage “good enough” | **No** (~9819 unrecognized) | +| Game boots | **Not tested** (non-goal) | + +**RB2 is proven as a second-title recomp evidence package:** finish A/B, AUTO, switch N/A with evidence, unrecognized cliff quantified. +**RB2 is not proven “as good as Burst Limit.”** diff --git a/tools/testdata/sample_auto_rest.log b/tools/testdata/sample_auto_rest.log new file mode 100644 index 0000000..edf114c --- /dev/null +++ b/tools/testdata/sample_auto_rest.log @@ -0,0 +1,8 @@ +AUTO: __restgprlr_14 = 0x8247DD50 (1 match) +AUTO: __savegprlr_14 = 0x8247DD00 (1 match) +AUTO: __restfpr_14 = 0x8247EE8C (1 match) +AUTO: __savefpr_14 = 0x8247EE40 (1 match) +AUTO: __restvmx_14 = 0x8247F318 (1 match) +AUTO: __savevmx_14 = 0x8247F080 (1 match) +AUTO: __restvmx_64 = 0x8247F3AC (1 match) +AUTO: __savevmx_64 = 0x8247F114 (1 match) diff --git a/tools/testdata/sample_empty_jumptable.toml b/tools/testdata/sample_empty_jumptable.toml new file mode 100644 index 0000000..9737723 --- /dev/null +++ b/tools/testdata/sample_empty_jumptable.toml @@ -0,0 +1,4 @@ +# Generated by XenonAnalyse +# ---- ABSOLUTE JUMPTABLE ---- +# ---- COMPUTED JUMPTABLE ---- +# ---- OFFSETED JUMPTABLE ---- diff --git a/tools/testdata/sample_recomp_report.json b/tools/testdata/sample_recomp_report.json new file mode 100644 index 0000000..695cea1 --- /dev/null +++ b/tools/testdata/sample_recomp_report.json @@ -0,0 +1,19 @@ +{ + "function_count": 21124, + "switch_table_count": 0, + "unrecognized_instructions": 9819, + "decode_failures": 0, + "switch_outside_function": 0, + "premature_function_ends": 0, + "restgprlr_14": "0x8247DD50", + "savegprlr_14": "0x8247DD00", + "restfpr_14": "0x8247EE8C", + "savefpr_14": "0x8247EE40", + "restvmx_14": "0x8247F318", + "savevmx_14": "0x8247F080", + "restvmx_64": "0x8247F3AC", + "savevmx_64": "0x8247F114", + "notes": [ + "switch tables: 0, mapped to bctr: 0" + ] +} diff --git a/tools/verify_recomp_metrics.py b/tools/verify_recomp_metrics.py new file mode 100644 index 0000000..5292ebc --- /dev/null +++ b/tools/verify_recomp_metrics.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +Verify recomp_report.json / AUTO logs produced by XenonRecomp (shipped tool output). + +Does not reimplement the recompiler. Validates real artifacts from XenonRecomp.exe: + - report JSON keys and types written by Recompiler::WriteReport + - optional AUTO: lines from AutoDetectSaveRestoreHelpers + - optional jump-table TOML switch count from XenonAnalyse + +Usage: + python tools/verify_recomp_metrics.py --report path/to/recomp_report.json + python tools/verify_recomp_metrics.py --report ... --auto-log ... --jumptable ... + python tools/verify_recomp_metrics.py --expect-auto 8 --expect-switch-na +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +REQUIRED_REPORT_KEYS = { + "function_count": int, + "switch_table_count": int, + "unrecognized_instructions": int, + "decode_failures": int, + "switch_outside_function": int, + "premature_function_ends": int, +} + +AUTO_HELPERS = ( + "__restgprlr_14", + "__savegprlr_14", + "__restfpr_14", + "__savefpr_14", + "__restvmx_14", + "__savevmx_14", + "__restvmx_64", + "__savevmx_64", +) + +AUTO_RE = re.compile( + r"^AUTO:\s+(__restgprlr_14|__savegprlr_14|__restfpr_14|__savefpr_14|" + r"__restvmx_14|__savevmx_14|__restvmx_64|__savevmx_64)\s*=\s*0x([0-9A-Fa-f]+)", + re.M, +) + + +def verify_report(path: Path) -> dict: + data = json.loads(path.read_text(encoding="utf-8")) + for key, typ in REQUIRED_REPORT_KEYS.items(): + if key not in data: + raise SystemExit(f"FAIL report missing key: {key}") + if not isinstance(data[key], typ): + raise SystemExit(f"FAIL report {key} type {type(data[key])} want {typ}") + if data[key] < 0: + raise SystemExit(f"FAIL report {key} negative: {data[key]}") + if data["function_count"] <= 0: + raise SystemExit("FAIL function_count must be > 0 for a real recomp") + print( + f"OK report: functions={data['function_count']} " + f"switches={data['switch_table_count']} " + f"switch_outside={data['switch_outside_function']} " + f"unrecognized={data['unrecognized_instructions']}" + ) + return data + + +def _read_text(path: Path) -> str: + raw = path.read_bytes() + if raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"): + return path.read_text(encoding="utf-16") + if raw.startswith(b"\xef\xbb\xbf"): + return path.read_text(encoding="utf-8-sig") + # PowerShell *>> sometimes writes UTF-16 without relying on BOM alone + if b"\x00" in raw[:64]: + return path.read_text(encoding="utf-16") + return path.read_text(encoding="utf-8", errors="replace") + + +def verify_auto_log(path: Path, expect: int) -> dict[str, str]: + text = _read_text(path) + found = {m.group(1): m.group(2) for m in AUTO_RE.finditer(text)} + missing = [h for h in AUTO_HELPERS if h not in found] + if missing: + raise SystemExit(f"FAIL AUTO missing helpers: {missing}") + if len(found) != expect: + raise SystemExit(f"FAIL AUTO count {len(found)} != expect {expect}") + for name, va in found.items(): + if int(va, 16) == 0: + raise SystemExit(f"FAIL AUTO {name} is zero") + print(f"OK AUTO: {len(found)}/{expect} helpers non-zero") + return found + + +def count_switches(path: Path) -> int: + text = path.read_text(encoding="utf-8", errors="replace") + return len(re.findall(r"^\[\[switch\]\]", text, re.M)) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--report", type=Path, help="recomp_report.json from XenonRecomp") + ap.add_argument("--auto-log", type=Path, help="log containing AUTO: lines") + ap.add_argument("--expect-auto", type=int, default=8) + ap.add_argument("--jumptable", type=Path, help="XenonAnalyse TOML") + ap.add_argument( + "--expect-switch-na", + action="store_true", + help="Require jumptable to have zero [[switch]] entries (RB2-style N/A)", + ) + ap.add_argument( + "--min-unrecognized", + type=int, + default=None, + help="Optional lower bound on unrecognized_instructions (prove cliff exists)", + ) + args = ap.parse_args() + + if not args.report and not args.auto_log and not args.jumptable: + ap.error("need at least one of --report / --auto-log / --jumptable") + + data = None + if args.report: + data = verify_report(args.report) + if args.min_unrecognized is not None: + if data["unrecognized_instructions"] < args.min_unrecognized: + raise SystemExit( + f"FAIL unrecognized {data['unrecognized_instructions']} " + f"< min {args.min_unrecognized}" + ) + print(f"OK unrecognized >= {args.min_unrecognized}") + + if args.auto_log: + verify_auto_log(args.auto_log, args.expect_auto) + + if args.jumptable: + n = count_switches(args.jumptable) + print(f"OK jumptable switches={n}") + if args.expect_switch_na and n != 0: + raise SystemExit(f"FAIL expected switch N/A (0 tables) got {n}") + if args.expect_switch_na: + print("OK switch KPI marked N/A (0 [[switch]] entries)") + if data is not None and data["switch_table_count"] != n: + # report uses config switch map size; empty TOML => 0 + if n == 0 and data["switch_table_count"] == 0: + pass + elif n != data["switch_table_count"]: + print( + f"WARN report switch_table_count={data['switch_table_count']} " + f"vs TOML {n}" + ) + + print("ALL CHECKS PASSED") + return 0 + + +if __name__ == "__main__": + sys.exit(main())