Three untracked files in the project root had no home in either repository: - the Xbox 360 technical reference report (a research compilation) becomes docs/reference/xbox360-re-technical-reference.md, unchanged; - XBOX360_ARCHITECTURE.md becomes docs/reference/xbox360-architecture.md, trimmed to its platform facts. Its format "status" sections (PAK unknown, mesh unknown, audio TODO) and the `just sniff` workflow predate every decoder in this repo and were wrong; - generate_export_docs.py becomes tools/generate_export_docs.py, the path the committed xbox360-exports.* already name as their generator. It lived inside a Canary checkout, so it now takes --canary and --out and fails loudly on a tree that is not Canary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
369 lines
12 KiB
Python
Executable File
369 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Generate documentation of all Xbox 360 system exports in Xenia Canary.
|
|
|
|
Parses the export table .inc files and implementation .cc files to produce
|
|
a comprehensive listing of all exports with their implementation status.
|
|
|
|
Reads a Xenia Canary working tree and writes
|
|
`docs/reference/xbox360-exports.{md,json}`. The committed copies were generated
|
|
from an older Canary, so a fresh run changes some statuses.
|
|
|
|
Usage:
|
|
python3 tools/generate_export_docs.py [--canary ../xenia-canary] [--out docs/reference]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import pathlib
|
|
import re
|
|
from collections import OrderedDict
|
|
|
|
SYLPHEED_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
CANARY_DEFAULT = SYLPHEED_ROOT.parent / "xenia-canary"
|
|
|
|
# --- Configuration ---
|
|
|
|
# Set by configure() from --canary, before anything reads them.
|
|
REPO_ROOT = CANARY_DEFAULT
|
|
TABLE_FILES = {}
|
|
IMPL_DIRS = {}
|
|
|
|
|
|
def configure(canary_root):
|
|
"""Point the table and implementation paths at a Canary checkout."""
|
|
global REPO_ROOT, TABLE_FILES, IMPL_DIRS
|
|
REPO_ROOT = canary_root
|
|
TABLE_FILES = {
|
|
"xboxkrnl": REPO_ROOT / "src/xenia/kernel/xboxkrnl/xboxkrnl_table.inc",
|
|
"xam": REPO_ROOT / "src/xenia/kernel/xam/xam_table.inc",
|
|
"xbdm": REPO_ROOT / "src/xenia/kernel/xbdm/xbdm_table.inc",
|
|
}
|
|
IMPL_DIRS = {
|
|
"xboxkrnl": REPO_ROOT / "src/xenia/kernel/xboxkrnl",
|
|
"xam": REPO_ROOT / "src/xenia/kernel/xam",
|
|
"xbdm": REPO_ROOT / "src/xenia/kernel/xbdm",
|
|
}
|
|
missing = [str(p) for p in TABLE_FILES.values() if not p.is_file()]
|
|
if missing:
|
|
raise SystemExit(
|
|
f"not a Xenia Canary checkout: {canary_root} "
|
|
f"(missing {', '.join(missing)}); pass --canary"
|
|
)
|
|
|
|
MODULE_NAMES = {
|
|
"xboxkrnl": "xboxkrnl.exe",
|
|
"xam": "xam.xex",
|
|
"xbdm": "xbdm.xex",
|
|
}
|
|
|
|
MODULE_MAP = {"XBOXKRNL": "xboxkrnl", "XAM": "xam", "XBDM": "xbdm"}
|
|
|
|
# Regex patterns
|
|
XE_EXPORT_RE = re.compile(
|
|
r"XE_EXPORT\(\s*(\w+)\s*,\s*(0x[0-9A-Fa-f]+)\s*,\s*(\w+)\s*,\s*k(\w+)\s*\)"
|
|
)
|
|
|
|
DECLARE_RE = re.compile(
|
|
r"DECLARE_(XBOXKRNL|XAM|XBDM)_EXPORT\d*\(\s*(\w+)\s*,"
|
|
r"\s*k(\w+)\s*,\s*(.+?)\)\s*;",
|
|
re.DOTALL,
|
|
)
|
|
|
|
TAG_RE = re.compile(r"k(\w+)")
|
|
|
|
SHIM_RE = re.compile(r'SHIM_SET_MAPPING\(\s*"[^"]+"\s*,\s*(\w+)\s*,')
|
|
|
|
VAR_MAPPING_RE = re.compile(r"ordinals::(\w+)")
|
|
EXPORT_KVAR_RE = re.compile(r"EXPORT_KVAR\((\w+)\)")
|
|
|
|
|
|
def parse_table_files():
|
|
"""Phase A: Parse all XE_EXPORT entries from *_table.inc files."""
|
|
exports = OrderedDict()
|
|
|
|
for module, path in TABLE_FILES.items():
|
|
content = path.read_text()
|
|
for match in XE_EXPORT_RE.finditer(content):
|
|
mod, ordinal_hex, name, typ = match.groups()
|
|
exports[(mod, name)] = {
|
|
"module": mod,
|
|
"ordinal": int(ordinal_hex, 16),
|
|
"ordinal_hex": ordinal_hex.upper().replace("0X", "0x"),
|
|
"name": name,
|
|
"type": "variable" if typ == "Variable" else "function",
|
|
"status": "not_implemented",
|
|
"tags": [],
|
|
"category": "",
|
|
"source_file": "",
|
|
}
|
|
|
|
return exports
|
|
|
|
|
|
def scan_declare_macros(exports):
|
|
"""Phase B: Scan DECLARE_*_EXPORT macros in .cc files."""
|
|
for mod_key, dir_path in IMPL_DIRS.items():
|
|
for cc_file in sorted(dir_path.glob("*.cc")):
|
|
content = cc_file.read_text()
|
|
for match in DECLARE_RE.finditer(content):
|
|
mod_prefix, name, category, tags_str = match.groups()
|
|
module = MODULE_MAP[mod_prefix]
|
|
tags = TAG_RE.findall(tags_str)
|
|
key = (module, name)
|
|
|
|
if key in exports:
|
|
entry = exports[key]
|
|
entry["category"] = category
|
|
entry["tags"] = tags
|
|
entry["source_file"] = str(
|
|
cc_file.relative_to(REPO_ROOT)
|
|
)
|
|
|
|
if "Implemented" in tags:
|
|
entry["status"] = "implemented"
|
|
elif "Stub" in tags:
|
|
entry["status"] = "stub"
|
|
elif "Sketchy" in tags:
|
|
entry["status"] = "sketchy"
|
|
else:
|
|
entry["status"] = "stub"
|
|
|
|
|
|
def scan_legacy_patterns(exports):
|
|
"""Phase C: Handle SHIM_SET_MAPPING and SetVariableMapping."""
|
|
# SHIM_SET_MAPPING in xboxkrnl_strings.cc
|
|
strings_file = IMPL_DIRS["xboxkrnl"] / "xboxkrnl_strings.cc"
|
|
if strings_file.exists():
|
|
content = strings_file.read_text()
|
|
for match in SHIM_RE.finditer(content):
|
|
name = match.group(1)
|
|
key = ("xboxkrnl", name)
|
|
if key in exports:
|
|
exports[key]["status"] = "implemented"
|
|
exports[key]["source_file"] = str(
|
|
strings_file.relative_to(REPO_ROOT)
|
|
)
|
|
if not exports[key]["tags"]:
|
|
exports[key]["tags"] = ["Implemented"]
|
|
|
|
# SetVariableMapping / EXPORT_KVAR in xboxkrnl_module.cc
|
|
module_file = IMPL_DIRS["xboxkrnl"] / "xboxkrnl_module.cc"
|
|
if module_file.exists():
|
|
content = module_file.read_text()
|
|
for match in VAR_MAPPING_RE.finditer(content):
|
|
name = match.group(1)
|
|
key = ("xboxkrnl", name)
|
|
if key in exports:
|
|
exports[key]["status"] = "implemented"
|
|
exports[key]["source_file"] = str(
|
|
module_file.relative_to(REPO_ROOT)
|
|
)
|
|
if not exports[key]["tags"]:
|
|
exports[key]["tags"] = ["Implemented"]
|
|
|
|
for match in EXPORT_KVAR_RE.finditer(content):
|
|
name = match.group(1)
|
|
key = ("xboxkrnl", name)
|
|
if key in exports:
|
|
exports[key]["status"] = "implemented"
|
|
exports[key]["source_file"] = str(
|
|
module_file.relative_to(REPO_ROOT)
|
|
)
|
|
if not exports[key]["tags"]:
|
|
exports[key]["tags"] = ["Implemented"]
|
|
|
|
|
|
def compute_statistics(exports):
|
|
"""Compute per-module statistics."""
|
|
stats = {}
|
|
for module in TABLE_FILES:
|
|
module_exports = [e for e in exports.values() if e["module"] == module]
|
|
stats[module] = {
|
|
"total": len(module_exports),
|
|
"implemented": sum(
|
|
1 for e in module_exports if e["status"] == "implemented"
|
|
),
|
|
"stub": sum(1 for e in module_exports if e["status"] == "stub"),
|
|
"sketchy": sum(
|
|
1 for e in module_exports if e["status"] == "sketchy"
|
|
),
|
|
"not_implemented": sum(
|
|
1
|
|
for e in module_exports
|
|
if e["status"] == "not_implemented"
|
|
),
|
|
"functions": sum(
|
|
1 for e in module_exports if e["type"] == "function"
|
|
),
|
|
"variables": sum(
|
|
1 for e in module_exports if e["type"] == "variable"
|
|
),
|
|
}
|
|
return stats
|
|
|
|
|
|
def generate_markdown(exports, stats):
|
|
"""Generate the Markdown documentation."""
|
|
lines = []
|
|
lines.append("# Xbox 360 System Exports - Xenia Canary")
|
|
lines.append("")
|
|
lines.append(
|
|
"Auto-generated by `tools/generate_export_docs.py`. "
|
|
"Do not edit manually."
|
|
)
|
|
lines.append("")
|
|
|
|
# Overall summary
|
|
lines.append("## Summary")
|
|
lines.append("")
|
|
total_all = sum(s["total"] for s in stats.values())
|
|
impl_all = sum(s["implemented"] for s in stats.values())
|
|
stub_all = sum(s["stub"] for s in stats.values())
|
|
sketchy_all = sum(s["sketchy"] for s in stats.values())
|
|
not_impl_all = sum(s["not_implemented"] for s in stats.values())
|
|
|
|
lines.append(
|
|
f"**Total exports: {total_all}** | "
|
|
f"Implemented: {impl_all} | "
|
|
f"Stub: {stub_all} | "
|
|
f"Sketchy: {sketchy_all} | "
|
|
f"Not implemented: {not_impl_all}"
|
|
)
|
|
lines.append("")
|
|
|
|
lines.append(
|
|
"| Module | File | Total | Implemented | Stub | Sketchy | Not Implemented | Functions | Variables |"
|
|
)
|
|
lines.append(
|
|
"|--------|------|------:|------------:|-----:|--------:|----------------:|----------:|----------:|"
|
|
)
|
|
for module in TABLE_FILES:
|
|
s = stats[module]
|
|
lines.append(
|
|
f"| {module} | {MODULE_NAMES[module]} | {s['total']} | "
|
|
f"{s['implemented']} | {s['stub']} | {s['sketchy']} | "
|
|
f"{s['not_implemented']} | {s['functions']} | {s['variables']} |"
|
|
)
|
|
lines.append("")
|
|
|
|
# Per-module tables
|
|
for module in TABLE_FILES:
|
|
s = stats[module]
|
|
lines.append(f"## {module} ({MODULE_NAMES[module]})")
|
|
lines.append("")
|
|
lines.append(
|
|
f"Total: {s['total']} | "
|
|
f"Implemented: {s['implemented']} | "
|
|
f"Stub: {s['stub']} | "
|
|
f"Sketchy: {s['sketchy']} | "
|
|
f"Not implemented: {s['not_implemented']}"
|
|
)
|
|
lines.append("")
|
|
|
|
lines.append(
|
|
"| Ordinal | Name | Type | Status | Category | Source |"
|
|
)
|
|
lines.append(
|
|
"|--------:|------|------|--------|----------|--------|"
|
|
)
|
|
|
|
module_exports = sorted(
|
|
[e for e in exports.values() if e["module"] == module],
|
|
key=lambda e: e["ordinal"],
|
|
)
|
|
|
|
for e in module_exports:
|
|
ordinal = f"0x{e['ordinal']:03X}"
|
|
status_icon = {
|
|
"implemented": "implemented",
|
|
"stub": "stub",
|
|
"sketchy": "sketchy",
|
|
"not_implemented": "-",
|
|
}.get(e["status"], e["status"])
|
|
|
|
source = e["source_file"]
|
|
if source:
|
|
# Show just the filename for brevity
|
|
source = pathlib.Path(source).name
|
|
|
|
lines.append(
|
|
f"| {ordinal} | {e['name']} | {e['type']} | "
|
|
f"{status_icon} | {e['category']} | {source} |"
|
|
)
|
|
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_json(exports, stats):
|
|
"""Generate the JSON documentation."""
|
|
data = {
|
|
"description": "Xbox 360 system exports implemented in Xenia Canary",
|
|
"generator": "tools/generate_export_docs.py",
|
|
"statistics": stats,
|
|
"modules": {},
|
|
}
|
|
|
|
for module in TABLE_FILES:
|
|
module_exports = sorted(
|
|
[e for e in exports.values() if e["module"] == module],
|
|
key=lambda e: e["ordinal"],
|
|
)
|
|
data["modules"][module] = {
|
|
"file": MODULE_NAMES[module],
|
|
"exports": module_exports,
|
|
}
|
|
|
|
return json.dumps(data, indent=2)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
parser.add_argument("--canary", type=pathlib.Path, default=CANARY_DEFAULT,
|
|
help="Xenia Canary working tree (default: %(default)s)")
|
|
parser.add_argument("--out", type=pathlib.Path,
|
|
default=SYLPHEED_ROOT / "docs" / "reference",
|
|
help="output directory (default: %(default)s)")
|
|
args = parser.parse_args()
|
|
configure(args.canary.resolve())
|
|
|
|
print("Parsing export tables...")
|
|
exports = parse_table_files()
|
|
print(f" Found {len(exports)} exports across {len(TABLE_FILES)} modules")
|
|
|
|
print("Scanning DECLARE_*_EXPORT macros...")
|
|
scan_declare_macros(exports)
|
|
|
|
print("Scanning legacy registration patterns...")
|
|
scan_legacy_patterns(exports)
|
|
|
|
stats = compute_statistics(exports)
|
|
|
|
for module, s in stats.items():
|
|
print(
|
|
f" {module}: {s['total']} total, "
|
|
f"{s['implemented']} implemented, "
|
|
f"{s['stub']} stub, "
|
|
f"{s['sketchy']} sketchy, "
|
|
f"{s['not_implemented']} not implemented"
|
|
)
|
|
|
|
# Generate outputs
|
|
docs_dir = args.out
|
|
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
md_path = docs_dir / "xbox360-exports.md"
|
|
md_content = generate_markdown(exports, stats)
|
|
md_path.write_text(md_content)
|
|
print(f"Written: {md_path}")
|
|
|
|
json_path = docs_dir / "xbox360-exports.json"
|
|
json_content = generate_json(exports, stats)
|
|
json_path.write_text(json_content)
|
|
print(f"Written: {json_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|