mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
6f7fcdc89b
The "v4" suffix was a holdover from iterative project restarts. There is now one psxrecomp; all identifiers, include guards, cmake variables, function names, log strings, schema tags, and tool paths drop the v4 suffix. Changes: - PSXRECOMP_V4_ROOT → PSXRECOMP_ROOT (cmake) - PSXRECOMP_V4_RUNTIME_SOURCES/_INCLUDE_DIRS/_BIOS_GENERATED → no V4 - psxrecomp_v4_add_runtime_target → psxrecomp_add_runtime_target - PSXRECOMP_V4_*_H include guards → PSXRECOMP_*_H - "psxrecomp-v4" log/window strings → "psxrecomp" - Schema version strings in recompiler JSON output - Tool script hardcoded absolute paths updated Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
"""Shared audit-config loader.
|
|
|
|
Reads a TOML config (BIOS or game) into a normalized object that the
|
|
psxrecomp audit tools can consume. See `bios/SCPH1001.toml` and
|
|
`TombaRecomp/game.toml` for the schema this module accepts.
|
|
|
|
Project root is auto-detected by walking up from the config file until a
|
|
directory containing `CMakeLists.txt` is found; this works for both
|
|
configs placed at project root (game.toml) and configs in a subdirectory
|
|
(bios/SCPH1001.toml). All paths in the config are resolved relative to
|
|
the detected root.
|
|
"""
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
try:
|
|
import tomllib # Python 3.11+
|
|
except ImportError:
|
|
import tomli as tomllib # type: ignore
|
|
|
|
|
|
def _hex(s, field_name: str) -> int:
|
|
if not isinstance(s, str):
|
|
raise ValueError(f"{field_name}: expected hex string, got {type(s).__name__}")
|
|
return int(s, 16)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Region:
|
|
name: str
|
|
rom_start: int # byte offset into rom file
|
|
rom_end: int # exclusive
|
|
vaddr_base: int # virtual address of rom_start
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Remap:
|
|
description: str
|
|
from_lo: int # inclusive
|
|
from_hi: int # exclusive
|
|
to_lo: int # offset target start
|
|
|
|
|
|
@dataclass
|
|
class AuditConfig:
|
|
# Provenance
|
|
config_path: Path
|
|
project_root: Path
|
|
|
|
# Program info
|
|
name: str
|
|
rom: Path # absolute path to ROM/exe
|
|
load_address: int # vaddr of rom_start of first code region
|
|
text_size: int # bytes of code segment (for derived bounds)
|
|
|
|
# Generated artifacts (derived from rom stem + out_dir)
|
|
out_dir: Path
|
|
full_c: Path
|
|
dispatch_c: Path
|
|
|
|
# Audit-specific
|
|
function_starts: Optional[Path]
|
|
regions: List[Region]
|
|
kseg_mask: int
|
|
remaps: List[Remap] = field(default_factory=list)
|
|
|
|
def normalize_addr(self, addr: int) -> int:
|
|
"""Mirror the dispatch-table normalization rules from the config."""
|
|
phys = addr & self.kseg_mask
|
|
for rm in self.remaps:
|
|
if rm.from_lo <= phys < rm.from_hi:
|
|
phys = phys - rm.from_lo + rm.to_lo
|
|
return phys
|
|
|
|
def is_in_code_region(self, vaddr: int) -> bool:
|
|
"""True if `vaddr` falls inside any declared code region."""
|
|
for r in self.regions:
|
|
region_end_vaddr = r.vaddr_base + (r.rom_end - r.rom_start)
|
|
if r.vaddr_base <= vaddr < region_end_vaddr:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _find_project_root(config_path: Path) -> Path:
|
|
"""Walk up from config_path until we find a root marker.
|
|
|
|
Markers (any one is sufficient): `.gitignore`, `.git`, `CMakeLists.txt`.
|
|
`.gitignore` is the most reliable — both psxrecomp and TombaRecomp
|
|
have one at the repo root, while CMakeLists.txt only lives in subdirs
|
|
of psxrecomp (recompiler/, runtime/).
|
|
"""
|
|
cur = config_path.parent.resolve()
|
|
for _ in range(8): # bounded search
|
|
for marker in (".gitignore", ".git", "CMakeLists.txt"):
|
|
if (cur / marker).exists():
|
|
return cur
|
|
if cur.parent == cur:
|
|
break
|
|
cur = cur.parent
|
|
raise FileNotFoundError(
|
|
f"could not locate project root (no .gitignore/.git/CMakeLists.txt) "
|
|
f"walking up from {config_path}"
|
|
)
|
|
|
|
|
|
def load(config_path) -> AuditConfig:
|
|
config_path = Path(config_path).resolve()
|
|
if not config_path.exists():
|
|
raise FileNotFoundError(f"audit config not found: {config_path}")
|
|
|
|
root = _find_project_root(config_path)
|
|
|
|
with open(config_path, "rb") as f:
|
|
cfg = tomllib.load(f)
|
|
|
|
# Program info: [program] (preferred) or [game] (legacy)
|
|
prog = cfg.get("program") or cfg.get("game")
|
|
if prog is None:
|
|
raise KeyError(f"{config_path}: missing [program] or [game] block")
|
|
|
|
name = prog["name"]
|
|
rom_field = prog.get("rom") or prog.get("exe")
|
|
if rom_field is None:
|
|
raise KeyError(f"{config_path}: program/game block missing rom/exe path")
|
|
rom = (root / rom_field).resolve()
|
|
|
|
load_address = _hex(prog["load_address"], "load_address")
|
|
text_size = _hex(prog["text_size"], "text_size")
|
|
|
|
# Recompiler-derived paths
|
|
recomp = cfg.get("recompiler", {})
|
|
out_dir_field = recomp.get("out_dir", "generated")
|
|
out_dir = (root / out_dir_field).resolve()
|
|
|
|
# Output filename stem. Explicit override wins; otherwise strip a
|
|
# known binary extension (.BIN/.EXE, case-insensitive) from the rom
|
|
# name. Plain `Path.stem` is wrong here — it would turn "SCUS_942.36"
|
|
# into "SCUS_942" because it treats `.36` as an extension.
|
|
out_stem = recomp.get("out_stem")
|
|
if not out_stem:
|
|
rom_name = Path(rom_field).name
|
|
upper = rom_name.upper()
|
|
if upper.endswith(".BIN") or upper.endswith(".EXE"):
|
|
out_stem = rom_name[:-4]
|
|
else:
|
|
out_stem = rom_name
|
|
full_c = out_dir / f"{out_stem}_full.c"
|
|
dispatch_c = out_dir / f"{out_stem}_dispatch.c"
|
|
|
|
# Audit block
|
|
audit = cfg.get("audit")
|
|
if audit is None:
|
|
raise KeyError(f"{config_path}: missing [audit] block")
|
|
|
|
fs_field = audit.get("function_starts")
|
|
function_starts = (root / fs_field).resolve() if fs_field else None
|
|
|
|
regions = []
|
|
for r in audit.get("regions", []):
|
|
regions.append(Region(
|
|
name=r["name"],
|
|
rom_start=_hex(r["rom_start"], "rom_start"),
|
|
rom_end=_hex(r["rom_end"], "rom_end"),
|
|
vaddr_base=_hex(r["vaddr_base"], "vaddr_base"),
|
|
))
|
|
if not regions:
|
|
raise ValueError(f"{config_path}: [audit] must declare at least one region")
|
|
|
|
norm = audit.get("normalize", {})
|
|
kseg_mask = _hex(norm.get("kseg_mask", "0xFFFFFFFF"), "kseg_mask")
|
|
|
|
remaps = []
|
|
for rm in norm.get("remap", []):
|
|
remaps.append(Remap(
|
|
description=rm.get("description", ""),
|
|
from_lo=_hex(rm["from_lo"], "from_lo"),
|
|
from_hi=_hex(rm["from_hi"], "from_hi"),
|
|
to_lo=_hex(rm["to_lo"], "to_lo"),
|
|
))
|
|
|
|
return AuditConfig(
|
|
config_path=config_path,
|
|
project_root=root,
|
|
name=name,
|
|
rom=rom,
|
|
load_address=load_address,
|
|
text_size=text_size,
|
|
out_dir=out_dir,
|
|
full_c=full_c,
|
|
dispatch_c=dispatch_c,
|
|
function_starts=function_starts,
|
|
regions=regions,
|
|
kseg_mask=kseg_mask,
|
|
remaps=remaps,
|
|
)
|
|
|
|
|
|
def from_argv(argv, default_config: Optional[str] = None) -> AuditConfig:
|
|
"""CLI helper: parse `--config PATH` from argv and load.
|
|
|
|
If `--config` is absent and `default_config` is provided, use that.
|
|
Falls back to looking for `audit.toml` or `game.toml` in CWD.
|
|
"""
|
|
config_path: Optional[str] = None
|
|
i = 1
|
|
while i < len(argv):
|
|
a = argv[i]
|
|
if a == "--config" and i + 1 < len(argv):
|
|
config_path = argv[i + 1]
|
|
i += 2
|
|
elif a.startswith("--config="):
|
|
config_path = a.split("=", 1)[1]
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
if config_path is None:
|
|
config_path = default_config
|
|
|
|
if config_path is None:
|
|
for candidate in ("audit.toml", "game.toml"):
|
|
p = Path.cwd() / candidate
|
|
if p.exists():
|
|
config_path = str(p)
|
|
break
|
|
|
|
if config_path is None:
|
|
raise SystemExit(
|
|
"usage: ... --config PATH (or place audit.toml/game.toml in CWD)"
|
|
)
|
|
|
|
return load(config_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
cfg = from_argv(sys.argv)
|
|
print(f"loaded {cfg.config_path}")
|
|
print(f" project_root = {cfg.project_root}")
|
|
print(f" name = {cfg.name}")
|
|
print(f" rom = {cfg.rom}")
|
|
print(f" full_c = {cfg.full_c}")
|
|
print(f" dispatch_c = {cfg.dispatch_c}")
|
|
print(f" load_address = 0x{cfg.load_address:08X}")
|
|
print(f" text_size = 0x{cfg.text_size:08X}")
|
|
print(f" regions = {len(cfg.regions)}")
|
|
for r in cfg.regions:
|
|
print(f" - {r.name:8s} rom 0x{r.rom_start:05X}..0x{r.rom_end:05X} vaddr 0x{r.vaddr_base:08X}")
|
|
print(f" kseg_mask = 0x{cfg.kseg_mask:08X}")
|
|
print(f" remaps = {len(cfg.remaps)}")
|
|
for rm in cfg.remaps:
|
|
print(f" - 0x{rm.from_lo:08X}..0x{rm.from_hi:08X} -> +0x{rm.to_lo:X} ({rm.description})")
|