mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
2f709ee542
- Config loader (recompiler/src/config_loader.{h,cpp}): single source of
truth for both BIOS and game runtime settings (debug_port, window_title,
memcard_dir, disc path). psxrecomp-bios and psxrecomp-game both accept
--config <toml>; runtime takes --game <toml> (no more CLI overrides).
- runtime/src/main.cpp: replaced --disc/--memcard-dir/--game-root CLI
overrides with --game <toml>; game.toml is the single source of truth.
Discless launch (no args) goes to BIOS shell.
- runtime/src/dma.c: fixed update_master_irq() re-asserting I_STAT bit 3
on every DICR read. BIOS shell DMA handler at 0x80058628 only clears
DICR bit 27 (ch3 flag), so level-sensitive re-assert pinned the kernel
in the exception loop. Bit 3 is now edge-triggered (one assert per DMA
completion). Discless boot now reaches MAIN MENU / MEMORY CARD / CD
PLAYER. With-disc Tomba boots through to Whoopee Camp splash.
- Audit tooling: codegen_audit.py (BIOS), codegen_audit_game.py (game),
audit_config.py (config loader for tools), verify_audit_schema.py.
Replaces older tools/_codegen_audit.py probe.
- Docs: CURRENT_STATE.md (project state), UNIFICATION_PLAN.md (Phase C
translator merge plan), docs/config_schema.md (TOML schema reference),
docs/audit_inventory.md (audit tier inventory).
- .gitignore: tools/_*.py convention for ad-hoc scratch probes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""codegen_audit — scan a generated `_full.c` + `_dispatch.c` for codegen bugs.
|
|
|
|
Generic across any PSX program (BIOS or game). Consumes an audit-config
|
|
TOML via `--config PATH`; see `bios/SCPH1001.toml` and game `audit.toml`
|
|
for the schema. The original BIOS-only version was at `_codegen_audit.py`
|
|
and is superseded by this file.
|
|
|
|
Five passes:
|
|
1. Predicate-init declaration audit (verifies 2026-05-04 fix landed).
|
|
2. Direct psx_dispatch(cpu, 0xXXXX) targets vs dispatch table.
|
|
3. Indirect psx_dispatch(cpu, cpu->gpr[N]) site distribution.
|
|
4. Tail-call (`cpu->pc = 0xXXXX; return;`) targets vs dispatch table.
|
|
5. Chained-branch + delay-slot-as-label site population (protected by
|
|
the predicate-init fix; informational).
|
|
|
|
Usage:
|
|
python tools/codegen_audit.py --config bios/SCPH1001.toml
|
|
python tools/codegen_audit.py --config /path/to/game.toml
|
|
|
|
Exit code:
|
|
0 no real bugs (excluding expected install-at-runtime stubs)
|
|
1 buggy predicate decls present, or unexpected dispatch misses
|
|
"""
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
# Allow `python tools/codegen_audit.py` from project root
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
import audit_config
|
|
|
|
|
|
# Tail-call targets we expect to miss (install-at-runtime stubs handled
|
|
# by the dirty-RAM interpreter). Documented per program — BIOS-only for
|
|
# now; games may add their own.
|
|
EXPECTED_TAIL_CALL_MISSES = {
|
|
"SCPH1001 BIOS": {0x00000CF0}, # SIO data-byte install stub
|
|
}
|
|
|
|
|
|
def main():
|
|
cfg = audit_config.from_argv(sys.argv)
|
|
print(f"# codegen audit: {cfg.name}")
|
|
print(f" full_c = {cfg.full_c}")
|
|
print(f" dispatch_c = {cfg.dispatch_c}")
|
|
print()
|
|
|
|
if not cfg.full_c.exists():
|
|
print(f"ERROR: full_c not found: {cfg.full_c}", file=sys.stderr)
|
|
return 2
|
|
if not cfg.dispatch_c.exists():
|
|
print(f"ERROR: dispatch_c not found: {cfg.dispatch_c}", file=sys.stderr)
|
|
return 2
|
|
|
|
full_c = cfg.full_c.read_text(encoding="utf-8", errors="replace")
|
|
dispatch_c = cfg.dispatch_c.read_text(encoding="utf-8", errors="replace")
|
|
|
|
expected_misses = EXPECTED_TAIL_CALL_MISSES.get(cfg.name, set())
|
|
|
|
# ---- 1. predicate-init declaration audit ----
|
|
buggy_decls = re.findall(r'int psx_taken_[0-9A-F]+ = \([^)]*\);', full_c)
|
|
zero_decls = re.findall(r'int psx_taken_[0-9A-F]+ = 0;', full_c)
|
|
assignments = re.findall(r'^\s+psx_taken_[0-9A-F]+ = \(', full_c, re.M)
|
|
print(f"[1] predicate decl audit: buggy={len(buggy_decls)} "
|
|
f"zero_init={len(zero_decls)} assigns={len(assignments)}")
|
|
if buggy_decls:
|
|
print(" ⚠ FIX REGRESSION — found buggy declarations:")
|
|
for b in buggy_decls[:5]:
|
|
print(f" {b}")
|
|
|
|
# ---- 2. direct psx_dispatch(cpu, 0xXXXX) targets ----
|
|
direct_targets = re.findall(r'psx_dispatch\(cpu,\s*0x([0-9A-Fa-f]+)u?\);', full_c)
|
|
direct_set = set(cfg.normalize_addr(int(t, 16)) for t in direct_targets)
|
|
print(f"\n[2] direct psx_dispatch targets (literal, normalized): "
|
|
f"{len(direct_set)} unique")
|
|
|
|
table_entries = re.findall(r'\{\s*0x([0-9A-Fa-f]+)u?\s*,\s*func_[0-9A-Fa-f]+', dispatch_c)
|
|
table_set = set(cfg.normalize_addr(int(e, 16)) for e in table_entries)
|
|
print(f" dispatch table size: {len(table_set)} entries")
|
|
|
|
missing = sorted(direct_set - table_set)
|
|
print(f" direct targets MISSING from table after normalization: "
|
|
f"{len(missing)}")
|
|
for m in missing[:25]:
|
|
cnt = sum(1 for t in direct_targets
|
|
if cfg.normalize_addr(int(t, 16)) == m)
|
|
print(f" 0x{m:08X} ({cnt} call site{'s' if cnt!=1 else ''})")
|
|
|
|
# ---- 3. indirect dispatch sites (jalr/jr through gpr[N]) ----
|
|
indirect_sites = re.findall(r'psx_dispatch\(cpu,\s*cpu->gpr\[(\d+)\]\);', full_c)
|
|
print(f"\n[3] indirect psx_dispatch sites: {len(indirect_sites)} total")
|
|
gpr_use = Counter(indirect_sites)
|
|
print(f" by register: {dict(gpr_use.most_common(8))}")
|
|
|
|
# ---- 4. tail-call targets ----
|
|
tail_calls = re.findall(r'cpu->pc = 0x([0-9A-Fa-f]+)u;\s*return;', full_c)
|
|
tail_set = set(cfg.normalize_addr(int(t, 16)) for t in tail_calls)
|
|
print(f"\n[4] tail-call (cpu->pc=...; return;) targets (normalized): "
|
|
f"{len(tail_set)} unique")
|
|
tail_missing = sorted(tail_set - table_set)
|
|
unexpected_missing = sorted(set(tail_missing) - expected_misses)
|
|
print(f" tail-call targets MISSING from dispatch: {len(tail_missing)} "
|
|
f"(of which {len(unexpected_missing)} unexpected)")
|
|
for m in tail_missing[:25]:
|
|
cnt = sum(1 for t in tail_calls
|
|
if cfg.normalize_addr(int(t, 16)) == m)
|
|
tag = "" if m not in expected_misses else " [expected install-at-runtime stub]"
|
|
print(f" 0x{m:08X} ({cnt} site{'s' if cnt!=1 else ''}){tag}")
|
|
|
|
# ---- 5. chained-branch + delay-slot-as-label population ----
|
|
labels = set(re.findall(r'^label_([0-9A-F]+):', full_c, re.M))
|
|
beq_sites = re.findall(
|
|
r'/\* 0x([0-9A-F]+):\s+[0-9A-F]+\s+(beq|bne|bgez|bgtz|blez|bltz|bnel|beql)\b',
|
|
full_c)
|
|
beq_addrs = [int(a, 16) for a, _ in beq_sites]
|
|
delay_is_label = sum(1 for a in beq_addrs if f"{a+4:08X}" in labels)
|
|
print(f"\n[5] chained-branch-with-delay-slot-label population:")
|
|
print(f" total conditional branches: {len(beq_addrs)}")
|
|
print(f" delay slot also a basic-block label: {delay_is_label} "
|
|
f"(protected by zero-init fix)")
|
|
|
|
# ---- summary ----
|
|
real_bugs = (
|
|
len(buggy_decls) # predicate-init regression
|
|
+ len(missing) # direct dispatch miss
|
|
+ len(unexpected_missing) # unexpected tail-call miss
|
|
)
|
|
|
|
print(f"\n[summary] {cfg.name}")
|
|
print(f" buggy predicate decls : {len(buggy_decls)}")
|
|
print(f" direct dispatch miss : {len(missing)}")
|
|
print(f" unexpected tail-call miss : {len(unexpected_missing)}")
|
|
print(f" total real-bug findings : {real_bugs}")
|
|
if real_bugs == 0:
|
|
print(f" STATUS : CLEAN")
|
|
else:
|
|
print(f" STATUS : ISSUES FOUND")
|
|
|
|
return 0 if real_bugs == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|