Port catalog
The project's progress ledger, kept at function granularity. Reverse-engineering the game means walking each traced retail function from disassembly to documentation to a clean-room Rust reimplementation - and the port catalog is the tool that tracks where every function stands. You only need this page if you are contributing to the decompilation or engine tracks.
What each column means
Each catalog row is one retail function address, with four independent yes/no signals: a Ghidra decompiler dump exists (dumped), a docs page cites it (documented), a Rust source carries a // PORT: tag for it (ported), and it sits on the ignore list (ignored - statically-linked PsyQ SDK code, Sony's standard PSX library, which the clean-room port maps to native equivalents rather than porting line by line). The interesting queries are the cross-cuts of those columns - see what the columns surface.
| Column | Source of truth |
|---|---|
| dumped | A Ghidra decompiler dump exists under ghidra/scripts/funcs/ (gitignored - regenerable from the Ghidra project). |
| documented | The address is cited from at least one file under docs/ (FUN_<addr>, 0x<addr>, or backtick-wrapped bare `<addr>`, case-insensitive). |
| ported | A Rust source under crates/ carries a // PORT: FUN_<addr> tag for that address. |
| ignored | The address is listed in scripts/ci/port-catalog-ignore.toml as a non-port-site (BIOS thunk / libc / libgte / libgs / libcd / libapi / libsnd / libspu / libetc). Excluded from --missing-ports by default. |
Tool: scripts/ci/port-catalog.py. Reuses helpers from scripts/ci/function-coverage.py and shares the same code-range filter (SCUS 0x80010000-0x8006FFFF, overlays 0x801C0000-0x8020FFFF).
The // PORT: tag
The catalog's "ported" column keys off a structured comment in Rust source:
// PORT: FUN_801dd35c // single address
// PORT: FUN_801dd35c, FUN_801cf244 // multiple on one line
// PORT: FUN_801dd35c (sub-mode jump table) // trailing context allowed
//! PORT: FUN_801dd35c // inside `//!` module doc
/// PORT: FUN_801dd35c // inside `///` outer doc
The tag may appear as plain //, doc //!, or outer-doc /// - putting it in the doc block keeps the provenance co-located with the rustdoc description and makes it visible in generated docs.
Rules:
- The tag is the only signal trusted for "ported". Plain mentions of
FUN_<addr>in module docs or comments are ignored - they show up in many contexts that don't imply a port (cross-refs, "inspired by", "not yet ported", etc.) and noisily inflate the column. - Address must be lowercase hex in the SCUS / overlay code range.
- Match is line-local - put the tag on its own line or as a trailing comment.
- A single Rust file can carry many tags. The catalog records the crate name each tag appears in.
- One Ghidra function can be ported into more than one crate (e.g. a formula shared between
engine-vm::battle_formulasand a helper inengine-core). The catalog lists every crate that tags the address.
When porting a Ghidra function, add the tag once in the Rust function that implements its behaviour. Don't tag every caller of the ported function.
The // REF: tag
Sibling of // PORT:. Marks an address as a cross-reference citation - the file mentions FUN_<addr> in a docstring or comment but isn't claiming to port it. Same comment shapes as // PORT: (plain //, doc //!, outer-doc ///) and same multi-address syntax.
//! PORT: FUN_801E30E4
//! REF: FUN_801E7320, FUN_801CF098 -- callees, not yet ported
port-catalog.py ignores REF tags - they don't set the "ported" column - but the drift checker (scripts/ci/check-port-tags.py, see below) treats them as equivalent to PORT for warning suppression.
Tag drift checker
scripts/ci/check-port-tags.py walks crates/engine-*/src/**.rs and warns when a FUN_<addr> citation lacks a matching // PORT: or // REF: tag in the same file. The goal is to catch the "I ported X but forgot the tag" pattern so the catalog stays in sync with what the engine actually implements.
Default mode is --staged - only lines being added in the staging area are checked, which is what the pre-commit hook runs. --scan-all audits every line of every engine-crate file (full historical sweep). --strict turns warnings into a nonzero exit for CI.
python3 scripts/ci/check-port-tags.py # default = --staged
python3 scripts/ci/check-port-tags.py --scan-all # full audit
python3 scripts/ci/check-port-tags.py --strict # exit 1 on warning
python3 scripts/ci/check-port-tags.py --addr 80019b28 # drill-down
python3 scripts/ci/check-port-tags.py --backfill-refs # one-shot grandfather pass
Scope rule: only files that already carry a // PORT: tag are checked. Pure-docs files (no port tag anywhere) are treated as reference-only and skipped - they exist to describe retail behaviour without claiming ports, and requiring REF tags inside them would be churn for no signal.
Backfill workflow: --backfill-refs rewrites in place. For each port-bearing file with untagged citations, it inserts a //! REF: ... block after the last //! line of the leading module-doc comment (or at the top of the file if there's no leading block). Re-run whenever new ports or citations land to keep the REF set fresh.
Pre-commit integration: scripts/git-hooks/pre-commit runs check-port-tags.py --staged --quiet after cargo clippy. The hook is warn-only - drift output prints but never blocks the commit, so the checker doesn't gate unrelated PRs. CI can tighten by switching to --strict.
Usage
python3 scripts/ci/port-catalog.py # global catalog -> target/port-catalog/
python3 scripts/ci/port-catalog.py --missing-ports # dumped + documented, not ported (excludes ignore-list)
python3 scripts/ci/port-catalog.py --missing-ports --include-ignored # include ignore-list entries
python3 scripts/ci/port-catalog.py --missing-dumps # cited but not dumped
python3 scripts/ci/port-catalog.py --ported-only # show only ported addresses
python3 scripts/ci/port-catalog.py --ignored-only # show only ignore-list entries
python3 scripts/ci/port-catalog.py --addr 801dd35c # drill-down on one address
python3 scripts/ci/port-catalog.py --md # markdown to stdout
python3 scripts/ci/port-catalog.py --list-features # list features in features.toml
python3 scripts/ci/port-catalog.py --feature title-screen # BFS from a feature's roots
python3 scripts/ci/port-catalog.py --dashboard # open-work rollup -> open-work.md
Output is written to target/port-catalog/ (gitignored):
catalog.csv/catalog.md- every tracked address, machine-readable + markdown.<feature>.csv/<feature>.md- per-feature subset when--featureis used.open-work.md- single-page dashboard combining per-feature port % + top-N missing-ports per feature + ignore-list summary (see "Open-work dashboard" below).
Features (BFS from roots)
A feature in this tool is a named set of seed Ghidra function addresses (roots) plus an optional list of stop_at boundaries. Running --feature <name> filters the catalog to the addresses reachable from those roots via the citation graph (one edge per "this dump cites that address").
Features live in scripts/ci/features.toml:
[title-screen]
description = "Title overlay tick + boot UI"
roots = ["801dd35c"]
# Optional boundaries kept in the result but not recursed past.
stop_at = ["801de840", "801e295c"]
# Optional BFS depth cap.
max_depth = 2
The citation graph only has edges between dumped functions - undumped helpers have no outgoing edges, so the BFS frontier widens as more dumps land. This is intentional: it lets you start tight (small feature with few dumps) and progressively widen as you dig in.
Use feature views to:
- Find unported helpers in scope of a specific feature (filter by
--feature X --missing-ports). - Confirm a port is reachable from the feature root.
- Spot shared-infrastructure spillover that wants a
stop_atentry.
Ignore list
scripts/ci/port-catalog-ignore.toml lists addresses that the catalog should treat as out-of-scope for engine porting - statically-linked PsyQ kernel / runtime / SDK code. The clean-room port maps these clusters to native equivalents (Rust stdlib, wgpu, cpal) rather than reimplementing the PSX wrappers, so they shouldn't pollute the port worklist.
[bios]
"80056678" = "EnterCriticalSection (syscall(0), a0=1)"
"80056688" = "ExitCriticalSection (syscall(0), a0=2)"
[libgte]
"8005ba1c" = "GTE sqrt / normalise (mtc2 0xF000 / mfc2 0xF800)"
[libsnd]
"80062340" = "SsSeqOpen (slot-bitmap walk + load)"
Categories are organisational (one TOML table per cluster - bios / libc / libgte / libgs / libcd / libapi / libsnd / libspu / libetc); the tool treats every entry the same way. Provenance for each entry lives in docs/reference/functions.md and the audio / save-screen subsystem docs.
Default behaviour:
--missing-portsexcludes ignored entries. The summary line breaks the count down (of which ignored / remaining port worklist).--include-ignoredopts back in for completeness checks.--ignored-onlylists the ignore-list itself (useful for auditing).
Adding an entry: copy the address into the appropriate category table with a one-line reason that names the PsyQ function and (where known) the BIOS vector. Keep the reason factual - it shows up in catalog drill-down output. Provenance citations belong in docs/reference/functions.md, not in the TOML reason field.
Open-work dashboard
--dashboard emits target/port-catalog/open-work.md, a single regenerable page that answers "what's left to port, in what scope" at a glance. The dashboard combines four signals:
- Global counts - dumped / documented / ported / ignored / remaining port worklist.
- Per-feature status table - for each feature in
scripts/ci/features.toml: reachable, ported, port %, missing (port worklist within the feature, ignore-list excluded), ignored. - Per-feature top-N missing-ports - the highest-citation-count helpers reachable from each feature's roots that don't yet carry a
// PORT:tag. Sorted high-leverage first, so a feature's blockers surface immediately. Cap is--dashboard-top N(default 10). - Ignore-list summary - count per category (bios / libc / libgte / libgs / libcd / libapi / libsnd / libspu / libetc).
- Provenance gaps - addresses with a
// PORT:tag but missing a dump or doc citation (shown only when nonzero).
The page is gitignored output (lives under target/). Re-run after landing a batch of ports to see which helpers are now top-of-list. The question-level companion - open hunts rather than per-function status - is open-rev-eng-threads.html.
What the columns surface
The point of the table is to make the cross-cuts cheap to read:
dumped + documented + not ported, not ignored→ port worklist. The function is understood (we have a Ghidra dump and at least one doc citation), not yet implemented in the engine, and not statically-linked PsyQ infra. Sort by citation count to find high-leverage helpers first.cited but not dumped→ dump worklist. Some other dump references this address but no dump exists for it yet. Add toghidra/scripts/dump_funcs.pyTARGETS.ported but not documented→ provenance gap. A// PORT:tag was added without any doc mentioning the source function. Either backfill the doc or remove the tag if the attribution was wrong.ported but not dumped→ provenance gap. Same shape, opposite axis.
Caveats
- Citation graph is dump-local. The "cited" signal comes from grepping dump files - so an undumped helper has no outgoing edges. The frontier of reachable functions widens only as dumps land.
functions.mdis curated, butdocumentedis broader. Any doc page that mentionsFUN_<addr>or0x<addr>counts. The catalog won't tell you which docs are authoritative - that's still a judgement call per topic.- One
// PORT:tag does not guarantee semantic equivalence. The tag is a provenance link, not a correctness proof. Tests + retail-comparison still do that job. - The ignore-list is curated, not exhaustive. Newly-dumped PsyQ helpers don't auto-classify -
--missing-portswill surface them until they're explicitly added toport-catalog-ignore.toml. Treat unfamiliar 16-byte thunks in0x8005xxxx/0x8006xxxxas likely ignore candidates rather than ports.