Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions XenonRecomp/recompiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,18 @@ bool Recompiler::Recompile(
println("\t{}.compare<int32_t>({}.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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
151 changes: 151 additions & 0 deletions rb2_test/VERIFY_REPORT.md
Original file line number Diff line number Diff line change
@@ -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 <recomp_report.json from XenonRecomp.exe> \
--auto-log <log with AUTO: lines> \
--jumptable <XenonAnalyse toml> \
--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.”**
8 changes: 8 additions & 0 deletions tools/testdata/sample_auto_rest.log
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions tools/testdata/sample_empty_jumptable.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by XenonAnalyse
# ---- ABSOLUTE JUMPTABLE ----
# ---- COMPUTED JUMPTABLE ----
# ---- OFFSETED JUMPTABLE ----
19 changes: 19 additions & 0 deletions tools/testdata/sample_recomp_report.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
160 changes: 160 additions & 0 deletions tools/verify_recomp_metrics.py
Original file line number Diff line number Diff line change
@@ -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())
Loading