From b1cb6ff61fccfef28d38590a8b819ccaafa5928a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 10 Mar 2026 15:17:36 +0100 Subject: [PATCH 01/71] fix to work with "ghidra-cli" and "ghidra" commands --- tools/decomp-context.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/decomp-context.py b/tools/decomp-context.py index 51ccb9cb8..f90903c15 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -15,6 +15,7 @@ import json import os import re +import shutil import subprocess import sys from typing import Any, Dict, List, Optional, Tuple @@ -130,10 +131,14 @@ def search_symbols_file(file: str, name: str) -> List[Tuple[str, str, str]]: def ghidra_decompile(address: str, program: str) -> Optional[str]: """Decompile a function at the given address using Ghidra CLI.""" + ghidra_cmd = shutil.which("ghidra-cli") or shutil.which("ghidra") + if ghidra_cmd is None: + return None + try: result = subprocess.run( [ - "ghidra", + ghidra_cmd, "decompile", "--program", program, @@ -173,9 +178,9 @@ def tu_name_from_unit(unit: Dict[str, Any]) -> str: def print_section(title: str, content: str) -> None: """Print a labeled section.""" - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f" {title}") - print(f"{'='*60}") + print(f"{'=' * 60}") print(content) From d7291b431145268ccb67489fe7332da8e67b93f0 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 10 Mar 2026 19:22:01 +0100 Subject: [PATCH 02/71] Improve AI workflow tooling and documentation - decomp-context.py: scope source output to function line range instead of full file; add Ghidra error diagnostics with stderr reporting and --ghidra-check flag - tools/find-symbol.py: new CLI symbol finder to replace clangd workspace/symbol for checking existing definitions before declaring new types - refiner/SKILL.md: new skill for resolving stubborn instruction mismatches with systematic lateral strategies - AGENTS.md: document new tools, expand Matching Tips with branch/stack/vtable/register/inline patterns, add Discovered Matching Patterns section with entry template - lookup/SKILL.md: replace clangd instructions with find-symbol.py - scaffold/SKILL.md: fix typos, replace clangd reference, add jumbo unit identification guidance - execute/SKILL.md: fix section numbering gap (1a/1c -> 1a/1b), link skill files in agent type list - implement/SKILL.md: replace cat command with Read tool guidance --- .github/skills/execute/SKILL.md | 8 +- .github/skills/implement/SKILL.md | 10 +- .github/skills/lookup/SKILL.md | 25 ++-- .github/skills/refiner/SKILL.md | 135 ++++++++++++++++++++ .github/skills/scaffold/SKILL.md | 14 ++- AGENTS.md | 73 ++++++++++- tools/decomp-context.py | 202 +++++++++++++++++++++++++----- tools/find-symbol.py | 152 ++++++++++++++++++++++ 8 files changed, 564 insertions(+), 55 deletions(-) create mode 100644 .github/skills/refiner/SKILL.md create mode 100644 tools/find-symbol.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 413f9a893..5772356a3 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -14,9 +14,9 @@ original retail binary. This skill coordinates several agent types: 1. **reverse-engineer** — Update Ghidra with accurate data types for the class -2. **scaffolder** — Create header/source if the class is not yet in the project -3. **implementer** — Match each function one at a time until the TU is complete. -4. **refiner** — Use on non-matching functions to improve the match. This uses a slower, but more thorough model that can fix issues the implementer can't. +2. **scaffolder** — Create header/source if the class is not yet in the project (see `.github/skills/scaffold/SKILL.md`) +3. **implementer** — Match each function one at a time until the TU is complete (see `.github/skills/implement/SKILL.md`) +4. **refiner** — Use on non-matching functions to improve the match. Applies systematic lateral strategies for stubborn mismatches (see `.github/skills/refiner/SKILL.md`). All non-read-only work is done **sequentially** — never spawn multiple writing agents at the same time, as they will interfere with each other. @@ -44,7 +44,7 @@ Before spawning any implementation agents, understand the current state of the T Determine the file path (e.g. `src/Speed/Indep/SourceLists/zWorld2`). The game uses unity builds, so such a file includes multiple source files from `src/Speed/Indep/Src/`. -### 1c. Get the full function list +### 1b. Get the full function list ```sh python tools/decomp-diff.py -u main/Path/To/TU diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index b37898c28..315f7fa8c 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -34,12 +34,14 @@ Reference the skill for the usage. It gives info based on the virtual address of ### 1e. Assembly reference -If these doesn't provide enough detail, check the generated assembly: +If these don't provide enough detail, check the generated assembly. Use the Read tool +to open the relevant `.s` file (prefer this over dumping the whole file): -```sh -# Look at the target disassembly -cat build/GOWE69/asm/Path/To/TU.s ``` +build/GOWE69/asm/Path/To/TU.s +``` + +Search for the function label (mangled name) to navigate directly to its section. ### 1f. Related functions diff --git a/.github/skills/lookup/SKILL.md b/.github/skills/lookup/SKILL.md index f911ea3ce..100020bed 100644 --- a/.github/skills/lookup/SKILL.md +++ b/.github/skills/lookup/SKILL.md @@ -127,26 +127,21 @@ python tools/lookup.py ./symbols/Dwarf typedef HHANDLER ## Checking for Existing Definitions -**Before declaring any new symbol** (struct, class, enum, global variable, typedef, or any other type), use clangd to search for an existing definition in the source tree. Only declare it yourself if clangd finds nothing. +**Before declaring any new symbol** (struct, class, enum, global variable, typedef, or any other type), search for an existing definition in the source tree using `find-symbol.py`. Only declare it yourself if the script finds nothing. -Use clangd's workspace symbol search to look up the name: - -``` -clangd: workspace/symbol { "query": "AIGoal" } -``` - -Or equivalently via any LSP-capable tool: - -``` -clangd: textDocument/definition (on any usage of the symbol) +```bash +python tools/find-symbol.py AIGoal +python tools/find-symbol.py CEntity --type class +python tools/find-symbol.py EState --type enum +python tools/find-symbol.py HHANDLER --type typedef ``` -If clangd returns a definition inside `./src/`: +If the script prints results inside `./src/`: -- **Do not redeclare it.** Include the header that contains it instead. +- **Do not redeclare it.** Include the header shown in the output instead. - Note the file path and `#include` it in your translation unit. -If clangd finds nothing in `./src/`: +If the script prints "Not found ... Safe to declare": - Declare it yourself, using the Dwarf and PS2 dumps as the source of truth for layout and `struct` vs `class`. @@ -167,7 +162,7 @@ This applies to **all** of the following before writing them: - **Always use `./symbols/Dwarf` as the primary source** for all decompilation work. - **Always check `./symbols/PS2/PS2_types.nothpp`** for every type to determine `struct` vs `class` and vtable layout. - **`struct` vs `class` rule:** no visibility modifiers in PS2 dump → `struct`; any visibility modifier present → `class`. -- **Never declare a symbol without first searching for it with clangd.** If it exists in `./src/`, include that header instead. +- **Never declare a symbol without first running `find-symbol.py`.** If it exists in `./src/`, include that header instead. - **Never guess field offsets or sizes.** Look them up. - **Nested enums** must be queried as `StructName::EnumName`, not just `EnumName`. - **Function addresses** are hex. Always include the `0x` prefix. diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md new file mode 100644 index 000000000..b35017fa2 --- /dev/null +++ b/.github/skills/refiner/SKILL.md @@ -0,0 +1,135 @@ +--- +name: refiner +description: Workflow for resolving stubborn instruction mismatches in a function that the standard implementer has already attempted. Use when a function is stuck at 80–99% match and the obvious approaches have already been tried. Assumes the function compiles cleanly, a diff exists, and the implementer has already made one or more passes. +--- + +# Refiner Workflow + +Your goal is to close the remaining instruction mismatches in a function that is partially +matching. The implementer has already made a pass. You must **not** repeat the same +approaches that were tried before — instead, apply systematic lateral analysis. + +## Starting assumptions + +- The function already compiles. +- A diff is available (`decomp-diff.py -u -d `). +- The "obvious" translation from Ghidra has been attempted. +- You have been given the current source code and the diff. + +## Phase 1: Read the full diff without collapsing + +```sh +python tools/decomp-diff.py -u main/Path/To/TU -d FunctionName --no-collapse +``` + +Read every instruction pair. Categorize each mismatch: + +| Category | Symptom | Strategy | +|---|---|---| +| **Branch inversion** | Entire blocks swapped, branch condition inverted | Invert the `if` condition and swap the two bodies | +| **Register pressure** | Same ops, different register allocation | Reorder source expressions; introduce/remove a named temp | +| **Stack frame size** | Wrong frame size in prologue | Count locals in DWARF; remove temporaries not in DWARF | +| **Float vs int sequence** | `xoris` present → field is `int`; absent → `uint` | Check field type in DWARF; change cast | +| **`fmuls` operand order** | `fmuls fX, fX, fY` or `fmuls fX, fY, fX` | Try `v *= fY` vs `fY * v` explicitly | +| **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order | +| **Virtual vs direct call** | `bl` vs indirect through vtable | Check const-qualifier; use `GetFoo()` vs `Foo()` | +| **Inline vs outlined** | Extra call to helper vs inlined sequence | Force inline by rewriting the expression without calling the helper | +| **Missing `this->` dereference** | Wrong address in load/store | Ensure member access goes through the correct `this` pointer | +| **Loop structure** | `do/while` vs `for` vs `while` | Try all three forms; compiler emits different branch sequences | + +## Phase 2: Systematic permutation strategies + +Try these **in order**, rebuilding and diffing after each: + +### 2a. Temporaries + +Remove any named temporary that is **not** in the DWARF, or add one that **is**. +Temporaries affect register allocation significantly. + +```bash +python tools/lookup.py ./symbols/Dwarf function 0xADDR # check for temps in DWARF +``` + +### 2b. Expression decomposition + +Split or merge compound expressions. GCC often schedules arithmetic differently when +sub-expressions are named: + +```cpp +// Try decomposed: +float a = foo->x * bar; +float b = a + baz->y; +result = b; + +// vs composed: +result = foo->x * bar + baz->y; +``` + +### 2c. Const-correctness + +Check every method call in the diff against the DWARF. A const method resolves to a +different symbol than its non-const overload. Even one wrong const qualifier causes +a `bl` mismatch. + +```bash +python tools/lookup.py ./symbols/Dwarf struct ClassName +``` + +### 2d. Inline math + +The game uses `UMath` and `bMath` inlines. If the diff shows a hand-rolled float +sequence, look up whether an inline covers the same operation: + +```bash +python tools/lookup.py ./symbols/Dwarf struct UMath +python tools/lookup.py ./symbols/Dwarf struct bMath +``` + +Replace hand-rolled sequences with the correct inline call. + +### 2e. Initializer list order + +Constructors compiled with GCC are sensitive to initializer list order. The DWARF +shows the canonical member order. If yours differs, reorder. + +### 2f. Cast type + +`static_cast` vs `static_cast` produces different assembly +sequences on PPC (see `xoris` pattern in AGENTS.md). Check all casts. + +### 2g. Compiler flag hint + +If none of the above resolve the mismatch, note the function address and consider +running `flag_permuter.py`. This is a last resort — only do this for a single +isolated function, not as a general strategy. + +## Phase 3: DWARF verification + +After any instruction match, verify the DWARF also matches: + +```bash +# Dump your compiled unit +dtk dwarf dump build/GOWE69/src/Path/To/UNIT.o -o /tmp/refiner__check.nothpp + +# Compare your function's DWARF against the original +python tools/lookup.py --file /tmp/refiner__check.nothpp function "ClassName::FunctionName(void)" +python tools/lookup.py ./symbols/Dwarf function 0xADDR +``` + +DWARF mismatches to watch for: + +- Extra or missing local variables (temporaries in DWARF = they must exist in source) +- Wrong parameter types or qualifiers +- Wrong return type +- Missing inlined function records (means an inline call was outlined) + +## Phase 4: Report + +Summarize: + +- Final match % and instruction count +- What was blocking the match (the root cause category from Phase 1) +- The specific source change that resolved it +- Any new generalizable assembly pattern discovered (add to AGENTS.md if so) +- DWARF match status +- If still not matching: the exact diff lines that remain and your best theory diff --git a/.github/skills/scaffold/SKILL.md b/.github/skills/scaffold/SKILL.md index f89d3da13..76abd7925 100644 --- a/.github/skills/scaffold/SKILL.md +++ b/.github/skills/scaffold/SKILL.md @@ -5,7 +5,7 @@ description: Workflow for creating an accurate header for an unimplemented class # Class Initialization Workflow -Your goal is to create copy the struct and header definitions from the dwarf that capture the exact class layout (vtable, members, sizes) using `clangd`, the `lookup` and the `line-lookup` skills. +Your goal is to copy the struct and header definitions from the dwarf that capture the exact class layout (vtable, members, sizes) using `find-symbol.py`, the `lookup` and the `line-lookup` skills. ## Phase 1: Gather Information @@ -39,7 +39,17 @@ Write a TODO comment over the struct/class if you aren't 100% sure that it belon ## Phase 3: Add needed files to jumbo file and compile -Since the project uses jumbo builds, you'll need to add the real source files to the jumbo build using `#include` statements before your changes can picked up by the build system. Try to build afterwards and see if there are any compile errors. +Since the project uses jumbo builds, you'll need to add the real source files to the jumbo +build using `#include` statements before your changes can be picked up by the build system. + +**Finding the correct jumbo unit:** Use the `line-lookup` skill on any function address from +the class you are scaffolding. The outermost `.cpp` file in the result (the one spanning +the widest sequential address range) is the source file being compiled. Its corresponding +`SourceLists/z*.cpp` jumbo unit is where the new `#include` belongs. For example, if +`line_lookup.py` shows `src/Speed/Indep/Src/zWorld2/CWorldObject.cpp` dominating the range, +the new file goes into `src/Speed/Indep/SourceLists/zWorld2.cpp`. + +Try to build afterwards and see if there are any compile errors. ## Phase 4: Report diff --git a/AGENTS.md b/AGENTS.md index 01518f3b7..57bb3a399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,14 +77,30 @@ python tools/decomp-status.py --json # machine-readable ### decomp-context.py — Function context for matching work -Gathers source code, objdiff diff, Ghidra decompile, and debug map info: +Gathers a scoped source excerpt, objdiff diff, and Ghidra decompile for a specific function: ```sh python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f AcceptScriptMsg python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --no-source +python tools/decomp-context.py --ghidra-check # verify Ghidra CLI is set up correctly ``` -Flags: `--no-source`, `--no-ghidra` to skip sections. +Flags: `--no-source`, `--no-ghidra` to skip sections. Source output is automatically scoped +to the function's line range (with a few lines of context) instead of dumping the whole file. + +### find-symbol.py — Check for existing definitions before declaring new types + +Before declaring any new struct, class, enum, global, or typedef, run this to check whether +it already exists in `src/`. This is the CLI alternative to clangd workspace/symbol search. + +```sh +python tools/find-symbol.py AITarget +python tools/find-symbol.py CEntity --type class +python tools/find-symbol.py EState --type enum +``` + +If it prints "Not found: ... Safe to declare", you can proceed to define the symbol. +If it finds a match, include that header instead of redeclaring. ### dtk (decomp-toolkit) @@ -167,3 +183,56 @@ register assignments but does NOT affect integer register assignments (and vice - `fmuls fX, fX, fY` or sometimes `fmuls fX, fY, fX` often translates to `v *= fY` - `xoris r0, r0, 0x8000` in int-to-float conversion => field is `int`, not `uint`. Unsigned int-to-float uses a different sequence without `xoris`. + +### Branch structure + +- Ghidra almost always **inverts** `if`-statement branch logic: the true and false bodies + are swapped in its output. Fix by inverting the condition and swapping the two code paths. +- A `do { ... } while (i < upperBound)` with a leading `if (upperBound > 0)` guard should + be written as a plain `for` loop — GCC emits the same code. + +### Stack frame and locals + +- Frame size (`stwu r1, -0xNNN(r1)`) is determined by the number and types of locals. + Every local that is NOT in the DWARF is a spurious temporary — remove it. +- Every local that IS in the DWARF must exist in the source, even if you don't use the name. + Name it exactly as the DWARF shows. + +### Virtual vs direct calls + +- A `bl` to a specific address = direct (non-virtual) call. +- An `lwz + mtctr + bctrl` sequence = virtual dispatch through vtable. +- If the diff shows a virtual call where you have a direct call (or vice versa), the + const-qualifier of the method or the object pointer is wrong. Check the DWARF. + +### Register allocation hints + +- GCC is sensitive to expression decomposition. Splitting a compound expression into + named sub-expressions often produces different (matching) register allocation. +- Conversely, merging sub-expressions into one can collapse intermediate registers. +- If two adjacent float ops are swapped, try commuting the operands or using a temp. + +### Inlines + +- Inlines at the bottom of a TU are emitted by usage, not by definition. Do not write + them as normal function bodies; their presence in source is controlled by `#include`. +- If an inline appears in the DWARF but does not exist in `src/`, deduce its body and add + it to the correct header (use `line-lookup` skill to find the header file). + +--- + +## Discovered Matching Patterns + +This section accumulates session-specific patterns discovered during decompilation. +Generalizable entries are promoted here; TU-specific ones stay in session context only. + +**Format for new entries:** + +``` +### +TU: | Function: + +``` + + + diff --git a/tools/decomp-context.py b/tools/decomp-context.py index f90903c15..1506ef08d 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -9,6 +9,8 @@ Usage: python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --no-ghidra + python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --no-source + python tools/decomp-context.py --ghidra-check """ import argparse @@ -31,6 +33,9 @@ GC_GHIDRA_PROGRAM = "NFSMWRELEASE.ELF" PS2_GHIDRA_PROGRAM = "NFS.ELF" +# Number of lines of context to show before/after the matched function in source +SOURCE_CONTEXT_LINES = 5 + def load_project_config() -> Dict[str, Any]: """Load objdiff.json.""" @@ -129,11 +134,15 @@ def search_symbols_file(file: str, name: str) -> List[Tuple[str, str, str]]: return results -def ghidra_decompile(address: str, program: str) -> Optional[str]: - """Decompile a function at the given address using Ghidra CLI.""" +def ghidra_decompile(address: str, program: str) -> Tuple[Optional[str], Optional[str]]: + """Decompile a function at the given address using Ghidra CLI. + + Returns (code, error_message). On success error_message is None. + On failure code is None and error_message describes the problem. + """ ghidra_cmd = shutil.which("ghidra-cli") or shutil.which("ghidra") if ghidra_cmd is None: - return None + return None, "ghidra-cli / ghidra not found in PATH" try: result = subprocess.run( @@ -149,14 +158,121 @@ def ghidra_decompile(address: str, program: str) -> Optional[str]: timeout=30, ) if result.returncode != 0: - return None + stderr = result.stderr.decode(errors="replace").strip() + msg = f"ghidra exited with code {result.returncode}" + if stderr: + msg += f"\n stderr: {stderr}" + return None, msg data = json.loads(result.stdout) if data and isinstance(data, list) and len(data) > 0: - return data[0].get("code", "") - return None - except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError): + return data[0].get("code", ""), None + return None, "ghidra returned empty result" + except subprocess.TimeoutExpired: + return None, "ghidra timed out after 30s" + except FileNotFoundError: + return None, f"ghidra binary not executable: {ghidra_cmd}" + except json.JSONDecodeError as e: + return None, f"ghidra returned invalid JSON: {e}" + + +def check_ghidra() -> None: + """Verify Ghidra CLI is reachable and the required programs are loaded.""" + ghidra_cmd = shutil.which("ghidra-cli") or shutil.which("ghidra") + if ghidra_cmd is None: + print("FAIL ghidra-cli / ghidra not found in PATH") + print(" Install ghidra-cli and ensure it is on your PATH.") + sys.exit(1) + print(f"OK ghidra binary: {ghidra_cmd}") + + # Try a minimal command that lists available programs + try: + result = subprocess.run( + [ghidra_cmd, "list", "programs"], + capture_output=True, + timeout=15, + ) + output = result.stdout.decode(errors="replace") + stderr = result.stderr.decode(errors="replace").strip() + + for prog in [GC_GHIDRA_PROGRAM, PS2_GHIDRA_PROGRAM]: + if prog in output: + print(f"OK program found: {prog}") + else: + print(f"WARN program not found in listing: {prog}") + print(f" Run: ghidra set-default project NeedForSpeed") + + if result.returncode != 0 and stderr: + print(f"WARN ghidra list programs exited {result.returncode}: {stderr}") + except subprocess.TimeoutExpired: + print("WARN ghidra list programs timed out — Ghidra may be slow to start") + except Exception as e: + print(f"WARN could not verify programs: {e}") + + # Quick decompile smoke test — use a known small GC function address if symbols exist + if os.path.exists(GC_SYMBOLS_FILE): + with open(GC_SYMBOLS_FILE) as f: + for line in f: + m = re.match(r"^\S+\s*=\s*(?:\.text:)?0x([0-9A-Fa-f]+)", line.strip()) + if m: + test_addr = m.group(1) + break + else: + test_addr = None + if test_addr: + code, err = ghidra_decompile(test_addr, GC_GHIDRA_PROGRAM) + if code is not None: + print(f"OK decompile smoke test passed (0x{test_addr})") + else: + print(f"FAIL decompile smoke test failed for 0x{test_addr}: {err}") + else: + print(f"SKIP smoke test — symbols file not found: {GC_SYMBOLS_FILE}") + + +def extract_source_for_function( + source_path: str, right_sym: Optional[Dict[str, Any]] +) -> Optional[str]: + """Extract the source lines relevant to the function from its source file. + + Uses the line numbers embedded in the right (decomp) symbol's instructions + to determine the source region. Falls back to full file if line numbers are + unavailable or the file cannot be read. + + Returns the extracted source text, or None if the file doesn't exist. + """ + full_path = os.path.join(root_dir, source_path) + if not os.path.exists(full_path): return None + with open(full_path) as f: + all_lines = f.readlines() + + # Collect all source line numbers referenced by the decomp symbol's instructions + line_numbers: List[int] = [] + if right_sym: + for inst_entry in right_sym.get("instructions", []): + ln = inst_entry.get("instruction", {}).get("line_number") + if ln is not None: + line_numbers.append(int(ln)) + + if not line_numbers: + # No line info available — return full source + return "".join(all_lines) + + first_line = min(line_numbers) + last_line = max(line_numbers) + + # Expand to capture the enclosing function: walk back to find the function + # signature (a line that looks like a function definition, not just a brace). + # Heuristic: walk backward from first_line to find an opening that precedes + # the function body, giving context. + start = max(1, first_line - SOURCE_CONTEXT_LINES) + end = min(len(all_lines), last_line + SOURCE_CONTEXT_LINES) + + # 1-indexed lines -> 0-indexed slice + excerpt = all_lines[start - 1 : end] + header = f"// Lines {start}–{end} of {source_path}\n" + return header + "".join(excerpt) + def strip_ansi(text: str) -> str: """Remove ANSI escape codes from text.""" @@ -203,13 +319,15 @@ def print_ghidra_decompilation( mangled_function = matches[0][0] if addr: - decomp = ghidra_decompile(addr, program) - if decomp: - print_section(f"{version_name}: Ghidra Decompile (0x{addr})", decomp) + code, err = ghidra_decompile(addr, program) + if code is not None: + print_section(f"{version_name}: Ghidra Decompile (0x{addr})", code) else: - print(f"\nGhidra decompile failed for 0x{addr}") + print(f"\nGhidra decompile failed for {version_name} 0x{addr}: {err}") else: - print(f"\nCould not find address for {mangled_function} in symbols.txt") + print( + f"\nCould not find address for {mangled_function} in {version_name} symbols.txt" + ) def main(): @@ -217,19 +335,29 @@ def main(): description="Gather context for decomp function matching" ) parser.add_argument( - "-u", "--unit", required=True, help="Unit name (e.g. main/MetroidPrime/CEntity)" - ) - parser.add_argument( - "-f", "--function", required=True, help="Function name to look up" + "-u", "--unit", help="Unit name (e.g. main/MetroidPrime/CEntity)" ) + parser.add_argument("-f", "--function", help="Function name to look up") parser.add_argument( "--no-source", action="store_true", help="Skip source file output" ) parser.add_argument( "--no-ghidra", action="store_true", help="Skip Ghidra decompile" ) + parser.add_argument( + "--ghidra-check", + action="store_true", + help="Verify Ghidra CLI is reachable and programs are loaded, then exit", + ) args = parser.parse_args() + if args.ghidra_check: + check_ghidra() + return + + if not args.unit or not args.function: + parser.error("-u/--unit and -f/--function are required (or use --ghidra-check)") + config = load_project_config() unit = find_unit(config, args.unit) if not unit: @@ -239,23 +367,40 @@ def main(): meta = unit.get("metadata", {}) source_path = meta.get("source_path", "") - # === Source File === + # === objdiff Status (run first so we have line numbers for source scoping) === + diff_data = run_objdiff(args.unit) + left_sym = right_sym = None + + if diff_data: + left_sym, right_sym = find_symbol_in_diff(diff_data, args.function) + + # === Source File (scoped to function if line info available) === if not args.no_source and source_path: - full_path = os.path.join(root_dir, source_path) - if os.path.exists(full_path): - with open(full_path) as f: - source = f.read() - print_section(f"Source: {source_path}", source) + excerpt = extract_source_for_function(source_path, right_sym) + if excerpt is not None: + label = "Source" + if right_sym and right_sym.get("instructions"): + # Check if we actually got line info + has_lines = any( + inst.get("instruction", {}).get("line_number") is not None + for inst in right_sym.get("instructions", []) + ) + if has_lines: + label = "Source (function excerpt)" + else: + label = f"Source (full file — no line info): {source_path}" + print_section(label, excerpt) else: - print(f"\nSource file not found: {full_path}", file=sys.stderr) + print( + f"\nSource file not found: {os.path.join(root_dir, source_path)}", + file=sys.stderr, + ) # === objdiff Status === - diff_data = run_objdiff(args.unit) if diff_data: - left_sym, right_sym = find_symbol_in_diff(diff_data, args.function) - if left_sym or right_sym: - sym = left_sym or right_sym + sym = left_sym if left_sym is not None else right_sym + assert sym is not None name = sym.get("demangled_name", sym.get("name", "?")) mangled = sym.get("name", "?") mp = sym.get("match_percent") @@ -320,7 +465,8 @@ def main(): # === Ghidra Decompile === if not args.no_ghidra and (left_sym or right_sym): - sym = left_sym or right_sym + sym = left_sym if left_sym is not None else right_sym + assert sym is not None mangled = sym.get("name", "") print_ghidra_decompilation( diff --git a/tools/find-symbol.py b/tools/find-symbol.py new file mode 100644 index 000000000..c44164234 --- /dev/null +++ b/tools/find-symbol.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +find-symbol.py + +Search src/ for existing definitions of a class, struct, enum, global variable, +or typedef. Use this before declaring any new symbol to avoid redeclaring something +that already exists in the project. + +This is the CLI alternative to clangd workspace/symbol search. + +Usage: + python tools/find-symbol.py AITarget + python tools/find-symbol.py CEntity --type class + python tools/find-symbol.py EState --type enum + python tools/find-symbol.py TheAnimCandidateData --type global + python tools/find-symbol.py HHANDLER --type typedef + python tools/find-symbol.py --all AIGoal # show all match types +""" + +import argparse +import os +import re +import sys +from typing import List, Optional, Tuple + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.abspath(os.path.join(script_dir, "..")) +SRC_DIR = os.path.join(root_dir, "src") + +# Extensions to search +SOURCE_EXTS = {".h", ".hpp", ".cpp", ".cc", ".c", ".hh"} + +# ------------------------------------------------------------------- +# Pattern builders +# ------------------------------------------------------------------- + + +def _word(name: str) -> str: + """Return a regex that matches `name` as a whole word.""" + return r"(?]*>\s*)?struct\s+({name})(?:\s|:|\{{|;)" + ), + "class": re.compile( + r"^\s*(?:template\s*<[^>]*>\s*)?class\s+({name})(?:\s|:|\{{|;)" + ), + "enum": re.compile(r"^\s*enum\s+(?:class\s+)?({name})(?:\s|:|\{{|;)"), + "typedef": re.compile(r"\btypedef\b.*\b({name})\s*;"), + "global": re.compile( + r"^(?:extern\s+)?(?:const\s+)?[\w:<>*&\s]+?\b({name})\s*(?:;|=|\[)" + ), +} + + +def make_pattern(kind: str, name: str) -> re.Pattern: + return re.compile(PATTERNS[kind].pattern.replace("{name}", re.escape(name))) + + +# ------------------------------------------------------------------- +# Search +# ------------------------------------------------------------------- + +Match = Tuple[str, int, str, str] # (filepath, lineno, kind, line_text) + + +def search_file(filepath: str, name: str, kinds: List[str]) -> List[Match]: + results: List[Match] = [] + compiled = {k: make_pattern(k, name) for k in kinds} + try: + with open(filepath, encoding="utf-8", errors="ignore") as f: + for lineno, line in enumerate(f, 1): + for kind, pat in compiled.items(): + if pat.search(line): + results.append((filepath, lineno, kind, line.rstrip())) + break # only report the first kind match per line + except OSError: + pass + return results + + +def search_tree(name: str, kinds: List[str]) -> List[Match]: + results: List[Match] = [] + for dirpath, _dirs, files in os.walk(SRC_DIR): + for fname in files: + if os.path.splitext(fname)[1] in SOURCE_EXTS: + fpath = os.path.join(dirpath, fname) + results.extend(search_file(fpath, name, kinds)) + return results + + +# ------------------------------------------------------------------- +# Main +# ------------------------------------------------------------------- + +ALL_KINDS = ["struct", "class", "enum", "typedef", "global"] + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Search src/ for existing symbol definitions", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument("name", help="Symbol name to search for") + parser.add_argument( + "--type", + dest="kind", + choices=ALL_KINDS, + help="Restrict search to a specific symbol kind", + ) + parser.add_argument( + "--all", + action="store_true", + help="Search for all symbol kinds (default when --type is omitted)", + ) + args = parser.parse_args() + + kinds = [args.kind] if args.kind else ALL_KINDS + + if not os.path.isdir(SRC_DIR): + print(f"Error: src/ directory not found at {SRC_DIR}", file=sys.stderr) + sys.exit(1) + + matches = search_tree(args.name, kinds) + + if not matches: + print(f"Not found: '{args.name}' in {SRC_DIR}") + print("Safe to declare — symbol does not exist in the project.") + sys.exit(0) + + # Group by file for readable output + by_file: dict = {} + for filepath, lineno, kind, text in matches: + rel = os.path.relpath(filepath, root_dir) + by_file.setdefault(rel, []).append((lineno, kind, text)) + + print(f"Found '{args.name}' in {len(by_file)} file(s):\n") + for rel_path, hits in sorted(by_file.items()): + print(f" {rel_path}") + for lineno, kind, text in hits: + print(f" {lineno:>5}: [{kind}] {text.strip()}") + print() + + print("Include the header above instead of redeclaring.") + sys.exit(0) + + +if __name__ == "__main__": + main() From 19f859ff938acd360ce86589e7d087636b7ae0f8 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 10 Mar 2026 15:49:48 +0100 Subject: [PATCH 03/71] Make non-owning VecHashMap clear without deletes --- .../Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index e63900e66..644670807 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -87,10 +87,12 @@ template Date: Tue, 10 Mar 2026 20:35:28 +0100 Subject: [PATCH 04/71] 99.3% --- AGENTS.md | 3 +++ src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h | 6 ++++-- src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h | 9 ++++++++- .../Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp | 2 ++ .../Tools/AttribSys/Runtime/Common/AttribDatabase.cpp | 2 ++ .../Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h | 3 +-- 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 57bb3a399..d2ebf6859 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -236,3 +236,6 @@ TU: | Function: +### NamedRodataForInlinedAllocatorStrings +TU: zAttribSys | Function: DatabaseExportPolicy::Initialize +When an inlined allocator path must reference a specific rodata symbol, replace a repeated string literal with a named `static const char[]` so the compiler preserves the expected rodata label and relocation pattern. diff --git a/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h b/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h index f47d09a39..a6037c36c 100644 --- a/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h +++ b/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h @@ -64,6 +64,8 @@ _STLP_BEGIN_NAMESPACE +static const char __stlp_node_alloc_name[] = {'S', 'T', 'L', '\0'}; + #if defined(_STLP_USE_RAW_SGI_ALLOCATORS) template struct __allocator; #endif @@ -233,7 +235,7 @@ template class __node_alloc { /* __n must be > 0 */ static void *_STLP_CALL allocate(size_t __n) { #ifndef CLANGD_DAMNIT - return gFastMem.Alloc(__n, "STL"); + return gFastMem.Alloc(__n, __stlp_node_alloc_name); #else return (__n > (size_t)_MAX_BYTES) ? __stl_new(__n) : _M_allocate(__n); #endif @@ -241,7 +243,7 @@ template class __node_alloc { /* __p may not be 0 */ static void _STLP_CALL deallocate(void *__p, size_t __n) { #ifndef CLANGD_DAMNIT - gFastMem.Free(__p, __n, "STL"); + gFastMem.Free(__p, __n, __stlp_node_alloc_name); #else if (__n > (size_t)_MAX_BYTES) __stl_delete(__p); diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index f458308a4..6181c0760 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -139,7 +139,14 @@ class TypeDesc { class TypeDescPtrVec : public std::vector {}; // total size: 0x10 -class TypeTable : public std::set {}; +class TypeTable : public std::set { + public: + ~TypeTable(); + + void operator delete(void *ptr, std::size_t bytes) { + Free(ptr, bytes, "Attrib::TypeTable"); + } +}; // total size: 0x8 class CollectionList : public std::list {}; diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 81714e16e..03afcbfda 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -140,4 +140,6 @@ void Class::FreeLayout(void *layout) const { } } +ClassPrivate::CollectionHashMap::~CollectionHashMap() {} + }; // namespace Attrib diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index 6f9d1d145..7913faed8 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -239,4 +239,6 @@ Key StringToKey(const char *str) { return StringHash32(str); } +TypeTable::~TypeTable() {} + }; // namespace Attrib diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index 66f23f886..b257bd9f3 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -202,10 +202,9 @@ class HashMap { unsigned int actualIndex = HashMapTablePolicy::KeyIndex(key, mTableSize, mKeyShift); unsigned int searchLen = 0; unsigned int maxSearchLen = table[actualIndex].MaxSearch(); - unsigned int currline = (uintptr_t)&table[actualIndex] >> (lineSize & 0x3f); // TODO huh? + unsigned int currline = (uintptr_t)&table[actualIndex] >> (lineSize & 0x3f); if (currline != 0) { - result = 1; // commenting this out improves the score prevline = currline; } for (; searchLen < maxSearchLen; searchLen++) { From f29f08dca345dd77ecfd97dbf6bd61a16af2ae8a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 10 Mar 2026 21:12:29 +0100 Subject: [PATCH 05/71] 99.8% --- AGENTS.md | 4 ++++ .../Indep/Libs/Support/Utility/UVectorMath.h | 4 ++-- .../Indep/Tools/AttribSys/Runtime/AttribSys.h | 18 ++++++++++++++++++ .../Runtime/Common/AttribDatabase.cpp | 1 + .../AttribSys/Runtime/Common/AttribHashMap.h | 3 ++- .../Tools/AttribSys/Runtime/VecHashMap64.h | 5 ++++- 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d2ebf6859..c4a67f725 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,3 +239,7 @@ TU: | Function: ### NamedRodataForInlinedAllocatorStrings TU: zAttribSys | Function: DatabaseExportPolicy::Initialize When an inlined allocator path must reference a specific rodata symbol, replace a repeated string literal with a named `static const char[]` so the compiler preserves the expected rodata label and relocation pattern. + +### ExplicitInlineSpecialMembersForSTLElements +TU: zAttribSys | Function: _STL::_Rb_tree::_M_insert +If an STL node insertion path refuses to match, check whether the element type is missing explicit inline special members that the original source exposed. Adding the Dwarf-backed `operator new`, `operator delete`, placement `new`, copy constructor, and tiny accessors to `TypeDesc` made the tree node creation/insertion path match exactly. diff --git a/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h b/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h index ae44b2d95..87987e57a 100644 --- a/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h +++ b/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h @@ -667,7 +667,7 @@ inline float V3DistanceSquared(const UMath::Vector3 &a, const UMath::Vector3 &b) } // TODO where to put these? TODO only one of them uses IntAsFloat actually -static const float kFloatScaleUp = IntAsFloat(0x7E800000); -static const float kFloatScaleDown = IntAsFloat(0x80000000); +static const float kFloatScaleUp = IntAsFloat(0x00800000); +static const float kFloatScaleDown = 1.0f / kFloatScaleUp; #endif diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index 6181c0760..ff7151d62 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -100,8 +100,22 @@ class TypeDesc { static ITypeHandler *Lookup(Type t); static Type NameToType(const char *name); + void *operator new(std::size_t bytes) { + return Alloc(bytes, "Attrib::TypeDesc"); + } + + void operator delete(void *ptr, std::size_t bytes) { + Free(ptr, bytes, "Attrib::TypeDesc"); + } + + void *operator new(std::size_t, void *ptr) { + return ptr; + } + TypeDesc() : mType(0), mName(""), mSize(0), mIndex(0), mHandler(nullptr) {} + TypeDesc(const TypeDesc &src) : mType(src.mType), mName(src.mName), mSize(src.mSize), mIndex(src.mIndex), mHandler(src.mHandler) {} + TypeDesc(unsigned int t) : mType(t), mName(nullptr), mSize(0), mIndex(0), mHandler(Lookup(t)) {} TypeDesc(const char *name, std::size_t size, std::size_t index) @@ -111,6 +125,10 @@ class TypeDesc { return mType; } + const char *GetName() const { + return mName; + } + unsigned int GetSize() const { return mSize; } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index 7913faed8..e40d7529d 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -1,6 +1,7 @@ #include "../AttribHash.h" #include "../AttribSys.h" #include "AttribPrivate.h" +#include "Speed/Indep/Libs/Support/Utility/UMath.h" #include "Speed/Indep/Tools/AttribSys/Runtime/AttribLoadAndGo.h" #include diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index b257bd9f3..7482c187a 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -207,7 +207,7 @@ class HashMap { if (currline != 0) { prevline = currline; } - for (; searchLen < maxSearchLen; searchLen++) { + while (searchLen < maxSearchLen) { if (table[actualIndex].GetKey() == key) { return result; } @@ -217,6 +217,7 @@ class HashMap { prevline = currline; result++; } + searchLen++; } return result; diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 644670807..787015ed1 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -234,7 +234,10 @@ template Date: Wed, 11 Mar 2026 13:05:54 +0100 Subject: [PATCH 06/71] zAttribSys: push to 99.9% match (179/196 functions) Key changes: - VecHashMap: struct, unsigned int members, private data, Clear() inlined into dtor - CollectionHashMap: struct, out-of-line ctor+dtor matching DWARF method order - Added Class::Reserve inline, GetTableNodeSize through mCollections - Node: struct, removed operator new, unsigned int types throughout - Moved GetNextValidIndex/GetKeyAtIndex to VecHashMap base - AttribHashMap: restructured to match DWARF layout NON_MATCHING (r6/r7 register swap, 300+ experiments exhausted): - Class::RemoveCollection: 98.6% - Database::RemoveClass: 98.5% Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build.ninja.bak | 1999 +++++++++++++++++ .../Indep/Tools/AttribSys/Runtime/AttribSys.h | 162 +- .../AttribSys/Runtime/Common/AttribClass.cpp | 14 +- .../Runtime/Common/AttribCollection.cpp | 2 +- .../Runtime/Common/AttribDatabase.cpp | 1 + .../AttribSys/Runtime/Common/AttribHashMap.h | 188 +- .../AttribSys/Runtime/Common/AttribPrivate.h | 20 +- .../Tools/AttribSys/Runtime/VecHashMap64.h | 242 +- 8 files changed, 2365 insertions(+), 263 deletions(-) create mode 100644 build.ninja.bak diff --git a/build.ninja.bak b/build.ninja.bak new file mode 100644 index 000000000..42d15171d --- /dev/null +++ b/build.ninja.bak @@ -0,0 +1,1999 @@ +ninja_required_version = 1.3 + +# The arguments passed to configure.py, for rerunning it. +configure_args = +python = "/Users/johannberger/.pyenv/versions/3.12.1/bin/python" + +# Variables +ldflags = -T config/GOWE69/ldscript.ld +toolchain_version = ProDG/3.9.3 +objdiff_report_args = + +# Tooling +rule download_tool + command = $python tools/download_tool.py $tool $out --tag $tag + description = TOOL $out +rule decompctx + command = $python tools/decompctx.py $in -o $out -d $out.d $includes $ + $excludes $defines + description = CTX $in + depfile = $out.d + deps = gcc +build build/tools/dtk: download_tool | tools/download_tool.py + tool = dtk + tag = v1.8.3 +build build/tools/objdiff-cli: download_tool | tools/download_tool.py + tool = objdiff-cli + tag = v3.7.0 +build build/tools/sjiswrap.exe: download_tool | tools/download_tool.py + tool = sjiswrap + tag = v1.2.0 +build build/compilers: download_tool | tools/download_tool.py + tool = compilers + tag = 20251015 +build build/ppc_binutils: download_tool | tools/download_tool.py + tool = ppc_binutils + tag = 2.42-1 + +# Download all tools +build tools: phony build/tools/sjiswrap.exe wine build/compilers $ + build/ppc_binutils build/tools/objdiff-cli build/tools/dtk + +# Link ELF file +rule link + command = wine build/compilers/$toolchain_version/ngcld.exe $ldflags -o $ + $out @$out.rsp + description = LINK $out + rspfile = $out.rsp + rspfile_content = $in_newline + +# Generate DOL +rule elf2dol + command = build/tools/dtk elf2dol $in $out + description = DOL $out + +# MWCC build +rule mwcc + command = wine build/compilers/$toolchain_version/mwcceppc.exe $cflags $ + -MMD -c $in -o $basedir && $python tools/transform_dep.py $basefile.d $ + $basefile.d + description = MWCC $out + depfile = $basefile.d + deps = gcc + +# MSVC build +msvc_deps_prefix = Note: including file: +rule msvc + command = wine build/compilers/$toolchain_version/cl.exe $cflags $ + /showIncludes /Fo$out $in + description = MSVC $out + deps = msvc + +# MWCC build (with UTF-8 to Shift JIS wrapper) +rule mwcc_sjis + command = wine build/tools/sjiswrap.exe $ + build/compilers/$toolchain_version/mwcceppc.exe $cflags -MMD -c $in $ + -o $basedir && $python tools/transform_dep.py $basefile.d $basefile.d + description = MWCC $out + depfile = $basefile.d + deps = gcc + +# ProDG build +rule prodg + command = env $ + SN_NGC_PATH=/Users/johannberger/nfsmw-zattribsys/build/compilers/$toolchain_version $ + wine build/compilers/$toolchain_version/ngccc.exe $cflags -MMD -c -o $ + $out $in && $python tools/transform_dep.py $basefile.d $basefile.d + description = ProDG $out + depfile = $basefile.d + deps = gcc + +# EE-GCC build +rule ee-gcc + command = env DEPENDENCIES_OUTPUT=$basefile.d wine $ + build/compilers/$toolchain_version/bin/ee-gcc.exe $cflags -c -o $out $ + $in && $python tools/transform_dep.py $basefile.d $basefile.d + description = EE-GCC $out + depfile = $basefile.d + deps = gcc + +# Assemble asm +rule as + command = build/ppc_binutils/powerpc-eabi-as $asflags -o $out $in -MD $ + $out.d && build/tools/dtk elf fixup $out $out + description = AS $out + +# Custom project build rules (pre/post-processing) +rule hashgen + command = $python tools/hasher.py $in $out + description = HASH $out + +# Custom build steps (pre-compile) +build src/Speed/Indep/Src/Generated/Hashes/zAI.h: hashgen $ + src/Speed/Indep/SourceLists/zAI.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zAnim.h: hashgen $ + src/Speed/Indep/SourceLists/zAnim.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zAttribSys.h: hashgen $ + src/Speed/Indep/SourceLists/zAttribSys.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zBWare.h: hashgen $ + src/Speed/Indep/SourceLists/zBWare.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zCamera.h: hashgen $ + src/Speed/Indep/SourceLists/zCamera.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zComms.h: hashgen $ + src/Speed/Indep/SourceLists/zComms.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zDebug.h: hashgen $ + src/Speed/Indep/SourceLists/zDebug.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zDynamics.h: hashgen $ + src/Speed/Indep/SourceLists/zDynamics.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zEagl4Anim.h: hashgen $ + src/Speed/Indep/SourceLists/zEagl4Anim.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zEAXSound.h: hashgen $ + src/Speed/Indep/SourceLists/zEAXSound.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zEAXSound2.h: hashgen $ + src/Speed/Indep/SourceLists/zEAXSound2.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zEcstasy.h: hashgen $ + src/Speed/Indep/SourceLists/zEcstasy.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zFe.h: hashgen $ + src/Speed/Indep/SourceLists/zFe.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zFe2.h: hashgen $ + src/Speed/Indep/SourceLists/zFe2.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zFEng.h: hashgen $ + src/Speed/Indep/SourceLists/zFEng.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zFoundation.h: hashgen $ + src/Speed/Indep/SourceLists/zFoundation.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zGameModes.h: hashgen $ + src/Speed/Indep/SourceLists/zGameModes.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zGameplay.h: hashgen $ + src/Speed/Indep/SourceLists/zGameplay.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zLua.h: hashgen $ + src/Speed/Indep/SourceLists/zLua.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zMain.h: hashgen $ + src/Speed/Indep/SourceLists/zMain.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zMisc.h: hashgen $ + src/Speed/Indep/SourceLists/zMisc.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zMiscSmall.h: hashgen $ + src/Speed/Indep/SourceLists/zMiscSmall.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zMission.h: hashgen $ + src/Speed/Indep/SourceLists/zMission.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zPhysics.h: hashgen $ + src/Speed/Indep/SourceLists/zPhysics.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zPhysicsBehaviors.h: hashgen $ + src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zPlatform.h: hashgen $ + src/Speed/Indep/SourceLists/zPlatform.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zRender.h: hashgen $ + src/Speed/Indep/SourceLists/zRender.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zSim.h: hashgen $ + src/Speed/Indep/SourceLists/zSim.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zSpeech.h: hashgen $ + src/Speed/Indep/SourceLists/zSpeech.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zTrack.h: hashgen $ + src/Speed/Indep/SourceLists/zTrack.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zWorld.h: hashgen $ + src/Speed/Indep/SourceLists/zWorld.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zWorld2.h: hashgen $ + src/Speed/Indep/SourceLists/zWorld2.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zOnline.h: hashgen $ + src/Speed/Indep/SourceLists/zOnline.cpp + +build src/Speed/Indep/Src/Generated/Hashes/zFeOverlay.h: hashgen $ + src/Speed/Indep/SourceLists/zFeOverlay.cpp + +build pre-compile: phony src/Speed/Indep/Src/Generated/Hashes/zAI.h $ + src/Speed/Indep/Src/Generated/Hashes/zAnim.h $ + src/Speed/Indep/Src/Generated/Hashes/zAttribSys.h $ + src/Speed/Indep/Src/Generated/Hashes/zBWare.h $ + src/Speed/Indep/Src/Generated/Hashes/zCamera.h $ + src/Speed/Indep/Src/Generated/Hashes/zComms.h $ + src/Speed/Indep/Src/Generated/Hashes/zDebug.h $ + src/Speed/Indep/Src/Generated/Hashes/zDynamics.h $ + src/Speed/Indep/Src/Generated/Hashes/zEagl4Anim.h $ + src/Speed/Indep/Src/Generated/Hashes/zEAXSound.h $ + src/Speed/Indep/Src/Generated/Hashes/zEAXSound2.h $ + src/Speed/Indep/Src/Generated/Hashes/zEcstasy.h $ + src/Speed/Indep/Src/Generated/Hashes/zFe.h $ + src/Speed/Indep/Src/Generated/Hashes/zFe2.h $ + src/Speed/Indep/Src/Generated/Hashes/zFEng.h $ + src/Speed/Indep/Src/Generated/Hashes/zFoundation.h $ + src/Speed/Indep/Src/Generated/Hashes/zGameModes.h $ + src/Speed/Indep/Src/Generated/Hashes/zGameplay.h $ + src/Speed/Indep/Src/Generated/Hashes/zLua.h $ + src/Speed/Indep/Src/Generated/Hashes/zMain.h $ + src/Speed/Indep/Src/Generated/Hashes/zMisc.h $ + src/Speed/Indep/Src/Generated/Hashes/zMiscSmall.h $ + src/Speed/Indep/Src/Generated/Hashes/zMission.h $ + src/Speed/Indep/Src/Generated/Hashes/zPhysics.h $ + src/Speed/Indep/Src/Generated/Hashes/zPhysicsBehaviors.h $ + src/Speed/Indep/Src/Generated/Hashes/zPlatform.h $ + src/Speed/Indep/Src/Generated/Hashes/zRender.h $ + src/Speed/Indep/Src/Generated/Hashes/zSim.h $ + src/Speed/Indep/Src/Generated/Hashes/zSpeech.h $ + src/Speed/Indep/Src/Generated/Hashes/zTrack.h $ + src/Speed/Indep/Src/Generated/Hashes/zWorld.h $ + src/Speed/Indep/Src/Generated/Hashes/zWorld2.h $ + src/Speed/Indep/Src/Generated/Hashes/zOnline.h $ + src/Speed/Indep/Src/Generated/Hashes/zFeOverlay.h +# Source files +# Speed/Indep/SourceLists/zAI.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zAI.o: prodg $ + src/Speed/Indep/SourceLists/zAI.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAI +build build/GOWE69/src/Speed/Indep/SourceLists/zAI.ctx: decompctx $ + src/Speed/Indep/SourceLists/zAI.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zAnim.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zAnim.o: prodg $ + src/Speed/Indep/SourceLists/zAnim.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAnim +build build/GOWE69/src/Speed/Indep/SourceLists/zAnim.ctx: decompctx $ + src/Speed/Indep/SourceLists/zAnim.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zAttribSys.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o: prodg $ + src/Speed/Indep/SourceLists/zAttribSys.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys +build build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.ctx: decompctx $ + src/Speed/Indep/SourceLists/zAttribSys.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zBWare.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zBWare.o: prodg $ + src/Speed/Indep/SourceLists/zBWare.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zBWare +build build/GOWE69/src/Speed/Indep/SourceLists/zBWare.ctx: decompctx $ + src/Speed/Indep/SourceLists/zBWare.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zCamera.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zCamera.o: prodg $ + src/Speed/Indep/SourceLists/zCamera.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zCamera +build build/GOWE69/src/Speed/Indep/SourceLists/zCamera.ctx: decompctx $ + src/Speed/Indep/SourceLists/zCamera.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zDebug.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zDebug.o: prodg $ + src/Speed/Indep/SourceLists/zDebug.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zDebug +build build/GOWE69/src/Speed/Indep/SourceLists/zDebug.ctx: decompctx $ + src/Speed/Indep/SourceLists/zDebug.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zDynamics.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.o: prodg $ + src/Speed/Indep/SourceLists/zDynamics.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zDynamics +build build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.ctx: decompctx $ + src/Speed/Indep/SourceLists/zDynamics.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zEagl4Anim.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.o: prodg $ + src/Speed/Indep/SourceLists/zEagl4Anim.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim +build build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.ctx: decompctx $ + src/Speed/Indep/SourceLists/zEagl4Anim.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zEAXSound.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.o: prodg $ + src/Speed/Indep/SourceLists/zEAXSound.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound +build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.ctx: decompctx $ + src/Speed/Indep/SourceLists/zEAXSound.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zEAXSound2.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.o: prodg $ + src/Speed/Indep/SourceLists/zEAXSound2.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2 +build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.ctx: decompctx $ + src/Speed/Indep/SourceLists/zEAXSound2.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zEcstasy.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.o: prodg $ + src/Speed/Indep/SourceLists/zEcstasy.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy +build build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.ctx: decompctx $ + src/Speed/Indep/SourceLists/zEcstasy.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zFe.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zFe.o: prodg $ + src/Speed/Indep/SourceLists/zFe.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFe +build build/GOWE69/src/Speed/Indep/SourceLists/zFe.ctx: decompctx $ + src/Speed/Indep/SourceLists/zFe.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zFe2.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zFe2.o: prodg $ + src/Speed/Indep/SourceLists/zFe2.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFe2 +build build/GOWE69/src/Speed/Indep/SourceLists/zFe2.ctx: decompctx $ + src/Speed/Indep/SourceLists/zFe2.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zFEng.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zFEng.o: prodg $ + src/Speed/Indep/SourceLists/zFEng.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFEng +build build/GOWE69/src/Speed/Indep/SourceLists/zFEng.ctx: decompctx $ + src/Speed/Indep/SourceLists/zFEng.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zFoundation.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.o: prodg $ + src/Speed/Indep/SourceLists/zFoundation.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFoundation +build build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.ctx: decompctx $ + src/Speed/Indep/SourceLists/zFoundation.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zGameModes.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.o: prodg $ + src/Speed/Indep/SourceLists/zGameModes.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zGameModes +build build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.ctx: decompctx $ + src/Speed/Indep/SourceLists/zGameModes.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zGameplay.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.o: prodg $ + src/Speed/Indep/SourceLists/zGameplay.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zGameplay +build build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.ctx: decompctx $ + src/Speed/Indep/SourceLists/zGameplay.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zLua.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zLua.o: prodg $ + src/Speed/Indep/SourceLists/zLua.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zLua +build build/GOWE69/src/Speed/Indep/SourceLists/zLua.ctx: decompctx $ + src/Speed/Indep/SourceLists/zLua.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zMain.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zMain.o: prodg $ + src/Speed/Indep/SourceLists/zMain.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMain +build build/GOWE69/src/Speed/Indep/SourceLists/zMain.ctx: decompctx $ + src/Speed/Indep/SourceLists/zMain.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zMisc.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zMisc.o: prodg $ + src/Speed/Indep/SourceLists/zMisc.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMisc +build build/GOWE69/src/Speed/Indep/SourceLists/zMisc.ctx: decompctx $ + src/Speed/Indep/SourceLists/zMisc.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zMiscSmall.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.o: prodg $ + src/Speed/Indep/SourceLists/zMiscSmall.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall +build build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.ctx: decompctx $ + src/Speed/Indep/SourceLists/zMiscSmall.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zMission.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zMission.o: prodg $ + src/Speed/Indep/SourceLists/zMission.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMission +build build/GOWE69/src/Speed/Indep/SourceLists/zMission.ctx: decompctx $ + src/Speed/Indep/SourceLists/zMission.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zPhysics.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.o: prodg $ + src/Speed/Indep/SourceLists/zPhysics.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPhysics +build build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.ctx: decompctx $ + src/Speed/Indep/SourceLists/zPhysics.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zPhysicsBehaviors.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.o: prodg $ + src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors +build build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.ctx: $ + decompctx src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zPlatform.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.o: prodg $ + src/Speed/Indep/SourceLists/zPlatform.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPlatform +build build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.ctx: decompctx $ + src/Speed/Indep/SourceLists/zPlatform.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zRender.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zRender.o: prodg $ + src/Speed/Indep/SourceLists/zRender.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zRender +build build/GOWE69/src/Speed/Indep/SourceLists/zRender.ctx: decompctx $ + src/Speed/Indep/SourceLists/zRender.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zSim.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zSim.o: prodg $ + src/Speed/Indep/SourceLists/zSim.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zSim +build build/GOWE69/src/Speed/Indep/SourceLists/zSim.ctx: decompctx $ + src/Speed/Indep/SourceLists/zSim.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zSpeech.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.o: prodg $ + src/Speed/Indep/SourceLists/zSpeech.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zSpeech +build build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.ctx: decompctx $ + src/Speed/Indep/SourceLists/zSpeech.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zTrack.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zTrack.o: prodg $ + src/Speed/Indep/SourceLists/zTrack.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zTrack +build build/GOWE69/src/Speed/Indep/SourceLists/zTrack.ctx: decompctx $ + src/Speed/Indep/SourceLists/zTrack.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zWorld.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zWorld.o: prodg $ + src/Speed/Indep/SourceLists/zWorld.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zWorld +build build/GOWE69/src/Speed/Indep/SourceLists/zWorld.ctx: decompctx $ + src/Speed/Indep/SourceLists/zWorld.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zWorld2.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.o: prodg $ + src/Speed/Indep/SourceLists/zWorld2.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zWorld2 +build build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.ctx: decompctx $ + src/Speed/Indep/SourceLists/zWorld2.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zOnline.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zOnline.o: prodg $ + src/Speed/Indep/SourceLists/zOnline.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zOnline +build build/GOWE69/src/Speed/Indep/SourceLists/zOnline.ctx: decompctx $ + src/Speed/Indep/SourceLists/zOnline.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Speed/Indep/SourceLists/zFeOverlay.cpp: Game (linked False) +build build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.o: prodg $ + src/Speed/Indep/SourceLists/zFeOverlay.cpp | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c++ + basedir = build/GOWE69/src/Speed/Indep/SourceLists + basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay +build build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.ctx: decompctx $ + src/Speed/Indep/SourceLists/zFeOverlay.cpp | tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Packages/snd/source/library/cmn/SNDI_findprime.c: snd (linked False) +build build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.o: $ + prodg src/Packages/snd/source/library/cmn/SNDI_findprime.c | $ + build/compilers tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c + basedir = build/GOWE69/src/Packages/snd/source/library/cmn + basefile = build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime +build build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.ctx: $ + decompctx src/Packages/snd/source/library/cmn/SNDI_findprime.c | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Packages/snd/source/library/cmn/sbpatinf.c: snd (linked False) +build build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.o: prodg $ + src/Packages/snd/source/library/cmn/sbpatinf.c | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c + basedir = build/GOWE69/src/Packages/snd/source/library/cmn + basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf +build build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.ctx: $ + decompctx src/Packages/snd/source/library/cmn/sbpatinf.c | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Packages/snd/source/library/cmn/sgetpvol.c: snd (linked False) +build build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.o: prodg $ + src/Packages/snd/source/library/cmn/sgetpvol.c | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c + basedir = build/GOWE69/src/Packages/snd/source/library/cmn + basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol +build build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.ctx: $ + decompctx src/Packages/snd/source/library/cmn/sgetpvol.c | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Packages/snd/source/library/cmn/sstgetpv.c: snd (linked False) +build build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.o: prodg $ + src/Packages/snd/source/library/cmn/sstgetpv.c | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c + basedir = build/GOWE69/src/Packages/snd/source/library/cmn + basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv +build build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.ctx: $ + decompctx src/Packages/snd/source/library/cmn/sstgetpv.c | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# Packages/snd/source/library/cmn/stimerem.c: snd (linked False) +build build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.o: prodg $ + src/Packages/snd/source/library/cmn/stimerem.c | build/compilers $ + tools/transform_dep.py || pre-compile + toolchain_version = ProDG/3.9.3 + cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ + src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ + -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ + -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ + -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ + -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ + -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ + -DLUA_NUMBER=float -x c + basedir = build/GOWE69/src/Packages/snd/source/library/cmn + basefile = build/GOWE69/src/Packages/snd/source/library/cmn/stimerem +build build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.ctx: $ + decompctx src/Packages/snd/source/library/cmn/stimerem.c | $ + tools/decompctx.py + includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ + src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ + src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ + include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + src/Packages -I ./ -I src -I build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + +# LibSN/crt0.s: libsn (linked False) +build build/GOWE69/src/LibSN/crt0.o: as src/LibSN/crt0.s | $ + build/ppc_binutils build/tools/dtk build/GOWE69/include/macros.inc || $ + pre-compile + asflags = -mgekko --strip-local-absolute -I include -I $ + build/GOWE69/include --defsym BUILD_VERSION=0 + +# Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp: os +# (linked False) +build $ + build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o: $ + mwcc $ + src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp $ + | build/compilers tools/transform_dep.py || pre-compile + toolchain_version = GC/1.2.5n + cflags = -nodefaults -proc gekko -align powerpc -enum int -fp hardware $ + -Cpp_exceptions off -O4,p -inline auto -pragma "cats off" -pragma $ + "warn_notinlined off" -maxerrors 1 -nosyspath -RTTI off -fp_contract $ + on -str reuse -i include -i $ + src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -i $ + build/GOWE69/include -multibyte -DVERSION=0 -lang=c++ + basedir = build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os + basefile = $ + build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init +build $ + build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.ctx: $ + decompctx $ + src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp $ + | tools/decompctx.py + includes = -I include -I $ + src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ + build/GOWE69/include + excludes = + defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ + _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC + + +build post-compile: phony || pre-compile +# Link main +build build/GOWE69/main.elf: link $ + build/GOWE69/obj/auto_10_804FFF40_sdata2.o $ + build/GOWE69/obj/auto_10_80500140_sdata2.o $ + build/GOWE69/obj/auto_10_80500240_sdata2.o $ + build/GOWE69/obj/auto_10_80500390_sdata2.o $ + build/GOWE69/obj/auto_10_805008A8_sdata2.o $ + build/GOWE69/obj/auto_10_80500938_sdata2.o $ + build/GOWE69/obj/auto_10_80500A00_sdata2.o $ + build/GOWE69/obj/auto_09_804FF8C0_sbss.o $ + build/GOWE69/obj/auto_09_804FF9B0_sbss.o $ + build/GOWE69/obj/auto_09_804FFC40_sbss.o build/GOWE69/obj/prodg_fixes.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zAI.o $ + build/GOWE69/obj/auto_08_804FEDD0_sdata.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zAnim.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zAttribSys.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zBWare.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zCamera.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zDebug.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zDynamics.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zEagl4Anim.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zEAXSound.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zEAXSound2.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zEcstasy.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zFe.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zFe2.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zFEng.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zFoundation.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zGameModes.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zGameplay.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zLua.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zMain.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zMisc.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zMiscSmall.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zMission.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zPhysics.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zPhysicsBehaviors.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zPlatform.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zRender.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zSim.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zSpeech.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zTrack.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zWorld.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zWorld2.o $ + build/GOWE69/obj/auto_05_8040FBB8_rodata.o $ + build/GOWE69/obj/auto_05_80412168_rodata.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zOnline.o $ + build/GOWE69/obj/Speed/Indep/SourceLists/zFeOverlay.o $ + build/GOWE69/obj/auto_07_804B4DE8_bss.o $ + build/GOWE69/obj/auto_07_804BA0B0_bss.o $ + build/GOWE69/obj/auto_07_804BA100_bss.o $ + build/GOWE69/obj/auto_07_804BA158_bss.o $ + build/GOWE69/obj/auto_07_804BAB60_bss.o $ + build/GOWE69/obj/auto_07_804BABF8_bss.o $ + build/GOWE69/obj/auto_07_804BAC88_bss.o $ + build/GOWE69/obj/auto_07_804BAD58_bss.o $ + build/GOWE69/obj/auto_07_804BAEA0_bss.o $ + build/GOWE69/obj/auto_07_804BAED0_bss.o $ + build/GOWE69/obj/auto_07_804BAF20_bss.o $ + build/GOWE69/obj/auto_07_804BB160_bss.o $ + build/GOWE69/obj/auto_07_804BB790_bss.o $ + build/GOWE69/obj/auto_07_804BBE28_bss.o $ + build/GOWE69/obj/auto_07_804BC0C8_bss.o $ + build/GOWE69/obj/auto_07_804DA698_bss.o $ + build/GOWE69/obj/auto_07_804DA970_bss.o $ + build/GOWE69/obj/auto_07_804DAA80_bss.o $ + build/GOWE69/obj/auto_07_804DDD80_bss.o $ + build/GOWE69/obj/auto_07_804E2800_bss.o $ + build/GOWE69/obj/auto_07_804E2840_bss.o $ + build/GOWE69/obj/auto_06_804394D0_data.o $ + build/GOWE69/obj/auto_06_8043A138_data.o $ + build/GOWE69/obj/auto_06_8043A330_data.o $ + build/GOWE69/obj/auto_06_8043A748_data.o $ + build/GOWE69/obj/auto_06_8043A978_data.o $ + build/GOWE69/obj/auto_06_8043AB50_data.o $ + build/GOWE69/obj/auto_06_8043BA70_data.o $ + build/GOWE69/obj/auto_06_8043C2E0_data.o $ + build/GOWE69/obj/auto_06_8044D548_data.o $ + build/GOWE69/obj/auto_06_8044D938_data.o $ + build/GOWE69/obj/auto_06_8044DAC0_data.o $ + build/GOWE69/obj/auto_06_8044DC70_data.o $ + build/GOWE69/obj/auto_06_8044E020_data.o $ + build/GOWE69/obj/auto_06_8044E090_data.o $ + build/GOWE69/obj/auto_06_8044E488_data.o $ + build/GOWE69/obj/auto_06_8044E4E0_data.o $ + build/GOWE69/obj/auto_06_8044E528_data.o $ + build/GOWE69/obj/auto_06_8044E5B0_data.o $ + build/GOWE69/obj/auto_06_8044E780_data.o $ + build/GOWE69/obj/auto_06_8044F030_data.o $ + build/GOWE69/obj/auto_06_8044F340_data.o $ + build/GOWE69/obj/auto_06_8044F450_data.o $ + build/GOWE69/obj/auto_06_8044F568_data.o $ + build/GOWE69/obj/auto_06_80450134_data.o $ + build/GOWE69/obj/auto_06_80452928_data.o $ + build/GOWE69/obj/auto_06_80452970_data.o $ + build/GOWE69/obj/auto_06_804563A0_data.o $ + build/GOWE69/obj/auto_06_80456420_data.o $ + build/GOWE69/obj/auto_02_803C6A3C_over.o $ + build/GOWE69/obj/auto_02_803C7338_over.o build/GOWE69/obj/asd2.o $ + build/GOWE69/obj/auto_03_803C8C20_ctors.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/PPCArch.o $ + build/GOWE69/obj/auto_01_8031F5D8_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OS.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAlarm.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAlloc.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSArena.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAudioSystem.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSCache.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSContext.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSError.o $ + build/GOWE69/obj/auto_01_803242FC_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSInterrupt.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSLink.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSMemory.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSMutex.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSReboot.o $ + build/GOWE69/obj/auto_01_80326948_text.o $ + build/GOWE69/obj/auto_01_8032DACC_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/pad/PadClamp.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/pad/Pad.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/ai.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/ar/ar.o $ + build/GOWE69/obj/auto_01_80332CFC_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardMount.o $ + build/GOWE69/obj/auto_01_80334F48_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardBios.o $ + build/GOWE69/obj/auto_01_8033667C_text.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardUnlock.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardRdwr.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardBlock.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardDir.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardCheck.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardOpen.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXInit.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXFifo.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXAttr.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXMisc.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXGeometry.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXFrameBuf.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXLight.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTexture.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXBump.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTev.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXPixel.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXDisplayList.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTransform.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXPerf.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/exi/EXIBios.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/exi/EXIUart.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/si/SIBios.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/si/SISamplingRate.o $ + build/GOWE69/obj/auto_01_803453B0_text.o $ + build/GOWE69/obj/auto_01_80348334_text.o $ + build/GOWE69/obj/auto_01_80348640_text.o $ + build/GOWE69/obj/auto_01_80348EE4_text.o $ + build/GOWE69/obj/auto_01_8034939C_text.o $ + build/GOWE69/obj/auto_01_8034983C_text.o $ + build/GOWE69/obj/auto_01_8034A348_text.o $ + build/GOWE69/obj/auto_01_8034A390_text.o $ + build/GOWE69/obj/auto_01_8034ADB0_text.o $ + build/GOWE69/obj/auto_01_8034ADE4_text.o $ + build/GOWE69/obj/auto_01_8034B900_text.o $ + build/GOWE69/obj/auto_01_8034BAC4_text.o $ + build/GOWE69/obj/auto_01_8034C0A4_text.o $ + build/GOWE69/obj/auto_01_8034C160_text.o $ + build/GOWE69/obj/auto_01_8034C4E4_text.o $ + build/GOWE69/obj/auto_01_8034D1A8_text.o $ + build/GOWE69/obj/auto_01_8034E180_text.o $ + build/GOWE69/obj/auto_01_8034E9F4_text.o $ + build/GOWE69/obj/auto_01_8034EB4C_text.o $ + build/GOWE69/obj/auto_01_8034EB64_text.o $ + build/GOWE69/obj/auto_01_8034ED68_text.o $ + build/GOWE69/obj/auto_01_8034F154_text.o $ + build/GOWE69/obj/auto_01_8034F21C_text.o $ + build/GOWE69/obj/auto_01_80351AC0_text.o $ + build/GOWE69/obj/auto_01_8035305C_text.o $ + build/GOWE69/obj/auto_01_80353564_text.o $ + build/GOWE69/obj/auto_01_80353734_text.o $ + build/GOWE69/obj/auto_01_803537FC_text.o $ + build/GOWE69/obj/auto_01_80354FF0_text.o $ + build/GOWE69/obj/auto_01_803556B0_text.o $ + build/GOWE69/obj/auto_01_80355CA8_text.o $ + build/GOWE69/obj/auto_01_8035645C_text.o $ + build/GOWE69/obj/auto_01_80356920_text.o $ + build/GOWE69/obj/auto_01_80356BB0_text.o $ + build/GOWE69/obj/auto_01_80357864_text.o $ + build/GOWE69/obj/auto_01_80357928_text.o $ + build/GOWE69/obj/auto_01_8035A830_text.o $ + build/GOWE69/obj/auto_01_8035A9B0_text.o $ + build/GOWE69/obj/auto_01_8035AA7C_text.o $ + build/GOWE69/obj/auto_01_8035D4A0_text.o $ + build/GOWE69/obj/auto_01_8035D6BC_text.o $ + build/GOWE69/obj/auto_01_8035DCA4_text.o $ + build/GOWE69/obj/auto_01_8035DFE4_text.o $ + build/GOWE69/obj/auto_01_8035E05C_text.o $ + build/GOWE69/obj/auto_01_8035E650_text.o $ + build/GOWE69/obj/auto_01_8035E6E8_text.o $ + build/GOWE69/obj/auto_01_8035E9CC_text.o $ + build/GOWE69/obj/auto_01_8035EA50_text.o $ + build/GOWE69/obj/auto_01_8035F650_text.o $ + build/GOWE69/obj/auto_01_8035F8E8_text.o $ + build/GOWE69/obj/auto_01_8035F95C_text.o $ + build/GOWE69/obj/auto_01_8035F974_text.o $ + build/GOWE69/obj/auto_01_8035FA0C_text.o $ + build/GOWE69/obj/auto_01_8035FBA0_text.o $ + build/GOWE69/obj/auto_01_8035FBFC_text.o $ + build/GOWE69/obj/auto_01_8035FC28_text.o $ + build/GOWE69/obj/auto_01_8035FD34_text.o $ + build/GOWE69/obj/auto_01_8035FDEC_text.o $ + build/GOWE69/obj/auto_01_8035FEF0_text.o $ + build/GOWE69/obj/auto_01_8035FF58_text.o $ + build/GOWE69/obj/auto_01_80360150_text.o $ + build/GOWE69/obj/auto_01_80360198_text.o $ + build/GOWE69/obj/auto_01_803601D8_text.o $ + build/GOWE69/obj/auto_01_80360760_text.o $ + build/GOWE69/obj/auto_01_8036080C_text.o $ + build/GOWE69/obj/auto_01_803608A0_text.o $ + build/GOWE69/obj/auto_01_80361480_text.o $ + build/GOWE69/obj/auto_01_803614D8_text.o $ + build/GOWE69/obj/auto_01_80361510_text.o $ + build/GOWE69/obj/auto_01_8036164C_text.o $ + build/GOWE69/obj/auto_01_80361708_text.o $ + build/GOWE69/obj/auto_01_8036178C_text.o $ + build/GOWE69/obj/auto_01_80361B6C_text.o $ + build/GOWE69/obj/auto_01_80361BEC_text.o $ + build/GOWE69/obj/auto_01_80362E80_text.o $ + build/GOWE69/obj/auto_01_80363134_text.o $ + build/GOWE69/obj/auto_01_8036340C_text.o $ + build/GOWE69/obj/auto_01_8036344C_text.o $ + build/GOWE69/obj/auto_01_80363548_text.o $ + build/GOWE69/obj/auto_01_803635C0_text.o $ + build/GOWE69/obj/auto_01_80363618_text.o $ + build/GOWE69/obj/auto_01_80363668_text.o $ + build/GOWE69/obj/auto_01_803636C8_text.o $ + build/GOWE69/obj/auto_01_80363754_text.o $ + build/GOWE69/obj/auto_01_803637B0_text.o $ + build/GOWE69/obj/auto_01_803637DC_text.o $ + build/GOWE69/obj/auto_01_80363808_text.o $ + build/GOWE69/obj/auto_01_80363858_text.o $ + build/GOWE69/obj/auto_01_80363A6C_text.o $ + build/GOWE69/obj/auto_01_80363AB8_text.o $ + build/GOWE69/obj/auto_01_80363B98_text.o $ + build/GOWE69/obj/auto_01_80363C04_text.o $ + build/GOWE69/obj/auto_01_80363E50_text.o $ + build/GOWE69/obj/auto_01_80364790_text.o $ + build/GOWE69/obj/auto_01_80364E14_text.o $ + build/GOWE69/obj/auto_01_80364F54_text.o $ + build/GOWE69/obj/auto_01_80364FD0_text.o $ + build/GOWE69/obj/auto_01_803658F4_text.o $ + build/GOWE69/obj/auto_01_8036598C_text.o $ + build/GOWE69/obj/auto_01_80366030_text.o $ + build/GOWE69/obj/auto_01_803660C8_text.o $ + build/GOWE69/obj/auto_01_803662C4_text.o $ + build/GOWE69/obj/auto_01_8036641C_text.o $ + build/GOWE69/obj/auto_01_80366C64_text.o $ + build/GOWE69/obj/auto_01_80369AF4_text.o $ + build/GOWE69/obj/auto_01_8036A948_text.o $ + build/GOWE69/obj/auto_01_8036A9CC_text.o $ + build/GOWE69/obj/auto_01_8036BB84_text.o $ + build/GOWE69/obj/auto_01_8036BC0C_text.o $ + build/GOWE69/obj/auto_01_8036BC54_text.o $ + build/GOWE69/obj/auto_01_8036BD24_text.o $ + build/GOWE69/obj/auto_01_8036BE70_text.o $ + build/GOWE69/obj/auto_01_8036BF2C_text.o $ + build/GOWE69/obj/auto_01_8036BF68_text.o $ + build/GOWE69/obj/auto_01_8036C8B4_text.o $ + build/GOWE69/obj/auto_01_8036C90C_text.o $ + build/GOWE69/obj/auto_01_8036D010_text.o $ + build/GOWE69/obj/auto_01_8036D170_text.o $ + build/GOWE69/obj/auto_01_8036D1D8_text.o $ + build/GOWE69/obj/auto_01_8036D248_text.o $ + build/GOWE69/obj/auto_01_8036D268_text.o $ + build/GOWE69/obj/auto_01_8036D2E8_text.o $ + build/GOWE69/obj/auto_01_8036D3D4_text.o $ + build/GOWE69/obj/auto_01_8036D53C_text.o $ + build/GOWE69/obj/auto_01_8036D668_text.o $ + build/GOWE69/obj/auto_01_8036D688_text.o $ + build/GOWE69/obj/auto_01_8036D720_text.o $ + build/GOWE69/obj/auto_01_8036D7B4_text.o $ + build/GOWE69/obj/auto_01_8036D7D8_text.o $ + build/GOWE69/obj/auto_01_8036D7EC_text.o $ + build/GOWE69/obj/auto_01_8036DD08_text.o $ + build/GOWE69/obj/auto_01_8036DD90_text.o $ + build/GOWE69/obj/auto_01_8036DE54_text.o $ + build/GOWE69/obj/auto_01_8036DEE8_text.o $ + build/GOWE69/obj/auto_01_8036DF44_text.o $ + build/GOWE69/obj/auto_01_8036DF5C_text.o $ + build/GOWE69/obj/auto_01_8036DF6C_text.o $ + build/GOWE69/obj/auto_01_8036E48C_text.o $ + build/GOWE69/obj/auto_01_8036E5A4_text.o $ + build/GOWE69/obj/auto_01_8036E730_text.o $ + build/GOWE69/obj/auto_01_8036EA40_text.o $ + build/GOWE69/obj/auto_01_8036EAB8_text.o $ + build/GOWE69/obj/auto_01_8036EC10_text.o $ + build/GOWE69/obj/auto_01_8036EDBC_text.o $ + build/GOWE69/obj/auto_01_8036F164_text.o $ + build/GOWE69/obj/auto_01_8036F250_text.o $ + build/GOWE69/obj/auto_01_8036F408_text.o $ + build/GOWE69/obj/auto_01_8036F560_text.o $ + build/GOWE69/obj/auto_01_8036F6BC_text.o $ + build/GOWE69/obj/auto_01_8036FA0C_text.o $ + build/GOWE69/obj/auto_01_8036FD48_text.o $ + build/GOWE69/obj/auto_01_8036FEC4_text.o $ + build/GOWE69/obj/auto_01_8036FF58_text.o $ + build/GOWE69/obj/auto_01_8036FFA0_text.o $ + build/GOWE69/obj/auto_01_80370000_text.o $ + build/GOWE69/obj/auto_01_80370048_text.o $ + build/GOWE69/obj/auto_01_80370078_text.o $ + build/GOWE69/obj/auto_01_80370198_text.o $ + build/GOWE69/obj/auto_01_803702A0_text.o $ + build/GOWE69/obj/auto_01_80370464_text.o $ + build/GOWE69/obj/auto_01_80370640_text.o $ + build/GOWE69/obj/auto_01_80370954_text.o $ + build/GOWE69/obj/auto_01_80370AF8_text.o $ + build/GOWE69/obj/auto_01_80370C50_text.o $ + build/GOWE69/obj/auto_01_80370FE4_text.o $ + build/GOWE69/obj/auto_01_80371258_text.o $ + build/GOWE69/obj/auto_01_80371460_text.o $ + build/GOWE69/obj/auto_01_80371494_text.o $ + build/GOWE69/obj/auto_01_80371524_text.o $ + build/GOWE69/obj/auto_01_80371A48_text.o $ + build/GOWE69/obj/auto_01_80372D10_text.o $ + build/GOWE69/obj/Packages/snd/source/library/cmn/SNDI_findprime.o $ + build/GOWE69/obj/auto_01_80372ED0_text.o $ + build/GOWE69/obj/auto_01_803748D4_text.o $ + build/GOWE69/obj/auto_01_803752AC_text.o $ + build/GOWE69/obj/auto_01_80375BC8_text.o $ + build/GOWE69/obj/auto_01_80375DB8_text.o $ + build/GOWE69/obj/auto_01_8037614C_text.o $ + build/GOWE69/obj/auto_01_803767F0_text.o $ + build/GOWE69/obj/auto_01_803788F4_text.o $ + build/GOWE69/obj/auto_01_8037929C_text.o $ + build/GOWE69/obj/auto_01_8037A2B8_text.o $ + build/GOWE69/obj/auto_01_8037C0F8_text.o $ + build/GOWE69/obj/auto_01_8037D164_text.o $ + build/GOWE69/obj/auto_01_8037D26C_text.o $ + build/GOWE69/obj/auto_01_8037D988_text.o $ + build/GOWE69/obj/auto_01_8037E814_text.o $ + build/GOWE69/obj/auto_01_8037EAC0_text.o $ + build/GOWE69/obj/auto_01_8038095C_text.o $ + build/GOWE69/obj/auto_01_80380D30_text.o $ + build/GOWE69/obj/auto_01_80380F14_text.o $ + build/GOWE69/obj/auto_01_803813F0_text.o $ + build/GOWE69/obj/auto_01_803814A8_text.o $ + build/GOWE69/obj/auto_01_803814E0_text.o $ + build/GOWE69/obj/auto_01_803814EC_text.o $ + build/GOWE69/obj/auto_01_80381654_text.o $ + build/GOWE69/obj/auto_01_80381874_text.o $ + build/GOWE69/obj/auto_01_80381A94_text.o $ + build/GOWE69/obj/auto_01_8038450C_text.o $ + build/GOWE69/obj/auto_01_80384630_text.o $ + build/GOWE69/obj/auto_01_80385034_text.o $ + build/GOWE69/obj/auto_01_8038510C_text.o $ + build/GOWE69/obj/auto_01_803852B8_text.o $ + build/GOWE69/obj/auto_01_80385D48_text.o $ + build/GOWE69/obj/auto_01_80386254_text.o $ + build/GOWE69/obj/auto_01_80386308_text.o $ + build/GOWE69/obj/auto_01_803866CC_text.o $ + build/GOWE69/obj/auto_01_80386898_text.o $ + build/GOWE69/obj/auto_01_8038693C_text.o $ + build/GOWE69/obj/auto_01_80386B38_text.o $ + build/GOWE69/obj/auto_01_80386B9C_text.o $ + build/GOWE69/obj/auto_01_80386CB4_text.o $ + build/GOWE69/obj/auto_01_80386CF4_text.o $ + build/GOWE69/obj/auto_01_80386D7C_text.o $ + build/GOWE69/obj/auto_01_80387A74_text.o $ + build/GOWE69/obj/auto_01_80387B90_text.o $ + build/GOWE69/obj/auto_01_80387C30_text.o $ + build/GOWE69/obj/auto_01_80387D88_text.o $ + build/GOWE69/obj/auto_01_80388450_text.o $ + build/GOWE69/obj/auto_01_80388B04_text.o $ + build/GOWE69/obj/auto_01_80388BC0_text.o $ + build/GOWE69/obj/auto_01_8038A5A0_text.o $ + build/GOWE69/obj/auto_01_8038D088_text.o $ + build/GOWE69/obj/auto_01_8038D1B8_text.o $ + build/GOWE69/obj/auto_01_8038DA18_text.o $ + build/GOWE69/obj/auto_01_8038FD10_text.o $ + build/GOWE69/obj/auto_01_80391570_text.o $ + build/GOWE69/obj/auto_01_80392910_text.o $ + build/GOWE69/obj/auto_01_80393AB8_text.o $ + build/GOWE69/obj/auto_01_80398988_text.o $ + build/GOWE69/obj/auto_01_80398D08_text.o $ + build/GOWE69/obj/auto_01_80398E40_text.o $ + build/GOWE69/obj/auto_01_80399038_text.o $ + build/GOWE69/obj/auto_01_8039917C_text.o $ + build/GOWE69/obj/auto_01_80399560_text.o $ + build/GOWE69/obj/auto_01_80399588_text.o $ + build/GOWE69/obj/auto_01_8039976C_text.o $ + build/GOWE69/obj/auto_01_8039989C_text.o $ + build/GOWE69/obj/auto_01_803998C8_text.o $ + build/GOWE69/obj/auto_01_80399B28_text.o $ + build/GOWE69/obj/auto_01_80399C4C_text.o $ + build/GOWE69/obj/auto_01_80399D18_text.o $ + build/GOWE69/obj/auto_01_80399EBC_text.o $ + build/GOWE69/obj/auto_01_80399FD8_text.o $ + build/GOWE69/obj/auto_01_8039A3C8_text.o $ + build/GOWE69/obj/auto_01_8039AA1C_text.o $ + build/GOWE69/obj/auto_01_8039AF04_text.o $ + build/GOWE69/obj/auto_01_8039AF70_text.o $ + build/GOWE69/obj/auto_01_8039AFD8_text.o $ + build/GOWE69/obj/auto_01_8039B238_text.o $ + build/GOWE69/obj/auto_01_8039BB58_text.o $ + build/GOWE69/obj/auto_01_8039BD38_text.o $ + build/GOWE69/obj/auto_01_8039BD54_text.o $ + build/GOWE69/obj/auto_01_8039BD88_text.o $ + build/GOWE69/obj/auto_01_8039BE20_text.o $ + build/GOWE69/obj/auto_01_8039BE6C_text.o $ + build/GOWE69/obj/auto_01_8039BED4_text.o $ + build/GOWE69/obj/auto_01_8039BF2C_text.o $ + build/GOWE69/obj/auto_01_8039BFEC_text.o $ + build/GOWE69/obj/auto_01_8039C268_text.o $ + build/GOWE69/obj/auto_01_8039C348_text.o $ + build/GOWE69/obj/auto_01_8039C43C_text.o $ + build/GOWE69/obj/auto_01_8039C4E8_text.o $ + build/GOWE69/obj/auto_01_8039C84C_text.o $ + build/GOWE69/obj/auto_01_8039D0D4_text.o $ + build/GOWE69/obj/auto_01_803A2DD8_text.o $ + build/GOWE69/obj/Packages/snd/source/library/cmn/sbpatinf.o $ + build/GOWE69/obj/Packages/snd/source/library/cmn/sgetpvol.o $ + build/GOWE69/obj/Packages/snd/source/library/cmn/sstgetpv.o $ + build/GOWE69/obj/auto_05_8041516C_rodata.o $ + build/GOWE69/obj/Packages/snd/source/library/cmn/stimerem.o $ + build/GOWE69/obj/auto_01_803A3398_text.o build/GOWE69/obj/LibSN/crt0.o $ + build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o $ + build/GOWE69/obj/auto_00_8000348C_init.o | build/compilers $ + build/GOWE69/ldscript.lcf || post-compile + ldflags = $ldflags + + +build post-link: phony || post-compile +build build/GOWE69/main.dol: elf2dol build/GOWE69/main.elf | $ + build/tools/dtk || post-link +# Generate REL(s) +rule makerel + command = build/tools/dtk rel make -w -c $config $names @$rspfile + description = REL + rspfile = $rspfile + rspfile_content = $in_newline +build post-build: phony || post-link +# Build all source files +build all_source: phony build/GOWE69/src/Speed/Indep/SourceLists/zAI.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zAnim.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zBWare.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zCamera.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zDebug.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zFe.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zFe2.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zFEng.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zLua.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zMain.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zMisc.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zMission.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zRender.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zSim.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zTrack.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zWorld.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zOnline.o $ + build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.o $ + build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.o $ + build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.o $ + build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.o $ + build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.o $ + build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.o $ + build/GOWE69/src/LibSN/crt0.o $ + build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o + +# Check hash +rule check + command = build/tools/dtk shasum -c $in -o $out + description = CHECK $in +build build/GOWE69/ok: check config/GOWE69/build.sha1 | build/tools/dtk $ + build/GOWE69/main.dol || post-build + +# Calculate progress +rule progress + command = $python configure.py $configure_args progress + description = PROGRESS +build progress: progress | configure.py tools/project.py $ + build/GOWE69/report.json build/GOWE69/ok || post-build +# Generate progress report +rule report + command = build/tools/objdiff-cli report generate $objdiff_report_args -o $ + $out + description = REPORT +build build/GOWE69/report.json: report | build/tools/objdiff-cli $ + objdiff.json all_source || post-build +# Phony edge that will always be considered dirty by ninja. +# This can be used as an implicit to a target that should always be rerun, +# ignoring file modified times. +build always: phony + +# Create a baseline progress report for later match regression testing +build build/GOWE69/baseline.json: report | build/tools/objdiff-cli $ + all_source always || post-build +build baseline: phony build/GOWE69/baseline.json +# Check for any match regressions against the baseline +# Will fail if no baseline has been created +rule report_changes + command = build/tools/objdiff-cli report changes --format json-pretty $ + build/GOWE69/baseline.json $in -o $out + description = CHANGES +build build/GOWE69/report_changes.json: report_changes $ + build/GOWE69/report.json | build/tools/objdiff-cli always +rule changes_fmt + command = $python tools/changes_fmt.py $args $in + description = CHANGESFMT +build changes: changes_fmt build/GOWE69/report_changes.json | $ + tools/changes_fmt.py +build changes_all: changes_fmt build/GOWE69/report_changes.json | $ + tools/changes_fmt.py + args = --all +rule changes_md + command = $python tools/changes_fmt.py $in -o $out + description = CHANGESFMT $out +build build/GOWE69/regressions.md: changes_md $ + build/GOWE69/report_changes.json | tools/changes_fmt.py + +# Check for mismatching symbols +rule dol_diff + command = build/tools/dtk -L error dol diff $in + description = DIFF build/GOWE69/main.elf +build dol_diff: dol_diff config/GOWE69/config.yml build/GOWE69/main.elf +build diff: phony dol_diff + +# Apply symbols from linked ELF +rule dol_apply + command = build/tools/dtk dol apply $in + description = APPLY build/GOWE69/main.elf +build dol_apply: dol_apply config/GOWE69/config.yml build/GOWE69/main.elf | $ + build/GOWE69/ok +build apply: phony dol_apply + +# Split DOL into relocatable objects +rule split + command = build/tools/dtk dol split $in $out_dir + description = SPLIT $in + depfile = $out_dir/dep + deps = gcc +build build/GOWE69/config.json: split config/GOWE69/config.yml | $ + build/tools/dtk + out_dir = build/GOWE69 + +# Reconfigure on change +rule configure + command = $python configure.py $configure_args + description = RUN configure.py + generator = 1 +build build.ninja objdiff.json: configure | build/GOWE69/config.json $ + configure.py tools/project.py tools/ninja_syntax.py + +# Default rule +default progress diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index ff7151d62..2e62f0405 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -188,17 +188,11 @@ class Database { const TypeDesc &GetTypeDesc(Type t) const; void DumpContents(Key classFilter) const; - static Database &Get() { - return *sThis; - } + static Database &Get(); - void operator delete(void *ptr, std::size_t bytes) { - Free(ptr, bytes, "Attrib::Database"); - } + void operator delete(void *ptr, unsigned int bytes); - bool IsInitialized() { - return sThis != nullptr; - } + static bool IsInitialized(); friend class DatabasePrivate; friend class DatabaseExportPolicy; @@ -212,6 +206,18 @@ class Database { DatabasePrivate &mPrivates; // offset 0x0, size 0x4 }; +inline Database &Database::Get() { + return *sThis; +} + +inline void Database::operator delete(void *ptr, unsigned int bytes) { + Free(ptr, bytes, "Attrib::Database"); +} + +inline bool Database::IsInitialized() { + return sThis != nullptr; +} + class Array { #define Flag_AlignedAt16 (1 << 15) private: // Returns the base location of this array's data @@ -408,10 +414,43 @@ class Node { Flag_IsLocatable = 1 << 6, }; + // DWARF order: GetFlag, SetFlag, operator new, operator delete, operator new(placement), + // operator delete(), Node(), Node(key,...), operator=, operator==/!=/< (Node), + // operator==/!=/< (uint), RequiresRelease..IsLocatable, Invalidate, IsValid, + // GetPointer(void*), GetPointer(const void*), GetArray, GetKey, GetType, + // GetSize, GetCount, GetTypeDesc, MaxSearch, ResetSearchLength, + // SetSearchLength, Move + + bool GetFlag(unsigned int mask) const { + return mFlags & mask; + } + + void SetFlag(unsigned int mask, bool value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + + void *operator new(std::size_t bytes) { + return AttribAlloc::Allocate(bytes, ""); + } + + void operator delete(void *ptr, std::size_t bytes) { + AttribAlloc::Free(ptr, bytes, ""); + } + void *operator new(std::size_t, void *ptr) { return ptr; } + void *operator new(std::size_t, void *ptr, unsigned int) { + return ptr; + } + + void operator delete(void *ptr) {} + Node() : mKey(0), mTypeIndex(0), mMax(0), mFlags(0), mPtr(this) {} Node(Key key, unsigned int type, void *ptr, bool ptrIsRaw, unsigned char flags, void *layoutptr) @@ -421,20 +460,22 @@ class Node { } } - void Move(Node &src) { - mKey = src.mKey; - mTypeIndex = src.mTypeIndex; - mPtr = src.mPtr; - mFlags = src.mFlags; - - src.mPtr = &src; - src.mFlags = 0; - src.mKey = 0; + const Node &operator=(const Node &rhs) { + mKey = rhs.mKey; + mPtr = rhs.mPtr; + mTypeIndex = rhs.mTypeIndex; + mMax = rhs.mMax; + mFlags = rhs.mFlags; + return *this; } - bool GetFlag(unsigned int mask) const { - return mFlags & mask; - } + bool operator==(const Node &rhs) const { return mKey == rhs.mKey; } + bool operator!=(const Node &rhs) const { return mKey != rhs.mKey; } + bool operator<(const Node &rhs) const { return mKey < rhs.mKey; } + + bool operator==(unsigned int rhs) const { return mKey == rhs; } + bool operator!=(unsigned int rhs) const { return mKey != rhs; } + bool operator<(unsigned int rhs) const { return mKey < rhs; } bool RequiresRelease() const { return GetFlag(Flag_RequiresRelease); @@ -464,6 +505,11 @@ class Node { return GetFlag(Flag_IsLocatable); } + void Invalidate() { + mPtr = this; + mKey = 0; + } + bool IsValid() const { return IsLaidOut() || mPtr != this; } @@ -478,6 +524,16 @@ class Node { } } + void *GetPointer(const void *layoutptr) const { + if (IsByValue()) { + return &mValue; + } else if (IsLaidOut()) { + return (void *)(uintptr_t(layoutptr) + uintptr_t(mPtr)); + } else { + return mPtr; + } + } + Array *GetArray(void *layoutptr) const { if (IsLaidOut()) { return (Array *)(uintptr_t(layoutptr) + uintptr_t(mArray)); @@ -486,6 +542,18 @@ class Node { } } + Key GetKey() const { + return IsValid() ? mKey : 0; + } + + unsigned int GetType() const { + return mTypeIndex; + } + + unsigned int GetSize(void *layoutptr) const { + return GetTypeDesc().GetSize(); + } + std::size_t GetCount(void *layoutptr) const { if (IsValid()) { if (IsArray()) { @@ -496,29 +564,31 @@ class Node { return 0; } - Key GetKey() const { - return IsValid() ? mKey : 0; + const TypeDesc &GetTypeDesc() const { + return Database::Get().GetIndexedTypeDesc(mTypeIndex); } std::size_t MaxSearch() const { return mMax; } - void SetSearchLength(std::size_t searchLen) { - mMax = std::max(mMax, (unsigned char)searchLen); - } - void ResetSearchLength(std::size_t searchLen) { mMax = searchLen; } - const TypeDesc &GetTypeDesc() const { - return Database::Get().GetIndexedTypeDesc(mTypeIndex); + void SetSearchLength(std::size_t searchLen) { + mMax = std::max(mMax, (unsigned char)searchLen); } - void Invalidate() { - mPtr = this; - mKey = 0; + void Move(Node &src) { + mKey = src.mKey; + mTypeIndex = src.mTypeIndex; + mPtr = src.mPtr; + mFlags = src.mFlags; + + src.mPtr = &src; + src.mFlags = 0; + src.mKey = 0; } private: @@ -537,18 +607,13 @@ class Node { // total size: 0xC class Class { public: - class TablePolicy { - public: - static std::size_t KeyIndex(std::size_t k, std::size_t tableSize, unsigned int keyShift) { - return RotateNTo32(k, keyShift) % tableSize; - } - - static std::size_t WrapIndex(std::size_t index, std::size_t tableSize, unsigned int keyShift) { - return index % tableSize; + struct TablePolicy { + static void *Alloc(std::size_t bytes) { + return TableAllocFunc(bytes); } - static std::size_t TableSize(std::size_t entries) { - return AdjustHashTableSize(entries); + static void Free(void *ptr, std::size_t bytes) { + TableFreeFunc(ptr, bytes); } static std::size_t GrowRequest(std::size_t currententries, bool collisionoverrun) { @@ -559,12 +624,16 @@ class Class { } } - static void *Alloc(std::size_t bytes) { - return TableAllocFunc(bytes); + static std::size_t TableSize(std::size_t entries) { + return AdjustHashTableSize(entries); } - static void Free(void *ptr, std::size_t bytes) { - TableFreeFunc(ptr, bytes); + static unsigned int KeyIndex(unsigned int k, unsigned int tableSize, unsigned int keyShift) { + return RotateNTo32(k, keyShift) % tableSize; + } + + static unsigned int WrapIndex(unsigned int index, unsigned int tableSize, unsigned int keyShift) { + return index % tableSize; } }; @@ -582,6 +651,7 @@ class Class { unsigned int GetNumCollections() const; Key GetFirstCollection() const; Key GetNextCollection(Key prev) const; + void Reserve(unsigned int spaceForAdditionalCollections); void SetTableBuffer(void *fixedAlloc, std::size_t bytes); unsigned int GetTableNodeSize() const; void CopyLayout(void *srcLayout, void *dstLayout) const; diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 03afcbfda..7e305653b 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -49,6 +49,11 @@ Key Class::GetNextDefinition(Key prev) const { return 0; } +ClassPrivate::CollectionHashMap::~CollectionHashMap() {} + +ClassPrivate::CollectionHashMap::CollectionHashMap(unsigned int reserve) + : VecHashMap(reserve) {} + const Collection *Class::GetCollection(Key key) const { return mPrivates.mCollections.Find(key); } @@ -77,12 +82,16 @@ Key Class::GetNextCollection(Key prev) const { return 0; } +inline void Class::Reserve(unsigned int spaceForAdditionalCollections) { + mPrivates.mCollections.Reserve(mPrivates.mCollections.Size() + spaceForAdditionalCollections); +} + void Class::SetTableBuffer(void *fixedAlloc, std::size_t bytes) { mPrivates.mCollections.SetTableBuffer(fixedAlloc, bytes); } unsigned int Class::GetTableNodeSize() const { - return 12; + return mPrivates.mCollections.GetTableNodeSize(); } void Class::Delete() const { @@ -93,6 +102,7 @@ bool Class::AddCollection(Collection *c) { return mPrivates.mCollections.Add(c->GetKey(), c); } +// NON_MATCHING: 98.6% - r6/r7 register swap in VecHashMap::FindIndex inlined into Remove bool Class::RemoveCollection(Collection *c) { return mPrivates.mCollections.Remove(c->GetKey()); } @@ -140,6 +150,4 @@ void Class::FreeLayout(void *layout) const { } } -ClassPrivate::CollectionHashMap::~CollectionHashMap() {} - }; // namespace Attrib diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp index 8fa2d693b..31ff372b2 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp @@ -56,7 +56,7 @@ Collection::Collection(const CollectionLoadData &loadData, Vault *v) : mTable(lo const unsigned int *typeList = loadData.GetTypes(); const CollectionLoadData::AttribEntry *entries = loadData.GetEntries(); - for (std::size_t i = 0; i < loadData.mNumEntries; i++) { + for (unsigned int i = 0; i < loadData.mNumEntries; i++) { const CollectionLoadData::AttribEntry &entry = entries[i]; if (entry.mNodeFlags & Node::Flag_IsByValue) { unsigned int bytes = Database::Get().GetTypeDesc(typeList[entry.mType]).GetSize(); diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index e40d7529d..e0d597bef 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -219,6 +219,7 @@ bool Database::AddClass(Class *c) { return mPrivates.mClasses.Add(c->GetKey(), c); } +// NON_MATCHING: 98.5% - r6/r7 register swap in VecHashMap::FindIndex inlined into Remove void Database::RemoveClass(const Class *c) { mPrivates.mClasses.Remove(c->GetKey()); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index 7482c187a..e8d1d7bad 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -10,10 +10,8 @@ // Credit: Brawltendo namespace Attrib { -class HashMap { - public: - class HashMapTablePolicy { - public: +struct HashMap { + struct HashMapTablePolicy { static std::size_t KeyIndex(Key k, std::size_t tableSize, unsigned int keyShift) { return RotateNTo32(k, keyShift) % tableSize; } @@ -43,10 +41,21 @@ class HashMap { } }; + void *operator new(std::size_t bytes) { + return Alloc(bytes, "Attrib::HashMap"); + } + void operator delete(void *ptr, std::size_t bytes) { Free(ptr, bytes, "Attrib::HashMap"); } + void *operator new(std::size_t, void *ptr) { + return ptr; + } + + void operator delete(void *) { + } + HashMap(std::size_t reservationSize, unsigned int keyShift, bool exactFit) : mTable(nullptr), mTableSize(0), mNumEntries(0), mWorstCollision(0), mKeyShift(keyShift) { if (reservationSize != 0) { @@ -60,6 +69,36 @@ class HashMap { } } + bool ValidIndex(unsigned int index) const { + return index < mTableSize && mTable[index].IsValid(); + } + + unsigned int GetKeyAtIndex(unsigned int index) const { + if (ValidIndex(index)) { + (void)ValidIndex(index); + return mTable[index].GetKey(); + } + return 0; + } + + Node *GetNodeAtIndex(unsigned int index) const { + if (ValidIndex(index)) { + return &mTable[index]; + } + return nullptr; + } + + bool IsArrayAtIndex(unsigned int index) const { + if (ValidIndex(index)) { + return mTable[index].IsArray(); + } + return false; + } + + std::size_t Size() const { + return mNumEntries; + } + bool Add(Key key, unsigned int type, void *ptr, bool ptrIsRaw, unsigned char flags, bool exactFit, void *layoutptr) { if (mNumEntries == mTableSize) { RebuildTable(HashMapTablePolicy::GrowRequest(mTableSize, false)); @@ -80,41 +119,25 @@ class HashMap { } } - void RebuildTable(std::size_t requestedCount) { - if (requestedCount == 0) { - return; - } - std::size_t tableSize = HashMapTablePolicy::TableSize(requestedCount); - Node *oldTable = mTable; - std::size_t oldSize = mTableSize; - mTableSize = tableSize; - mNumEntries = 0; - mWorstCollision = 0; - mTable = new (HashMapTablePolicy::Alloc(mTableSize * sizeof(Node))) Node(); - for (int i = 1; i < mTableSize; i++) { - new (&mTable[i]) Node(); + void *Remove(Node *node, void *layoutptr, bool maintainTableInvariant) { + if (node->IsValid()) { + // useless but needed to match } - if (oldTable) { - for (int i = 0; i < oldSize; i++) { - if (oldTable[i].IsValid()) { - oldTable[i].ResetSearchLength(0); - Transfer(oldTable[i]); - } + Key key = node->GetKey(); + void *result = node->GetPointer(layoutptr); + node->Invalidate(); + mNumEntries--; + + if (maintainTableInvariant) { + std::size_t actualIndex = node - mTable; + std::size_t freedIndex = UpdateSearchLength(HashMapTablePolicy::KeyIndex(key, mTableSize, mKeyShift), actualIndex); + while (freedIndex < mTableSize) { + freedIndex = UpdateSearchLength(freedIndex, freedIndex); } - HashMapTablePolicy::Free(oldTable, oldSize * sizeof(Node)); + } else { + node->ResetSearchLength(0); } - } - - void ClearForRelease() { - mNumEntries = 0; - } - - std::size_t Size() const { - return mNumEntries; - } - - bool ValidIndex(unsigned int index) const { - return index < mTableSize && mTable[index].IsValid(); + return result; } std::size_t FindIndex(Key key) const { @@ -124,8 +147,8 @@ class HashMap { Node *table = mTable; unsigned int actualIndex = HashMapTablePolicy::KeyIndex(key, mTableSize, mKeyShift); unsigned int searchLen = 0; - unsigned int maxSearchLen = table[actualIndex].MaxSearch(); - while (searchLen < maxSearchLen && table[actualIndex].GetKey() != key) { + unsigned int maxSearchlen = table[actualIndex].MaxSearch(); + while (searchLen < maxSearchlen && table[actualIndex].GetKey() != key) { if (table[actualIndex].IsValid()) { } actualIndex = HashMapTablePolicy::WrapIndex(actualIndex + 1, mTableSize, 0); @@ -153,66 +176,79 @@ class HashMap { return index; } - Node *GetNodeAtIndex(unsigned int index) const { - // TODO - if (ValidIndex(index)) { - return &mTable[index]; + void RebuildTable(std::size_t requestedCount) { + if (requestedCount == 0) { + return; + } + std::size_t tableSize = HashMapTablePolicy::TableSize(requestedCount); + Node *oldTable = mTable; + std::size_t oldSize = mTableSize; + mTableSize = tableSize; + mNumEntries = 0; + mWorstCollision = 0; + mTable = new (HashMapTablePolicy::Alloc(mTableSize * sizeof(Node))) Node(); + for (int i = 1; i < mTableSize; i++) { + new (&mTable[i]) Node(); + } + if (oldTable) { + for (int i = 0; i < oldSize; i++) { + if (oldTable[i].IsValid()) { + oldTable[i].ResetSearchLength(0); + Transfer(oldTable[i]); + } + } + HashMapTablePolicy::Free(oldTable, oldSize * sizeof(Node)); } - return nullptr; } - unsigned int GetKeyAtIndex(unsigned int index) const { - if (ValidIndex(index)) { - (void)ValidIndex(index); - return mTable[index].GetKey(); - } - return 0; + void Reserve(std::size_t requestedCount) { + RebuildTable(requestedCount); } - void *Remove(Node *node, void *layoutptr, bool maintainTableInvariant) { - if (node->IsValid()) { - // useless but needed to match - } - Key key = node->GetKey(); - void *result = node->GetPointer(layoutptr); - node->Invalidate(); - mNumEntries--; + std::size_t Capacity() const { + return mTableSize; + } - if (maintainTableInvariant) { - std::size_t actualIndex = node - mTable; // or directly and actualIndex is used for something else? - std::size_t freedIndex = UpdateSearchLength(HashMapTablePolicy::KeyIndex(key, mTableSize, mKeyShift), actualIndex); - while (freedIndex < mTableSize) { - freedIndex = UpdateSearchLength(freedIndex, freedIndex); - } - } else { - node->ResetSearchLength(0); - } - return result; + std::size_t Count() const { + return mNumEntries; + } + + unsigned short WorstCollision() const { + return mWorstCollision; + } + + unsigned short KeyShift() const { + return mKeyShift; } // UNSOLVED - unsigned int CountSearchCacheLines(Key key, unsigned int lineSize) { + unsigned int CountSearchCacheLines(Key key, unsigned int lineSize) const { unsigned int result = 0; if (mNumEntries == 0 || key == 0) { return result; } unsigned int prevline = 0; + unsigned int currline; Node *table = mTable; unsigned int actualIndex = HashMapTablePolicy::KeyIndex(key, mTableSize, mKeyShift); unsigned int searchLen = 0; - unsigned int maxSearchLen = table[actualIndex].MaxSearch(); - unsigned int currline = (uintptr_t)&table[actualIndex] >> (lineSize & 0x3f); + unsigned int maxSearchlen = table[actualIndex].MaxSearch(); + currline = (uintptr_t)&table[actualIndex] >> (lineSize & 0x3f); if (currline != 0) { prevline = currline; + result = 1; } - while (searchLen < maxSearchLen) { + for (;;) { + if (searchLen >= maxSearchlen) { + break; + } if (table[actualIndex].GetKey() == key) { - return result; + break; } - actualIndex = HashMapTablePolicy::WrapIndex(actualIndex + 1, mTableSize, mKeyShift); - currline = (uintptr_t)&table[actualIndex] >> (lineSize & 0x3f); + actualIndex = HashMapTablePolicy::WrapIndex(actualIndex + 1, mTableSize, 0); + currline = (uintptr_t)&mTable[actualIndex] >> (lineSize & 0x3f); if (currline != prevline) { prevline = currline; result++; @@ -223,6 +259,10 @@ class HashMap { return result; } + void ClearForRelease() { + mNumEntries = 0; + } + private: void Transfer(Node &src) { std::size_t searchLen = 0; diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index d27bcd498..df5177132 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -191,26 +191,10 @@ class ClassPrivate : public Class { } // total size: 0x10 - class CollectionHashMap : public VecHashMap { - public: - CollectionHashMap(std::size_t reserve) : VecHashMap(reserve) {} + struct CollectionHashMap : public VecHashMap { + CollectionHashMap(unsigned int reserve); ~CollectionHashMap(); - - unsigned int GetNextValidIndex(unsigned int startPoint) const { - unsigned int index = startPoint + 1; - for (; index < mTableSize && !mTable[index].IsValid(); index++) { - } - return index; - } - - unsigned int GetKeyAtIndex(unsigned int index) const { - if (ValidIndex(index)) { - (void)ValidIndex(index); - return mTable[index].Key(); - } - return 0; - } }; void *operator new(std::size_t bytes) { diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 787015ed1..59e356fb6 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -8,15 +8,9 @@ #include "Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h" // total size: 0x10 -template class VecHashMap { - public: +template struct VecHashMap { // total size: 0xC or 0x10 - class Node { - public: - void *operator new(std::size_t, void *place) { - return place; - } - + struct Node { Node() : mKey(0), mPtr(reinterpret_cast(this)), mMax(0) {} Node(KeyType key, T *ptr) : mKey(key), mPtr(ptr) {} @@ -68,25 +62,7 @@ template Unk3) { - RebuildTable(Policy::GrowRequest(mTableSize, true)); + bool InternalAdd(KeyType key, T *ptr) { + if (mNumEntries == mTableSize) { + RebuildTable(Policy::GrowRequest(mTableSize, false)); } - return result; + std::size_t targetIndex = Policy::KeyIndex(key, mTableSize, 0); + std::size_t actualIndex = targetIndex; + std::size_t searchLen = 0; + std::size_t tableSize = mTableSize; + while (mTable[actualIndex].IsValid()) { + if (mTable[actualIndex].Key() == key) { + return false; + } + searchLen++; + actualIndex = Policy::WrapIndex(actualIndex + 1, tableSize, 0); + } + if (actualIndex * sizeof(Node) + (uintptr_t)mTable) { + new (&mTable[actualIndex]) Node(key, ptr); + } + mTable[targetIndex].SetSearchLength(searchLen); + if (searchLen > mWorstCollision) { + mWorstCollision = searchLen; + } + mNumEntries++; + return true; } - T *RemoveIndex(std::size_t actualIndex) { - if (!ValidIndex(actualIndex)) { - return nullptr; + void CopyFromOldTable(Node *oldTable, std::size_t oldSize, bool needFree) { + for (std::size_t i = 0; i < mTableSize; i++) { + // TODO UNSOLVED + if (i * sizeof(Node) + (uintptr_t)mTable) { + new (&mTable[i]) Node(); + } } - T *result = mTable[actualIndex].Get(); - KeyType key = mTable[actualIndex].Key(); - mTable[actualIndex].Invalidate(); - mNumEntries--; + if (!oldTable) { + return; + } + for (std::size_t i = 0; i < oldSize; i++) { + if (oldTable[i].IsValid()) { + InternalAdd(oldTable[i].Key(), oldTable[i].Get()); + } + } + if (needFree) { + Policy::Free(oldTable, oldSize * sizeof(Node)); + } + } - std::size_t freedIndex = UpdateSearchLength(Policy::KeyIndex(key, mTableSize, 0), actualIndex); - // TODO UNSOLVED making it a while loop changes the output, in AttribHashMap.h it's a while loop - for (; freedIndex < mTableSize; freedIndex = UpdateSearchLength(freedIndex, freedIndex)) { + void RebuildTable(std::size_t requestedCount) { + if (requestedCount != 0) { + requestedCount--; + do { + requestedCount++; + std::size_t tableSize = Policy::TableSize(requestedCount); + Node *oldTable = mTable; + std::size_t oldSize = mTableSize; + + mTableSize = tableSize; + mNumEntries = 0; + mWorstCollision = 0; + mTable = reinterpret_cast(Policy::Alloc(mTableSize * sizeof(Node))); + CopyFromOldTable(oldTable, oldSize, true); + } while (mWorstCollision > Unk3); } - return result; } - T *Remove(KeyType key) { - std::size_t actualIndex = FindIndex(key); - return RemoveIndex(actualIndex); + VecHashMap(std::size_t reservationSize) : mTable(nullptr), mTableSize(0), mNumEntries(0), mFixedAlloc(0), mWorstCollision(0) { + if (reservationSize != 0) { + RebuildTable(reservationSize); + } } - std::size_t FindIndex(KeyType key) const { + unsigned int FindIndex(KeyType key) const { if (mNumEntries == 0) { return mTableSize; } Node *table = mTable; - std::size_t actualIndex = Policy::KeyIndex(key, mTableSize, 0); - std::size_t searchLen = 0; - std::size_t maxSearchLen = table[actualIndex].MaxSearch(); - while (searchLen < maxSearchLen && table[actualIndex].Key() != key) { - // TODO why is there a Node::IsValid call somewhere here? + unsigned int actualIndex = Policy::KeyIndex(key, mTableSize, 0); + unsigned int searchLen = 0; + unsigned int maxSearchlen = table[actualIndex].MaxSearch(); + while (searchLen < maxSearchlen && table[actualIndex].Key() != key) { if (table[actualIndex].IsValid()) { } actualIndex = Policy::WrapIndex(actualIndex + 1, mTableSize, 0); @@ -151,7 +167,7 @@ template mWorstCollision) { - mWorstCollision = searchLen; + std::size_t GetTableNodeSize() const { + return sizeof(Node); + } + + bool Add(KeyType key, T *ptr) { + bool result = InternalAdd(key, ptr); + if (mWorstCollision > Unk3) { + RebuildTable(Policy::GrowRequest(mTableSize, true)); } - mNumEntries++; - return true; + return result; } - // TODO might this be faulty? - std::size_t UpdateSearchLength(std::size_t targetIndex, std::size_t freeIndex) { + unsigned int UpdateSearchLength(unsigned int targetIndex, unsigned int freeIndex) { if (targetIndex == freeIndex && mTable[targetIndex].MaxSearch() == 0) { targetIndex = Policy::WrapIndex(targetIndex + mTableSize - mWorstCollision, mTableSize, 0); - std::size_t distance = mWorstCollision; + unsigned int distance = mWorstCollision; while (mTable[targetIndex].MaxSearch() < distance && distance > 0) { targetIndex = Policy::WrapIndex(targetIndex + 1, mTableSize, 0); distance--; } if (distance == 0) { - return static_cast(-1); + return static_cast(-1); } } - std::size_t maxSearch = mTable[targetIndex].MaxSearch(); - std::size_t worstIndex = Policy::WrapIndex(targetIndex + maxSearch, mTableSize, 0); + unsigned int maxSearch = mTable[targetIndex].MaxSearch(); + unsigned int worstIndex = Policy::WrapIndex(targetIndex + maxSearch, mTableSize, 0); if (mTable[worstIndex].IsValid()) { Policy::KeyIndex(mTable[worstIndex].Key(), mTableSize, 0); } @@ -243,9 +247,9 @@ template mWorstCollision) { prevWorst = mWorstCollision = mTable[i].MaxSearch(); } @@ -266,49 +270,45 @@ template (Policy::Alloc(mTableSize * sizeof(Node))); - CopyFromOldTable(oldTable, oldSize, true); - } while (mWorstCollision > Unk3); + std::size_t GetNextValidIndex(std::size_t startPoint) const { + std::size_t index = startPoint + 1; + for (; index < mTableSize && !mTable[index].IsValid(); index++) { } + return index; } - // TODO private - protected: + KeyType GetKeyAtIndex(std::size_t index) const { + if (ValidIndex(index)) { + (void)ValidIndex(index); + return mTable[index].Key(); + } + return 0; + } + + private: Node *mTable; // offset 0x0, size 0x4 - std::size_t mTableSize; // offset 0x4, size 0x4 - std::size_t mNumEntries; // offset 0x8, size 0x4 + unsigned int mTableSize; // offset 0x4, size 0x4 + unsigned int mNumEntries; // offset 0x8, size 0x4 unsigned int mFixedAlloc : 1; // offset 0xC, size 0x4 unsigned int mWorstCollision : 31; // offset 0xC, size 0x4 }; From 4bb7d344c30022baf1858ae367f5995593e136ed Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 13:15:49 +0100 Subject: [PATCH 07/71] delete wierd file --- build.ninja.bak | 1999 ----------------------------------------------- 1 file changed, 1999 deletions(-) delete mode 100644 build.ninja.bak diff --git a/build.ninja.bak b/build.ninja.bak deleted file mode 100644 index 42d15171d..000000000 --- a/build.ninja.bak +++ /dev/null @@ -1,1999 +0,0 @@ -ninja_required_version = 1.3 - -# The arguments passed to configure.py, for rerunning it. -configure_args = -python = "/Users/johannberger/.pyenv/versions/3.12.1/bin/python" - -# Variables -ldflags = -T config/GOWE69/ldscript.ld -toolchain_version = ProDG/3.9.3 -objdiff_report_args = - -# Tooling -rule download_tool - command = $python tools/download_tool.py $tool $out --tag $tag - description = TOOL $out -rule decompctx - command = $python tools/decompctx.py $in -o $out -d $out.d $includes $ - $excludes $defines - description = CTX $in - depfile = $out.d - deps = gcc -build build/tools/dtk: download_tool | tools/download_tool.py - tool = dtk - tag = v1.8.3 -build build/tools/objdiff-cli: download_tool | tools/download_tool.py - tool = objdiff-cli - tag = v3.7.0 -build build/tools/sjiswrap.exe: download_tool | tools/download_tool.py - tool = sjiswrap - tag = v1.2.0 -build build/compilers: download_tool | tools/download_tool.py - tool = compilers - tag = 20251015 -build build/ppc_binutils: download_tool | tools/download_tool.py - tool = ppc_binutils - tag = 2.42-1 - -# Download all tools -build tools: phony build/tools/sjiswrap.exe wine build/compilers $ - build/ppc_binutils build/tools/objdiff-cli build/tools/dtk - -# Link ELF file -rule link - command = wine build/compilers/$toolchain_version/ngcld.exe $ldflags -o $ - $out @$out.rsp - description = LINK $out - rspfile = $out.rsp - rspfile_content = $in_newline - -# Generate DOL -rule elf2dol - command = build/tools/dtk elf2dol $in $out - description = DOL $out - -# MWCC build -rule mwcc - command = wine build/compilers/$toolchain_version/mwcceppc.exe $cflags $ - -MMD -c $in -o $basedir && $python tools/transform_dep.py $basefile.d $ - $basefile.d - description = MWCC $out - depfile = $basefile.d - deps = gcc - -# MSVC build -msvc_deps_prefix = Note: including file: -rule msvc - command = wine build/compilers/$toolchain_version/cl.exe $cflags $ - /showIncludes /Fo$out $in - description = MSVC $out - deps = msvc - -# MWCC build (with UTF-8 to Shift JIS wrapper) -rule mwcc_sjis - command = wine build/tools/sjiswrap.exe $ - build/compilers/$toolchain_version/mwcceppc.exe $cflags -MMD -c $in $ - -o $basedir && $python tools/transform_dep.py $basefile.d $basefile.d - description = MWCC $out - depfile = $basefile.d - deps = gcc - -# ProDG build -rule prodg - command = env $ - SN_NGC_PATH=/Users/johannberger/nfsmw-zattribsys/build/compilers/$toolchain_version $ - wine build/compilers/$toolchain_version/ngccc.exe $cflags -MMD -c -o $ - $out $in && $python tools/transform_dep.py $basefile.d $basefile.d - description = ProDG $out - depfile = $basefile.d - deps = gcc - -# EE-GCC build -rule ee-gcc - command = env DEPENDENCIES_OUTPUT=$basefile.d wine $ - build/compilers/$toolchain_version/bin/ee-gcc.exe $cflags -c -o $out $ - $in && $python tools/transform_dep.py $basefile.d $basefile.d - description = EE-GCC $out - depfile = $basefile.d - deps = gcc - -# Assemble asm -rule as - command = build/ppc_binutils/powerpc-eabi-as $asflags -o $out $in -MD $ - $out.d && build/tools/dtk elf fixup $out $out - description = AS $out - -# Custom project build rules (pre/post-processing) -rule hashgen - command = $python tools/hasher.py $in $out - description = HASH $out - -# Custom build steps (pre-compile) -build src/Speed/Indep/Src/Generated/Hashes/zAI.h: hashgen $ - src/Speed/Indep/SourceLists/zAI.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zAnim.h: hashgen $ - src/Speed/Indep/SourceLists/zAnim.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zAttribSys.h: hashgen $ - src/Speed/Indep/SourceLists/zAttribSys.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zBWare.h: hashgen $ - src/Speed/Indep/SourceLists/zBWare.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zCamera.h: hashgen $ - src/Speed/Indep/SourceLists/zCamera.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zComms.h: hashgen $ - src/Speed/Indep/SourceLists/zComms.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zDebug.h: hashgen $ - src/Speed/Indep/SourceLists/zDebug.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zDynamics.h: hashgen $ - src/Speed/Indep/SourceLists/zDynamics.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zEagl4Anim.h: hashgen $ - src/Speed/Indep/SourceLists/zEagl4Anim.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zEAXSound.h: hashgen $ - src/Speed/Indep/SourceLists/zEAXSound.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zEAXSound2.h: hashgen $ - src/Speed/Indep/SourceLists/zEAXSound2.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zEcstasy.h: hashgen $ - src/Speed/Indep/SourceLists/zEcstasy.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zFe.h: hashgen $ - src/Speed/Indep/SourceLists/zFe.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zFe2.h: hashgen $ - src/Speed/Indep/SourceLists/zFe2.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zFEng.h: hashgen $ - src/Speed/Indep/SourceLists/zFEng.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zFoundation.h: hashgen $ - src/Speed/Indep/SourceLists/zFoundation.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zGameModes.h: hashgen $ - src/Speed/Indep/SourceLists/zGameModes.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zGameplay.h: hashgen $ - src/Speed/Indep/SourceLists/zGameplay.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zLua.h: hashgen $ - src/Speed/Indep/SourceLists/zLua.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zMain.h: hashgen $ - src/Speed/Indep/SourceLists/zMain.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zMisc.h: hashgen $ - src/Speed/Indep/SourceLists/zMisc.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zMiscSmall.h: hashgen $ - src/Speed/Indep/SourceLists/zMiscSmall.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zMission.h: hashgen $ - src/Speed/Indep/SourceLists/zMission.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zPhysics.h: hashgen $ - src/Speed/Indep/SourceLists/zPhysics.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zPhysicsBehaviors.h: hashgen $ - src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zPlatform.h: hashgen $ - src/Speed/Indep/SourceLists/zPlatform.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zRender.h: hashgen $ - src/Speed/Indep/SourceLists/zRender.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zSim.h: hashgen $ - src/Speed/Indep/SourceLists/zSim.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zSpeech.h: hashgen $ - src/Speed/Indep/SourceLists/zSpeech.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zTrack.h: hashgen $ - src/Speed/Indep/SourceLists/zTrack.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zWorld.h: hashgen $ - src/Speed/Indep/SourceLists/zWorld.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zWorld2.h: hashgen $ - src/Speed/Indep/SourceLists/zWorld2.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zOnline.h: hashgen $ - src/Speed/Indep/SourceLists/zOnline.cpp - -build src/Speed/Indep/Src/Generated/Hashes/zFeOverlay.h: hashgen $ - src/Speed/Indep/SourceLists/zFeOverlay.cpp - -build pre-compile: phony src/Speed/Indep/Src/Generated/Hashes/zAI.h $ - src/Speed/Indep/Src/Generated/Hashes/zAnim.h $ - src/Speed/Indep/Src/Generated/Hashes/zAttribSys.h $ - src/Speed/Indep/Src/Generated/Hashes/zBWare.h $ - src/Speed/Indep/Src/Generated/Hashes/zCamera.h $ - src/Speed/Indep/Src/Generated/Hashes/zComms.h $ - src/Speed/Indep/Src/Generated/Hashes/zDebug.h $ - src/Speed/Indep/Src/Generated/Hashes/zDynamics.h $ - src/Speed/Indep/Src/Generated/Hashes/zEagl4Anim.h $ - src/Speed/Indep/Src/Generated/Hashes/zEAXSound.h $ - src/Speed/Indep/Src/Generated/Hashes/zEAXSound2.h $ - src/Speed/Indep/Src/Generated/Hashes/zEcstasy.h $ - src/Speed/Indep/Src/Generated/Hashes/zFe.h $ - src/Speed/Indep/Src/Generated/Hashes/zFe2.h $ - src/Speed/Indep/Src/Generated/Hashes/zFEng.h $ - src/Speed/Indep/Src/Generated/Hashes/zFoundation.h $ - src/Speed/Indep/Src/Generated/Hashes/zGameModes.h $ - src/Speed/Indep/Src/Generated/Hashes/zGameplay.h $ - src/Speed/Indep/Src/Generated/Hashes/zLua.h $ - src/Speed/Indep/Src/Generated/Hashes/zMain.h $ - src/Speed/Indep/Src/Generated/Hashes/zMisc.h $ - src/Speed/Indep/Src/Generated/Hashes/zMiscSmall.h $ - src/Speed/Indep/Src/Generated/Hashes/zMission.h $ - src/Speed/Indep/Src/Generated/Hashes/zPhysics.h $ - src/Speed/Indep/Src/Generated/Hashes/zPhysicsBehaviors.h $ - src/Speed/Indep/Src/Generated/Hashes/zPlatform.h $ - src/Speed/Indep/Src/Generated/Hashes/zRender.h $ - src/Speed/Indep/Src/Generated/Hashes/zSim.h $ - src/Speed/Indep/Src/Generated/Hashes/zSpeech.h $ - src/Speed/Indep/Src/Generated/Hashes/zTrack.h $ - src/Speed/Indep/Src/Generated/Hashes/zWorld.h $ - src/Speed/Indep/Src/Generated/Hashes/zWorld2.h $ - src/Speed/Indep/Src/Generated/Hashes/zOnline.h $ - src/Speed/Indep/Src/Generated/Hashes/zFeOverlay.h -# Source files -# Speed/Indep/SourceLists/zAI.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zAI.o: prodg $ - src/Speed/Indep/SourceLists/zAI.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAI -build build/GOWE69/src/Speed/Indep/SourceLists/zAI.ctx: decompctx $ - src/Speed/Indep/SourceLists/zAI.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zAnim.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zAnim.o: prodg $ - src/Speed/Indep/SourceLists/zAnim.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAnim -build build/GOWE69/src/Speed/Indep/SourceLists/zAnim.ctx: decompctx $ - src/Speed/Indep/SourceLists/zAnim.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zAttribSys.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o: prodg $ - src/Speed/Indep/SourceLists/zAttribSys.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys -build build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.ctx: decompctx $ - src/Speed/Indep/SourceLists/zAttribSys.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zBWare.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zBWare.o: prodg $ - src/Speed/Indep/SourceLists/zBWare.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zBWare -build build/GOWE69/src/Speed/Indep/SourceLists/zBWare.ctx: decompctx $ - src/Speed/Indep/SourceLists/zBWare.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zCamera.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zCamera.o: prodg $ - src/Speed/Indep/SourceLists/zCamera.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zCamera -build build/GOWE69/src/Speed/Indep/SourceLists/zCamera.ctx: decompctx $ - src/Speed/Indep/SourceLists/zCamera.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zDebug.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zDebug.o: prodg $ - src/Speed/Indep/SourceLists/zDebug.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zDebug -build build/GOWE69/src/Speed/Indep/SourceLists/zDebug.ctx: decompctx $ - src/Speed/Indep/SourceLists/zDebug.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zDynamics.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.o: prodg $ - src/Speed/Indep/SourceLists/zDynamics.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zDynamics -build build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.ctx: decompctx $ - src/Speed/Indep/SourceLists/zDynamics.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zEagl4Anim.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.o: prodg $ - src/Speed/Indep/SourceLists/zEagl4Anim.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim -build build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.ctx: decompctx $ - src/Speed/Indep/SourceLists/zEagl4Anim.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zEAXSound.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.o: prodg $ - src/Speed/Indep/SourceLists/zEAXSound.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound -build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.ctx: decompctx $ - src/Speed/Indep/SourceLists/zEAXSound.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zEAXSound2.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.o: prodg $ - src/Speed/Indep/SourceLists/zEAXSound2.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2 -build build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.ctx: decompctx $ - src/Speed/Indep/SourceLists/zEAXSound2.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zEcstasy.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.o: prodg $ - src/Speed/Indep/SourceLists/zEcstasy.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy -build build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.ctx: decompctx $ - src/Speed/Indep/SourceLists/zEcstasy.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zFe.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zFe.o: prodg $ - src/Speed/Indep/SourceLists/zFe.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFe -build build/GOWE69/src/Speed/Indep/SourceLists/zFe.ctx: decompctx $ - src/Speed/Indep/SourceLists/zFe.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zFe2.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zFe2.o: prodg $ - src/Speed/Indep/SourceLists/zFe2.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFe2 -build build/GOWE69/src/Speed/Indep/SourceLists/zFe2.ctx: decompctx $ - src/Speed/Indep/SourceLists/zFe2.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zFEng.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zFEng.o: prodg $ - src/Speed/Indep/SourceLists/zFEng.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFEng -build build/GOWE69/src/Speed/Indep/SourceLists/zFEng.ctx: decompctx $ - src/Speed/Indep/SourceLists/zFEng.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zFoundation.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.o: prodg $ - src/Speed/Indep/SourceLists/zFoundation.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFoundation -build build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.ctx: decompctx $ - src/Speed/Indep/SourceLists/zFoundation.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zGameModes.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.o: prodg $ - src/Speed/Indep/SourceLists/zGameModes.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zGameModes -build build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.ctx: decompctx $ - src/Speed/Indep/SourceLists/zGameModes.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zGameplay.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.o: prodg $ - src/Speed/Indep/SourceLists/zGameplay.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zGameplay -build build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.ctx: decompctx $ - src/Speed/Indep/SourceLists/zGameplay.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zLua.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zLua.o: prodg $ - src/Speed/Indep/SourceLists/zLua.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zLua -build build/GOWE69/src/Speed/Indep/SourceLists/zLua.ctx: decompctx $ - src/Speed/Indep/SourceLists/zLua.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zMain.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zMain.o: prodg $ - src/Speed/Indep/SourceLists/zMain.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMain -build build/GOWE69/src/Speed/Indep/SourceLists/zMain.ctx: decompctx $ - src/Speed/Indep/SourceLists/zMain.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zMisc.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zMisc.o: prodg $ - src/Speed/Indep/SourceLists/zMisc.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMisc -build build/GOWE69/src/Speed/Indep/SourceLists/zMisc.ctx: decompctx $ - src/Speed/Indep/SourceLists/zMisc.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zMiscSmall.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.o: prodg $ - src/Speed/Indep/SourceLists/zMiscSmall.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall -build build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.ctx: decompctx $ - src/Speed/Indep/SourceLists/zMiscSmall.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zMission.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zMission.o: prodg $ - src/Speed/Indep/SourceLists/zMission.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zMission -build build/GOWE69/src/Speed/Indep/SourceLists/zMission.ctx: decompctx $ - src/Speed/Indep/SourceLists/zMission.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zPhysics.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.o: prodg $ - src/Speed/Indep/SourceLists/zPhysics.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPhysics -build build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.ctx: decompctx $ - src/Speed/Indep/SourceLists/zPhysics.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zPhysicsBehaviors.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.o: prodg $ - src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors -build build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.ctx: $ - decompctx src/Speed/Indep/SourceLists/zPhysicsBehaviors.cpp | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zPlatform.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.o: prodg $ - src/Speed/Indep/SourceLists/zPlatform.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zPlatform -build build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.ctx: decompctx $ - src/Speed/Indep/SourceLists/zPlatform.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zRender.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zRender.o: prodg $ - src/Speed/Indep/SourceLists/zRender.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zRender -build build/GOWE69/src/Speed/Indep/SourceLists/zRender.ctx: decompctx $ - src/Speed/Indep/SourceLists/zRender.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zSim.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zSim.o: prodg $ - src/Speed/Indep/SourceLists/zSim.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zSim -build build/GOWE69/src/Speed/Indep/SourceLists/zSim.ctx: decompctx $ - src/Speed/Indep/SourceLists/zSim.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zSpeech.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.o: prodg $ - src/Speed/Indep/SourceLists/zSpeech.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zSpeech -build build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.ctx: decompctx $ - src/Speed/Indep/SourceLists/zSpeech.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zTrack.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zTrack.o: prodg $ - src/Speed/Indep/SourceLists/zTrack.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zTrack -build build/GOWE69/src/Speed/Indep/SourceLists/zTrack.ctx: decompctx $ - src/Speed/Indep/SourceLists/zTrack.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zWorld.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zWorld.o: prodg $ - src/Speed/Indep/SourceLists/zWorld.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zWorld -build build/GOWE69/src/Speed/Indep/SourceLists/zWorld.ctx: decompctx $ - src/Speed/Indep/SourceLists/zWorld.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zWorld2.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.o: prodg $ - src/Speed/Indep/SourceLists/zWorld2.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zWorld2 -build build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.ctx: decompctx $ - src/Speed/Indep/SourceLists/zWorld2.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zOnline.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zOnline.o: prodg $ - src/Speed/Indep/SourceLists/zOnline.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zOnline -build build/GOWE69/src/Speed/Indep/SourceLists/zOnline.ctx: decompctx $ - src/Speed/Indep/SourceLists/zOnline.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Speed/Indep/SourceLists/zFeOverlay.cpp: Game (linked False) -build build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.o: prodg $ - src/Speed/Indep/SourceLists/zFeOverlay.cpp | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c++ - basedir = build/GOWE69/src/Speed/Indep/SourceLists - basefile = build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay -build build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.ctx: decompctx $ - src/Speed/Indep/SourceLists/zFeOverlay.cpp | tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Packages/snd/source/library/cmn/SNDI_findprime.c: snd (linked False) -build build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.o: $ - prodg src/Packages/snd/source/library/cmn/SNDI_findprime.c | $ - build/compilers tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c - basedir = build/GOWE69/src/Packages/snd/source/library/cmn - basefile = build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime -build build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.ctx: $ - decompctx src/Packages/snd/source/library/cmn/SNDI_findprime.c | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Packages/snd/source/library/cmn/sbpatinf.c: snd (linked False) -build build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.o: prodg $ - src/Packages/snd/source/library/cmn/sbpatinf.c | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c - basedir = build/GOWE69/src/Packages/snd/source/library/cmn - basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf -build build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.ctx: $ - decompctx src/Packages/snd/source/library/cmn/sbpatinf.c | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Packages/snd/source/library/cmn/sgetpvol.c: snd (linked False) -build build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.o: prodg $ - src/Packages/snd/source/library/cmn/sgetpvol.c | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c - basedir = build/GOWE69/src/Packages/snd/source/library/cmn - basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol -build build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.ctx: $ - decompctx src/Packages/snd/source/library/cmn/sgetpvol.c | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Packages/snd/source/library/cmn/sstgetpv.c: snd (linked False) -build build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.o: prodg $ - src/Packages/snd/source/library/cmn/sstgetpv.c | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c - basedir = build/GOWE69/src/Packages/snd/source/library/cmn - basefile = build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv -build build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.ctx: $ - decompctx src/Packages/snd/source/library/cmn/sstgetpv.c | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# Packages/snd/source/library/cmn/stimerem.c: snd (linked False) -build build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.o: prodg $ - src/Packages/snd/source/library/cmn/stimerem.c | build/compilers $ - tools/transform_dep.py || pre-compile - toolchain_version = ProDG/3.9.3 - cflags = -O1 -gdwarf -Wno-ctor-dtor-privacy -Woverloaded-virtual -I $ - src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -DEA_PLATFORM_GAMECUBE -DEA_REGION_AMERICA $ - -DGEKKO -D_USE_MATH_DEFINES -I build/GOWE69/include -DBUILD_VERSION=0 $ - -DVERSION_GOWE69 -DNDEBUG=1 -mps-nodf -G0 -ffast-math -fforce-addr $ - -fcse-follow-jumps -fcse-skip-blocks -fforce-mem -fgcse $ - -frerun-cse-after-loop -fschedule-insns -fschedule-insns2 $ - -fexpensive-optimizations -frerun-loop-opt -fmove-all-movables $ - -DLUA_NUMBER=float -x c - basedir = build/GOWE69/src/Packages/snd/source/library/cmn - basefile = build/GOWE69/src/Packages/snd/source/library/cmn/stimerem -build build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.ctx: $ - decompctx src/Packages/snd/source/library/cmn/stimerem.c | $ - tools/decompctx.py - includes = -I src/Speed/Indep/Libs/Support/stlgc -I $ - src/Speed/GameCube/Libs/stl/STLport-4.5/stlport -I $ - src/Speed/GameCube/bWare/GameCube/bWare/GameCube/SN/include -I $ - include -I src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - src/Packages -I ./ -I src -I build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - -# LibSN/crt0.s: libsn (linked False) -build build/GOWE69/src/LibSN/crt0.o: as src/LibSN/crt0.s | $ - build/ppc_binutils build/tools/dtk build/GOWE69/include/macros.inc || $ - pre-compile - asflags = -mgekko --strip-local-absolute -I include -I $ - build/GOWE69/include --defsym BUILD_VERSION=0 - -# Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp: os -# (linked False) -build $ - build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o: $ - mwcc $ - src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp $ - | build/compilers tools/transform_dep.py || pre-compile - toolchain_version = GC/1.2.5n - cflags = -nodefaults -proc gekko -align powerpc -enum int -fp hardware $ - -Cpp_exceptions off -O4,p -inline auto -pragma "cats off" -pragma $ - "warn_notinlined off" -maxerrors 1 -nosyspath -RTTI off -fp_contract $ - on -str reuse -i include -i $ - src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -i $ - build/GOWE69/include -multibyte -DVERSION=0 -lang=c++ - basedir = build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os - basefile = $ - build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init -build $ - build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.ctx: $ - decompctx $ - src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.cpp $ - | tools/decompctx.py - includes = -I include -I $ - src/Speed/GameCube/bWare/GameCube/dolphinsdk/include -I $ - build/GOWE69/include - excludes = - defines = -D EA_PLATFORM_GAMECUBE -D EA_REGION_AMERICA -D GEKKO -D $ - _USE_MATH_DEFINES -D __SN__ -D SN_TARGET_NGC - - -build post-compile: phony || pre-compile -# Link main -build build/GOWE69/main.elf: link $ - build/GOWE69/obj/auto_10_804FFF40_sdata2.o $ - build/GOWE69/obj/auto_10_80500140_sdata2.o $ - build/GOWE69/obj/auto_10_80500240_sdata2.o $ - build/GOWE69/obj/auto_10_80500390_sdata2.o $ - build/GOWE69/obj/auto_10_805008A8_sdata2.o $ - build/GOWE69/obj/auto_10_80500938_sdata2.o $ - build/GOWE69/obj/auto_10_80500A00_sdata2.o $ - build/GOWE69/obj/auto_09_804FF8C0_sbss.o $ - build/GOWE69/obj/auto_09_804FF9B0_sbss.o $ - build/GOWE69/obj/auto_09_804FFC40_sbss.o build/GOWE69/obj/prodg_fixes.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zAI.o $ - build/GOWE69/obj/auto_08_804FEDD0_sdata.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zAnim.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zAttribSys.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zBWare.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zCamera.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zDebug.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zDynamics.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zEagl4Anim.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zEAXSound.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zEAXSound2.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zEcstasy.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zFe.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zFe2.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zFEng.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zFoundation.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zGameModes.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zGameplay.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zLua.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zMain.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zMisc.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zMiscSmall.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zMission.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zPhysics.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zPhysicsBehaviors.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zPlatform.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zRender.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zSim.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zSpeech.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zTrack.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zWorld.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zWorld2.o $ - build/GOWE69/obj/auto_05_8040FBB8_rodata.o $ - build/GOWE69/obj/auto_05_80412168_rodata.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zOnline.o $ - build/GOWE69/obj/Speed/Indep/SourceLists/zFeOverlay.o $ - build/GOWE69/obj/auto_07_804B4DE8_bss.o $ - build/GOWE69/obj/auto_07_804BA0B0_bss.o $ - build/GOWE69/obj/auto_07_804BA100_bss.o $ - build/GOWE69/obj/auto_07_804BA158_bss.o $ - build/GOWE69/obj/auto_07_804BAB60_bss.o $ - build/GOWE69/obj/auto_07_804BABF8_bss.o $ - build/GOWE69/obj/auto_07_804BAC88_bss.o $ - build/GOWE69/obj/auto_07_804BAD58_bss.o $ - build/GOWE69/obj/auto_07_804BAEA0_bss.o $ - build/GOWE69/obj/auto_07_804BAED0_bss.o $ - build/GOWE69/obj/auto_07_804BAF20_bss.o $ - build/GOWE69/obj/auto_07_804BB160_bss.o $ - build/GOWE69/obj/auto_07_804BB790_bss.o $ - build/GOWE69/obj/auto_07_804BBE28_bss.o $ - build/GOWE69/obj/auto_07_804BC0C8_bss.o $ - build/GOWE69/obj/auto_07_804DA698_bss.o $ - build/GOWE69/obj/auto_07_804DA970_bss.o $ - build/GOWE69/obj/auto_07_804DAA80_bss.o $ - build/GOWE69/obj/auto_07_804DDD80_bss.o $ - build/GOWE69/obj/auto_07_804E2800_bss.o $ - build/GOWE69/obj/auto_07_804E2840_bss.o $ - build/GOWE69/obj/auto_06_804394D0_data.o $ - build/GOWE69/obj/auto_06_8043A138_data.o $ - build/GOWE69/obj/auto_06_8043A330_data.o $ - build/GOWE69/obj/auto_06_8043A748_data.o $ - build/GOWE69/obj/auto_06_8043A978_data.o $ - build/GOWE69/obj/auto_06_8043AB50_data.o $ - build/GOWE69/obj/auto_06_8043BA70_data.o $ - build/GOWE69/obj/auto_06_8043C2E0_data.o $ - build/GOWE69/obj/auto_06_8044D548_data.o $ - build/GOWE69/obj/auto_06_8044D938_data.o $ - build/GOWE69/obj/auto_06_8044DAC0_data.o $ - build/GOWE69/obj/auto_06_8044DC70_data.o $ - build/GOWE69/obj/auto_06_8044E020_data.o $ - build/GOWE69/obj/auto_06_8044E090_data.o $ - build/GOWE69/obj/auto_06_8044E488_data.o $ - build/GOWE69/obj/auto_06_8044E4E0_data.o $ - build/GOWE69/obj/auto_06_8044E528_data.o $ - build/GOWE69/obj/auto_06_8044E5B0_data.o $ - build/GOWE69/obj/auto_06_8044E780_data.o $ - build/GOWE69/obj/auto_06_8044F030_data.o $ - build/GOWE69/obj/auto_06_8044F340_data.o $ - build/GOWE69/obj/auto_06_8044F450_data.o $ - build/GOWE69/obj/auto_06_8044F568_data.o $ - build/GOWE69/obj/auto_06_80450134_data.o $ - build/GOWE69/obj/auto_06_80452928_data.o $ - build/GOWE69/obj/auto_06_80452970_data.o $ - build/GOWE69/obj/auto_06_804563A0_data.o $ - build/GOWE69/obj/auto_06_80456420_data.o $ - build/GOWE69/obj/auto_02_803C6A3C_over.o $ - build/GOWE69/obj/auto_02_803C7338_over.o build/GOWE69/obj/asd2.o $ - build/GOWE69/obj/auto_03_803C8C20_ctors.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/PPCArch.o $ - build/GOWE69/obj/auto_01_8031F5D8_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OS.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAlarm.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAlloc.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSArena.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSAudioSystem.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSCache.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSContext.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSError.o $ - build/GOWE69/obj/auto_01_803242FC_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSInterrupt.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSLink.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSMemory.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSMutex.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/OSReboot.o $ - build/GOWE69/obj/auto_01_80326948_text.o $ - build/GOWE69/obj/auto_01_8032DACC_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/pad/PadClamp.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/pad/Pad.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/ai.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/ar/ar.o $ - build/GOWE69/obj/auto_01_80332CFC_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardMount.o $ - build/GOWE69/obj/auto_01_80334F48_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardBios.o $ - build/GOWE69/obj/auto_01_8033667C_text.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardUnlock.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardRdwr.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardBlock.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardDir.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardCheck.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/card/CardOpen.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXInit.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXFifo.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXAttr.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXMisc.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXGeometry.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXFrameBuf.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXLight.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTexture.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXBump.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTev.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXPixel.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXDisplayList.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXTransform.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/gx/GXPerf.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/exi/EXIBios.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/exi/EXIUart.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/si/SIBios.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/si/SISamplingRate.o $ - build/GOWE69/obj/auto_01_803453B0_text.o $ - build/GOWE69/obj/auto_01_80348334_text.o $ - build/GOWE69/obj/auto_01_80348640_text.o $ - build/GOWE69/obj/auto_01_80348EE4_text.o $ - build/GOWE69/obj/auto_01_8034939C_text.o $ - build/GOWE69/obj/auto_01_8034983C_text.o $ - build/GOWE69/obj/auto_01_8034A348_text.o $ - build/GOWE69/obj/auto_01_8034A390_text.o $ - build/GOWE69/obj/auto_01_8034ADB0_text.o $ - build/GOWE69/obj/auto_01_8034ADE4_text.o $ - build/GOWE69/obj/auto_01_8034B900_text.o $ - build/GOWE69/obj/auto_01_8034BAC4_text.o $ - build/GOWE69/obj/auto_01_8034C0A4_text.o $ - build/GOWE69/obj/auto_01_8034C160_text.o $ - build/GOWE69/obj/auto_01_8034C4E4_text.o $ - build/GOWE69/obj/auto_01_8034D1A8_text.o $ - build/GOWE69/obj/auto_01_8034E180_text.o $ - build/GOWE69/obj/auto_01_8034E9F4_text.o $ - build/GOWE69/obj/auto_01_8034EB4C_text.o $ - build/GOWE69/obj/auto_01_8034EB64_text.o $ - build/GOWE69/obj/auto_01_8034ED68_text.o $ - build/GOWE69/obj/auto_01_8034F154_text.o $ - build/GOWE69/obj/auto_01_8034F21C_text.o $ - build/GOWE69/obj/auto_01_80351AC0_text.o $ - build/GOWE69/obj/auto_01_8035305C_text.o $ - build/GOWE69/obj/auto_01_80353564_text.o $ - build/GOWE69/obj/auto_01_80353734_text.o $ - build/GOWE69/obj/auto_01_803537FC_text.o $ - build/GOWE69/obj/auto_01_80354FF0_text.o $ - build/GOWE69/obj/auto_01_803556B0_text.o $ - build/GOWE69/obj/auto_01_80355CA8_text.o $ - build/GOWE69/obj/auto_01_8035645C_text.o $ - build/GOWE69/obj/auto_01_80356920_text.o $ - build/GOWE69/obj/auto_01_80356BB0_text.o $ - build/GOWE69/obj/auto_01_80357864_text.o $ - build/GOWE69/obj/auto_01_80357928_text.o $ - build/GOWE69/obj/auto_01_8035A830_text.o $ - build/GOWE69/obj/auto_01_8035A9B0_text.o $ - build/GOWE69/obj/auto_01_8035AA7C_text.o $ - build/GOWE69/obj/auto_01_8035D4A0_text.o $ - build/GOWE69/obj/auto_01_8035D6BC_text.o $ - build/GOWE69/obj/auto_01_8035DCA4_text.o $ - build/GOWE69/obj/auto_01_8035DFE4_text.o $ - build/GOWE69/obj/auto_01_8035E05C_text.o $ - build/GOWE69/obj/auto_01_8035E650_text.o $ - build/GOWE69/obj/auto_01_8035E6E8_text.o $ - build/GOWE69/obj/auto_01_8035E9CC_text.o $ - build/GOWE69/obj/auto_01_8035EA50_text.o $ - build/GOWE69/obj/auto_01_8035F650_text.o $ - build/GOWE69/obj/auto_01_8035F8E8_text.o $ - build/GOWE69/obj/auto_01_8035F95C_text.o $ - build/GOWE69/obj/auto_01_8035F974_text.o $ - build/GOWE69/obj/auto_01_8035FA0C_text.o $ - build/GOWE69/obj/auto_01_8035FBA0_text.o $ - build/GOWE69/obj/auto_01_8035FBFC_text.o $ - build/GOWE69/obj/auto_01_8035FC28_text.o $ - build/GOWE69/obj/auto_01_8035FD34_text.o $ - build/GOWE69/obj/auto_01_8035FDEC_text.o $ - build/GOWE69/obj/auto_01_8035FEF0_text.o $ - build/GOWE69/obj/auto_01_8035FF58_text.o $ - build/GOWE69/obj/auto_01_80360150_text.o $ - build/GOWE69/obj/auto_01_80360198_text.o $ - build/GOWE69/obj/auto_01_803601D8_text.o $ - build/GOWE69/obj/auto_01_80360760_text.o $ - build/GOWE69/obj/auto_01_8036080C_text.o $ - build/GOWE69/obj/auto_01_803608A0_text.o $ - build/GOWE69/obj/auto_01_80361480_text.o $ - build/GOWE69/obj/auto_01_803614D8_text.o $ - build/GOWE69/obj/auto_01_80361510_text.o $ - build/GOWE69/obj/auto_01_8036164C_text.o $ - build/GOWE69/obj/auto_01_80361708_text.o $ - build/GOWE69/obj/auto_01_8036178C_text.o $ - build/GOWE69/obj/auto_01_80361B6C_text.o $ - build/GOWE69/obj/auto_01_80361BEC_text.o $ - build/GOWE69/obj/auto_01_80362E80_text.o $ - build/GOWE69/obj/auto_01_80363134_text.o $ - build/GOWE69/obj/auto_01_8036340C_text.o $ - build/GOWE69/obj/auto_01_8036344C_text.o $ - build/GOWE69/obj/auto_01_80363548_text.o $ - build/GOWE69/obj/auto_01_803635C0_text.o $ - build/GOWE69/obj/auto_01_80363618_text.o $ - build/GOWE69/obj/auto_01_80363668_text.o $ - build/GOWE69/obj/auto_01_803636C8_text.o $ - build/GOWE69/obj/auto_01_80363754_text.o $ - build/GOWE69/obj/auto_01_803637B0_text.o $ - build/GOWE69/obj/auto_01_803637DC_text.o $ - build/GOWE69/obj/auto_01_80363808_text.o $ - build/GOWE69/obj/auto_01_80363858_text.o $ - build/GOWE69/obj/auto_01_80363A6C_text.o $ - build/GOWE69/obj/auto_01_80363AB8_text.o $ - build/GOWE69/obj/auto_01_80363B98_text.o $ - build/GOWE69/obj/auto_01_80363C04_text.o $ - build/GOWE69/obj/auto_01_80363E50_text.o $ - build/GOWE69/obj/auto_01_80364790_text.o $ - build/GOWE69/obj/auto_01_80364E14_text.o $ - build/GOWE69/obj/auto_01_80364F54_text.o $ - build/GOWE69/obj/auto_01_80364FD0_text.o $ - build/GOWE69/obj/auto_01_803658F4_text.o $ - build/GOWE69/obj/auto_01_8036598C_text.o $ - build/GOWE69/obj/auto_01_80366030_text.o $ - build/GOWE69/obj/auto_01_803660C8_text.o $ - build/GOWE69/obj/auto_01_803662C4_text.o $ - build/GOWE69/obj/auto_01_8036641C_text.o $ - build/GOWE69/obj/auto_01_80366C64_text.o $ - build/GOWE69/obj/auto_01_80369AF4_text.o $ - build/GOWE69/obj/auto_01_8036A948_text.o $ - build/GOWE69/obj/auto_01_8036A9CC_text.o $ - build/GOWE69/obj/auto_01_8036BB84_text.o $ - build/GOWE69/obj/auto_01_8036BC0C_text.o $ - build/GOWE69/obj/auto_01_8036BC54_text.o $ - build/GOWE69/obj/auto_01_8036BD24_text.o $ - build/GOWE69/obj/auto_01_8036BE70_text.o $ - build/GOWE69/obj/auto_01_8036BF2C_text.o $ - build/GOWE69/obj/auto_01_8036BF68_text.o $ - build/GOWE69/obj/auto_01_8036C8B4_text.o $ - build/GOWE69/obj/auto_01_8036C90C_text.o $ - build/GOWE69/obj/auto_01_8036D010_text.o $ - build/GOWE69/obj/auto_01_8036D170_text.o $ - build/GOWE69/obj/auto_01_8036D1D8_text.o $ - build/GOWE69/obj/auto_01_8036D248_text.o $ - build/GOWE69/obj/auto_01_8036D268_text.o $ - build/GOWE69/obj/auto_01_8036D2E8_text.o $ - build/GOWE69/obj/auto_01_8036D3D4_text.o $ - build/GOWE69/obj/auto_01_8036D53C_text.o $ - build/GOWE69/obj/auto_01_8036D668_text.o $ - build/GOWE69/obj/auto_01_8036D688_text.o $ - build/GOWE69/obj/auto_01_8036D720_text.o $ - build/GOWE69/obj/auto_01_8036D7B4_text.o $ - build/GOWE69/obj/auto_01_8036D7D8_text.o $ - build/GOWE69/obj/auto_01_8036D7EC_text.o $ - build/GOWE69/obj/auto_01_8036DD08_text.o $ - build/GOWE69/obj/auto_01_8036DD90_text.o $ - build/GOWE69/obj/auto_01_8036DE54_text.o $ - build/GOWE69/obj/auto_01_8036DEE8_text.o $ - build/GOWE69/obj/auto_01_8036DF44_text.o $ - build/GOWE69/obj/auto_01_8036DF5C_text.o $ - build/GOWE69/obj/auto_01_8036DF6C_text.o $ - build/GOWE69/obj/auto_01_8036E48C_text.o $ - build/GOWE69/obj/auto_01_8036E5A4_text.o $ - build/GOWE69/obj/auto_01_8036E730_text.o $ - build/GOWE69/obj/auto_01_8036EA40_text.o $ - build/GOWE69/obj/auto_01_8036EAB8_text.o $ - build/GOWE69/obj/auto_01_8036EC10_text.o $ - build/GOWE69/obj/auto_01_8036EDBC_text.o $ - build/GOWE69/obj/auto_01_8036F164_text.o $ - build/GOWE69/obj/auto_01_8036F250_text.o $ - build/GOWE69/obj/auto_01_8036F408_text.o $ - build/GOWE69/obj/auto_01_8036F560_text.o $ - build/GOWE69/obj/auto_01_8036F6BC_text.o $ - build/GOWE69/obj/auto_01_8036FA0C_text.o $ - build/GOWE69/obj/auto_01_8036FD48_text.o $ - build/GOWE69/obj/auto_01_8036FEC4_text.o $ - build/GOWE69/obj/auto_01_8036FF58_text.o $ - build/GOWE69/obj/auto_01_8036FFA0_text.o $ - build/GOWE69/obj/auto_01_80370000_text.o $ - build/GOWE69/obj/auto_01_80370048_text.o $ - build/GOWE69/obj/auto_01_80370078_text.o $ - build/GOWE69/obj/auto_01_80370198_text.o $ - build/GOWE69/obj/auto_01_803702A0_text.o $ - build/GOWE69/obj/auto_01_80370464_text.o $ - build/GOWE69/obj/auto_01_80370640_text.o $ - build/GOWE69/obj/auto_01_80370954_text.o $ - build/GOWE69/obj/auto_01_80370AF8_text.o $ - build/GOWE69/obj/auto_01_80370C50_text.o $ - build/GOWE69/obj/auto_01_80370FE4_text.o $ - build/GOWE69/obj/auto_01_80371258_text.o $ - build/GOWE69/obj/auto_01_80371460_text.o $ - build/GOWE69/obj/auto_01_80371494_text.o $ - build/GOWE69/obj/auto_01_80371524_text.o $ - build/GOWE69/obj/auto_01_80371A48_text.o $ - build/GOWE69/obj/auto_01_80372D10_text.o $ - build/GOWE69/obj/Packages/snd/source/library/cmn/SNDI_findprime.o $ - build/GOWE69/obj/auto_01_80372ED0_text.o $ - build/GOWE69/obj/auto_01_803748D4_text.o $ - build/GOWE69/obj/auto_01_803752AC_text.o $ - build/GOWE69/obj/auto_01_80375BC8_text.o $ - build/GOWE69/obj/auto_01_80375DB8_text.o $ - build/GOWE69/obj/auto_01_8037614C_text.o $ - build/GOWE69/obj/auto_01_803767F0_text.o $ - build/GOWE69/obj/auto_01_803788F4_text.o $ - build/GOWE69/obj/auto_01_8037929C_text.o $ - build/GOWE69/obj/auto_01_8037A2B8_text.o $ - build/GOWE69/obj/auto_01_8037C0F8_text.o $ - build/GOWE69/obj/auto_01_8037D164_text.o $ - build/GOWE69/obj/auto_01_8037D26C_text.o $ - build/GOWE69/obj/auto_01_8037D988_text.o $ - build/GOWE69/obj/auto_01_8037E814_text.o $ - build/GOWE69/obj/auto_01_8037EAC0_text.o $ - build/GOWE69/obj/auto_01_8038095C_text.o $ - build/GOWE69/obj/auto_01_80380D30_text.o $ - build/GOWE69/obj/auto_01_80380F14_text.o $ - build/GOWE69/obj/auto_01_803813F0_text.o $ - build/GOWE69/obj/auto_01_803814A8_text.o $ - build/GOWE69/obj/auto_01_803814E0_text.o $ - build/GOWE69/obj/auto_01_803814EC_text.o $ - build/GOWE69/obj/auto_01_80381654_text.o $ - build/GOWE69/obj/auto_01_80381874_text.o $ - build/GOWE69/obj/auto_01_80381A94_text.o $ - build/GOWE69/obj/auto_01_8038450C_text.o $ - build/GOWE69/obj/auto_01_80384630_text.o $ - build/GOWE69/obj/auto_01_80385034_text.o $ - build/GOWE69/obj/auto_01_8038510C_text.o $ - build/GOWE69/obj/auto_01_803852B8_text.o $ - build/GOWE69/obj/auto_01_80385D48_text.o $ - build/GOWE69/obj/auto_01_80386254_text.o $ - build/GOWE69/obj/auto_01_80386308_text.o $ - build/GOWE69/obj/auto_01_803866CC_text.o $ - build/GOWE69/obj/auto_01_80386898_text.o $ - build/GOWE69/obj/auto_01_8038693C_text.o $ - build/GOWE69/obj/auto_01_80386B38_text.o $ - build/GOWE69/obj/auto_01_80386B9C_text.o $ - build/GOWE69/obj/auto_01_80386CB4_text.o $ - build/GOWE69/obj/auto_01_80386CF4_text.o $ - build/GOWE69/obj/auto_01_80386D7C_text.o $ - build/GOWE69/obj/auto_01_80387A74_text.o $ - build/GOWE69/obj/auto_01_80387B90_text.o $ - build/GOWE69/obj/auto_01_80387C30_text.o $ - build/GOWE69/obj/auto_01_80387D88_text.o $ - build/GOWE69/obj/auto_01_80388450_text.o $ - build/GOWE69/obj/auto_01_80388B04_text.o $ - build/GOWE69/obj/auto_01_80388BC0_text.o $ - build/GOWE69/obj/auto_01_8038A5A0_text.o $ - build/GOWE69/obj/auto_01_8038D088_text.o $ - build/GOWE69/obj/auto_01_8038D1B8_text.o $ - build/GOWE69/obj/auto_01_8038DA18_text.o $ - build/GOWE69/obj/auto_01_8038FD10_text.o $ - build/GOWE69/obj/auto_01_80391570_text.o $ - build/GOWE69/obj/auto_01_80392910_text.o $ - build/GOWE69/obj/auto_01_80393AB8_text.o $ - build/GOWE69/obj/auto_01_80398988_text.o $ - build/GOWE69/obj/auto_01_80398D08_text.o $ - build/GOWE69/obj/auto_01_80398E40_text.o $ - build/GOWE69/obj/auto_01_80399038_text.o $ - build/GOWE69/obj/auto_01_8039917C_text.o $ - build/GOWE69/obj/auto_01_80399560_text.o $ - build/GOWE69/obj/auto_01_80399588_text.o $ - build/GOWE69/obj/auto_01_8039976C_text.o $ - build/GOWE69/obj/auto_01_8039989C_text.o $ - build/GOWE69/obj/auto_01_803998C8_text.o $ - build/GOWE69/obj/auto_01_80399B28_text.o $ - build/GOWE69/obj/auto_01_80399C4C_text.o $ - build/GOWE69/obj/auto_01_80399D18_text.o $ - build/GOWE69/obj/auto_01_80399EBC_text.o $ - build/GOWE69/obj/auto_01_80399FD8_text.o $ - build/GOWE69/obj/auto_01_8039A3C8_text.o $ - build/GOWE69/obj/auto_01_8039AA1C_text.o $ - build/GOWE69/obj/auto_01_8039AF04_text.o $ - build/GOWE69/obj/auto_01_8039AF70_text.o $ - build/GOWE69/obj/auto_01_8039AFD8_text.o $ - build/GOWE69/obj/auto_01_8039B238_text.o $ - build/GOWE69/obj/auto_01_8039BB58_text.o $ - build/GOWE69/obj/auto_01_8039BD38_text.o $ - build/GOWE69/obj/auto_01_8039BD54_text.o $ - build/GOWE69/obj/auto_01_8039BD88_text.o $ - build/GOWE69/obj/auto_01_8039BE20_text.o $ - build/GOWE69/obj/auto_01_8039BE6C_text.o $ - build/GOWE69/obj/auto_01_8039BED4_text.o $ - build/GOWE69/obj/auto_01_8039BF2C_text.o $ - build/GOWE69/obj/auto_01_8039BFEC_text.o $ - build/GOWE69/obj/auto_01_8039C268_text.o $ - build/GOWE69/obj/auto_01_8039C348_text.o $ - build/GOWE69/obj/auto_01_8039C43C_text.o $ - build/GOWE69/obj/auto_01_8039C4E8_text.o $ - build/GOWE69/obj/auto_01_8039C84C_text.o $ - build/GOWE69/obj/auto_01_8039D0D4_text.o $ - build/GOWE69/obj/auto_01_803A2DD8_text.o $ - build/GOWE69/obj/Packages/snd/source/library/cmn/sbpatinf.o $ - build/GOWE69/obj/Packages/snd/source/library/cmn/sgetpvol.o $ - build/GOWE69/obj/Packages/snd/source/library/cmn/sstgetpv.o $ - build/GOWE69/obj/auto_05_8041516C_rodata.o $ - build/GOWE69/obj/Packages/snd/source/library/cmn/stimerem.o $ - build/GOWE69/obj/auto_01_803A3398_text.o build/GOWE69/obj/LibSN/crt0.o $ - build/GOWE69/obj/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o $ - build/GOWE69/obj/auto_00_8000348C_init.o | build/compilers $ - build/GOWE69/ldscript.lcf || post-compile - ldflags = $ldflags - - -build post-link: phony || post-compile -build build/GOWE69/main.dol: elf2dol build/GOWE69/main.elf | $ - build/tools/dtk || post-link -# Generate REL(s) -rule makerel - command = build/tools/dtk rel make -w -c $config $names @$rspfile - description = REL - rspfile = $rspfile - rspfile_content = $in_newline -build post-build: phony || post-link -# Build all source files -build all_source: phony build/GOWE69/src/Speed/Indep/SourceLists/zAI.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zAnim.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zBWare.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zCamera.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zDebug.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zDynamics.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zEagl4Anim.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zEAXSound2.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zEcstasy.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zFe.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zFe2.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zFEng.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zFoundation.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zGameModes.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zGameplay.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zLua.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zMain.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zMisc.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zMiscSmall.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zMission.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zPhysics.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zPhysicsBehaviors.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zPlatform.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zRender.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zSim.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zSpeech.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zTrack.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zWorld.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zWorld2.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zOnline.o $ - build/GOWE69/src/Speed/Indep/SourceLists/zFeOverlay.o $ - build/GOWE69/src/Packages/snd/source/library/cmn/SNDI_findprime.o $ - build/GOWE69/src/Packages/snd/source/library/cmn/sbpatinf.o $ - build/GOWE69/src/Packages/snd/source/library/cmn/sgetpvol.o $ - build/GOWE69/src/Packages/snd/source/library/cmn/sstgetpv.o $ - build/GOWE69/src/Packages/snd/source/library/cmn/stimerem.o $ - build/GOWE69/src/LibSN/crt0.o $ - build/GOWE69/src/Speed/GameCube/bWare/GameCube/dolphinsdk/src/os/__ppc_eabi_init.o - -# Check hash -rule check - command = build/tools/dtk shasum -c $in -o $out - description = CHECK $in -build build/GOWE69/ok: check config/GOWE69/build.sha1 | build/tools/dtk $ - build/GOWE69/main.dol || post-build - -# Calculate progress -rule progress - command = $python configure.py $configure_args progress - description = PROGRESS -build progress: progress | configure.py tools/project.py $ - build/GOWE69/report.json build/GOWE69/ok || post-build -# Generate progress report -rule report - command = build/tools/objdiff-cli report generate $objdiff_report_args -o $ - $out - description = REPORT -build build/GOWE69/report.json: report | build/tools/objdiff-cli $ - objdiff.json all_source || post-build -# Phony edge that will always be considered dirty by ninja. -# This can be used as an implicit to a target that should always be rerun, -# ignoring file modified times. -build always: phony - -# Create a baseline progress report for later match regression testing -build build/GOWE69/baseline.json: report | build/tools/objdiff-cli $ - all_source always || post-build -build baseline: phony build/GOWE69/baseline.json -# Check for any match regressions against the baseline -# Will fail if no baseline has been created -rule report_changes - command = build/tools/objdiff-cli report changes --format json-pretty $ - build/GOWE69/baseline.json $in -o $out - description = CHANGES -build build/GOWE69/report_changes.json: report_changes $ - build/GOWE69/report.json | build/tools/objdiff-cli always -rule changes_fmt - command = $python tools/changes_fmt.py $args $in - description = CHANGESFMT -build changes: changes_fmt build/GOWE69/report_changes.json | $ - tools/changes_fmt.py -build changes_all: changes_fmt build/GOWE69/report_changes.json | $ - tools/changes_fmt.py - args = --all -rule changes_md - command = $python tools/changes_fmt.py $in -o $out - description = CHANGESFMT $out -build build/GOWE69/regressions.md: changes_md $ - build/GOWE69/report_changes.json | tools/changes_fmt.py - -# Check for mismatching symbols -rule dol_diff - command = build/tools/dtk -L error dol diff $in - description = DIFF build/GOWE69/main.elf -build dol_diff: dol_diff config/GOWE69/config.yml build/GOWE69/main.elf -build diff: phony dol_diff - -# Apply symbols from linked ELF -rule dol_apply - command = build/tools/dtk dol apply $in - description = APPLY build/GOWE69/main.elf -build dol_apply: dol_apply config/GOWE69/config.yml build/GOWE69/main.elf | $ - build/GOWE69/ok -build apply: phony dol_apply - -# Split DOL into relocatable objects -rule split - command = build/tools/dtk dol split $in $out_dir - description = SPLIT $in - depfile = $out_dir/dep - deps = gcc -build build/GOWE69/config.json: split config/GOWE69/config.yml | $ - build/tools/dtk - out_dir = build/GOWE69 - -# Reconfigure on change -rule configure - command = $python configure.py $configure_args - description = RUN configure.py - generator = 1 -build build.ninja objdiff.json: configure | build/GOWE69/config.json $ - configure.py tools/project.py tools/ninja_syntax.py - -# Default rule -default progress From 915e47671faf840515e540e5f80e42e362618f91 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 13:53:15 +0100 Subject: [PATCH 08/71] AGENTS: document zAttribSys register swap dead end Add a Discovered Matching Patterns note for the stable r6/r7 register-allocation tie-break seen in zAttribSys RemoveCollection and RemoveClass so future matching passes do not repeat 300+ exhausted source-level experiments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c4a67f725..5f364c349 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -243,3 +243,7 @@ When an inlined allocator path must reference a specific rodata symbol, replace ### ExplicitInlineSpecialMembersForSTLElements TU: zAttribSys | Function: _STL::_Rb_tree::_M_insert If an STL node insertion path refuses to match, check whether the element type is missing explicit inline special members that the original source exposed. Adding the Dwarf-backed `operator new`, `operator delete`, placement `new`, copy constructor, and tiny accessors to `TypeDesc` made the tree node creation/insertion path match exactly. + +### RegisterAllocatorTieBreakDeadEnd +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass +If two near-matching functions differ only because the same inlined helper chain lands `mTableSize` in `r6` in the original but `r7` in the rebuild, treat it as a likely GCC 3.x register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, `VecHashMap::FindIndex` inlined through `Remove -> RemoveIndex -> UpdateSearchLength` produced a stable `lwz r6, 4(r3)` vs `lwz r7, 4(r3)` split, which then propagated into later `UpdateSearchLength` control-flow differences. This survived 300+ source experiments: loop-form changes, adding/removing temporaries, splitting/merging expressions, helper inline/outline changes, declaration-order tweaks, member type changes, access-control changes, template method reorderings, and inline vs out-of-line ctor/dtor placement. Once the diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only consider flag permutation or compiler-level investigation as a last resort. From 47dd0e1c620bfede6ab6e1914d9983d23d7ae5e5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 15:45:14 +0100 Subject: [PATCH 09/71] doc: sub agent parallel work + only use sub agents for simple tasks --- AGENTS.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fa020e417..7a64c0ddd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,15 +207,17 @@ Do not batch up multiple percentage milestones into one commit — commit as eac ## Parallel Sub-Agent Matching -When working on a translation unit with multiple non-matching functions, you are encouraged to spawn sub-agents to work on individual functions in parallel. Each sub-agent should focus on **exactly one function** — do not assign a sub-agent more than one function at a time. +When working on a translation unit with multiple non-matching functions, use sub-agents selectively for **simple, small, isolated** functions. The main agent should keep ownership of the harder matching work instead of delegating it away. Each sub-agent should focus on **exactly one function** — do not assign a sub-agent more than one function at a time. **Limit: never run more than 5 sub-agents concurrently.** Spawning too many at once causes resource contention and makes it harder to reason about progress. Guidelines: -- Spawn a sub-agent per function for functions that are independent (no shared edits to the same source lines). +- Prefer solving difficult, high-risk, or cross-cutting functions yourself. Use sub-agents only for straightforward functions with small, well-bounded edits. +- Spawn a sub-agent per function only when the functions are independent (no shared edits to the same source lines). - Each sub-agent must use `build-unit.py` for parallel-safe compilation (never plain `ninja`). -- Wait for a batch of sub-agents to finish before spawning the next batch. -- After all sub-agents in a batch complete, check the updated match percentage and commit if it improved. +- Do **not** sit idle waiting for sub-agents to finish. While they run, continue investigating or implementing other independent work in parallel. +- Before applying a sub-agent's result, re-read the touched area and make sure it still fits the current state of the TU. +- After a useful sub-agent result lands, check the updated match percentage and commit if it improved. ## Matching Philosophy From d0e8614015477d31678ab5d25d865275688ed066 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 15:59:25 +0100 Subject: [PATCH 10/71] zAttribSys: restore VecHashMap merge regression Restore the pre-merge VecHashMap64 condition form after syncing with origin/dev. The merged condition regressed zAttribSys from 99.9% to 99.6%; this puts the translation unit back at the verified 99.9% baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 9e30fdbba..59e356fb6 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -238,7 +238,10 @@ template Date: Wed, 11 Mar 2026 19:54:50 +0100 Subject: [PATCH 11/71] Update shared worktree tooling and docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 112 ++++------ .github/skills/implement/SKILL.md | 2 +- AGENTS.md | 54 +++-- README.md | 22 ++ tools/build-unit.py | 142 +++++++++--- tools/decomp-context.py | 6 +- tools/share_worktree_assets.py | 351 ++++++++++++++++++++++++++++++ 7 files changed, 564 insertions(+), 125 deletions(-) create mode 100644 tools/share_worktree_assets.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 6b0d78ce0..b82c8b223 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -5,23 +5,25 @@ description: Workflow for decompiling an entire translation unit end-to-end. # Translation Unit Execution Workflow -Your goal is to orchestrate reverse engineering, class scaffolding, and function-by-function -matching to produce C++ source that compiles to byte-identical object code against the -original retail binary. +Your goal is to decompile a full translation unit: understand the current state, +scaffold any missing classes if needed, then match the unit function by function until +the produced C++ compiles to byte-identical object code against the original retail binary. ## Overview -This skill coordinates several agent types: +This workflow combines several smaller workflows: -1. **reverse-engineer** — Update Ghidra with accurate data types for the class -2. **scaffolder** — Create header/source if the class is not yet in the project (see `.github/skills/scaffold/SKILL.md`) -3. **implementer** — Match each function one at a time until the TU is complete (see `.github/skills/implement/SKILL.md`) -4. **refiner** — Use on non-matching functions to improve the match. Applies systematic lateral strategies for stubborn mismatches (see `.github/skills/refiner/SKILL.md`). +1. **Scaffold** missing classes or headers when the TU depends on types that do not yet exist (see `.github/skills/scaffold/SKILL.md`). +2. **Implement** each missing or nonmatching function one at a time (see `.github/skills/implement/SKILL.md`). +3. **Refine** stubborn 80–99% functions after the obvious implementation has already been tried (see `.github/skills/refiner/SKILL.md`). -All non-read-only work is done **sequentially** — never spawn multiple writing agents at -the same time, as they will interfere with each other. +Work through the TU **sequentially** and keep one coherent state in the source tree. -**Avoid** doing deep dives into Ghidra or the assembly yourself — instead, rely on the agents to gather and analyze that context. Your context window is precious, so focus on high-level orchestration, monitoring progress, and providing agents the necessary information they need to do their work. +You may use sub-agents for **read-only reconnaissance only**: symbol searches, Ghidra +inspection, dump lookups, line mapping, assembly review, or summarizing the current +state of a TU. Sub-agents must **not** write or edit repository files. All scaffolding, +implementation, refactoring, and other persistent file changes must be done directly by +the main worker after reviewing the read-only findings. ## Phase 0: Establish Baseline @@ -32,13 +34,17 @@ ninja # ensure clean build ninja baseline # snapshot current match state ``` -All agents that modify code should check `ninja changes` after modifying shared headers -to verify no regressions were introduced. An empty changeset means no regressions. If -regressions appear, the shared header change must be reverted. +After modifying shared headers, check `ninja changes` to verify no regressions were +introduced. An empty changeset means no regressions. If regressions appear, the shared +header change must be reverted. ## Phase 1: Reconnaissance -Before spawning any implementation agents, understand the current state of the TU. +Before making changes, understand the current state of the TU. + +This phase is a good fit for read-only sub-agents. They can gather function lists, inspect +Ghidra output, trace line mappings, and summarize missing/nonmatching areas, but they must +not edit files or apply code changes. ### 1a. Identify the file path @@ -55,12 +61,10 @@ nonmatching, and matching functions. ## Phase 2: Scaffold (if needed) -A jump file contains many files and classes. Spawn a `scaffolder` -agent to create each class whose definition does not yet exist in the project. The scaffolder will: - -- Create the structs in the correct header files with accurate class layouts from the dwarf - -Wait for this agent to complete before proceeding. +A jump file contains many files and classes. If the TU depends on a type whose +definition does not yet exist in the project, follow the scaffold workflow in +`.github/skills/scaffold/SKILL.md` to create the needed header/source definitions +before moving on. ## Phase 3: Implement Functions @@ -68,7 +72,7 @@ Wait for this agent to complete before proceeding. After scaffolding, rebuild and re-check the function list. Use `build-unit.py` to compile to a private temp `.o` so the status check isn't -polluted by another parallel agent's compilation: +polluted by another concurrent temp build: ```sh ninja # full build to update shared state (progress, sha1) @@ -79,51 +83,40 @@ python tools/decomp-diff.py -u main/Path/To/TU -s missing -t function --base-obj ### 3c. Implement each function sequentially -For each missing or nonmatching function, spawn an `implementer` agent. Provide: - -- The class name and function name -- The TU path -- Any context from previous iterations (e.g. patterns discovered, field types clarified) -- Accumulated matching tips from previous agents (see below) +For each missing or nonmatching function, follow the implementation workflow in +`.github/skills/implement/SKILL.md`. **Important considerations:** -- **One at a time.** Never spawn multiple implementer agents concurrently. +- **One at a time.** Keep the tree in a coherent state as you work through the list. - **Balance new vs fixing.** Don't get stuck on one stubborn function — sometimes implementing the next function reveals patterns that make the previous one click. - But don't leave too many functions nonmatching, as agents may copy incorrect patterns. - **Mismatch triage:** - `@stringBase0` offset mismatches often resolve as more string literals are added - Register swaps and stack layout issues require direct intervention - Branch structure mismatches indicate wrong control flow (if/switch/loop) - **Match percentage is misleading.** The last few percent are often the hardest. - Agents may assume a 95% match is "close enough" — remind them that the goal is 100%. + Treat 95% as unfinished; the goal is 100%. ### 3d. Collect and propagate matching tips -Every implementer agent prompt should include: - -- All matching tips accumulated so far from previous agents in this session -- A request to **report any new assembly patterns or matching tips** discovered - -After each agent completes, evaluate its reported tips: +Keep notes on useful patterns you discover while working through the TU. +After each useful result, evaluate whether the pattern is generalizable: - **Generalizable patterns** (e.g. `fmuls fX, fX, fY` == `*=`) should be added to AGENTS.md's "Assembly patterns" section so all future sessions benefit. - **TU-specific patterns** (e.g. "this class uses `const char*` cast for bool array - access") should be kept in the session context and passed to subsequent agents but + access") should be kept in the session context and applied to subsequent functions but not added to AGENTS.md. ### 3f. Regression checking -Remind agents in their prompts: +After modifying any shared headers, run `ninja changes` to check for regressions. +Empty changeset = no regressions. If regressions appear, revert the shared change +and use a local workaround instead. -> After modifying any shared headers, run `ninja changes` to check for regressions. -> Empty changeset = no regressions. If regressions appear, revert the shared change -> and use a local workaround instead. - -> Use `build-unit.py` + `--base-obj` for all diff and context commands so your -> results are isolated from other agents compiling the same TU concurrently. +Use `build-unit.py` + `--base-obj` for diff and context commands when you want +results isolated from other concurrent builds of the same TU. ### 3g. Periodic reassessment @@ -154,8 +147,8 @@ python tools/decomp-diff.py -u main/Path/To/TU -s nonmatching ninja changes ``` -For any remaining nonmatching functions, make one final pass with the implementer agent, -providing all context accumulated during the session. +For any remaining nonmatching functions, make one final pass using the implementation +or refiner workflow with all context accumulated during the session. ## Phase 5: Report @@ -167,28 +160,3 @@ Summarize the session: - **Matching tips** — new assembly patterns discovered (note which are generalizable) - **Adjacent classes touched** — any scaffolding/RE done on related classes - **Recommendations** — what to tackle next, dependencies on other TUs - -## Agent Prompt Template - -When spawning implementer agents, always include these standard instructions: - -``` -Source file: src/[PathToClass].cpp -Header: include/[PathToClass].hpp -ASM: build/GOWE69/asm/[PathToClass].s - -Implement the function [ClassName]::[FunctionName] - -**Standard agent instructions:** -- Use the lookup and line-lookup skills for dwarf info. -- After modifying shared headers, run `ninja changes` to check for regressions (empty = good). -- Use `build-unit.py` + `--base-obj` for all build/diff/context/dwarf-dump commands so your - compiled output is isolated from other agents working on different TUs: - TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) - python tools/decomp-diff.py -u main/Path/To/TU -d [FunctionName] --base-obj "$TEMPOBJ" - dtk dwarf dump "$TEMPOBJ" -o /tmp/TU_check.nothpp -- Report any new general assembly patterns or matching tips you discover. - -**Matching tips from this session:** -[accumulated tips here] -``` diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 65b2ddd2a..52f30fefb 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -89,7 +89,7 @@ The game uses stlport, so you'll often encounter \_STL, but in the code it must ### Initial build -Compile to a private temp `.o` so your output isn't overwritten by other parallel agents: +Compile to a private temp `.o` so your output isn't overwritten by other concurrent builds: ```sh TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) diff --git a/AGENTS.md b/AGENTS.md index 7a64c0ddd..cdf83bc28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,19 @@ objdiff.json Generated build/diff configuration ## Agent Tooling +## Sub-Agent Usage + +Sub-agents are allowed only for **read-only exploration** tasks such as: + +- searching the codebase for symbols, call sites, or include relationships +- inspecting decomp output, assembly, DWARF, PS2 dumps, or line mappings +- gathering context from Ghidra, `lookup.py`, `decomp-diff.py`, or similar tools +- summarizing findings that help the main worker decide what to change + +Sub-agents must **not** write or edit code files, headers, configs, or other repository files. +All persistent file changes, decomp implementations, scaffolding, and follow-up fixes must be +done by the main worker after reviewing the read-only findings. + ### lookup.py — Symbol lookup from the debug dump Query structs, enums, functions, globals, and typedefs directly from the pre-extracted @@ -66,8 +79,8 @@ python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin - Mismatched args are wrapped in `{}`. Matching runs are collapsed (control with `-C ` context lines, `--no-collapse`). Left = original, right = decomp. -**Parallel-safe usage** — when multiple agents compile the same TU, pass a private `--base-obj` -so each agent diffs against its own compiled output and they never interfere: +**Parallel-safe usage** — when you compile the same TU in multiple concurrent iterations, +pass a private `--base-obj` so each diff uses its own compiled output: ```sh TEMPOBJ=$(python tools/build-unit.py -u main/Speed/Indep/SourceLists/zAnim) @@ -135,7 +148,7 @@ If it finds a match, include that header instead of redeclaring. Dump the dwarf of your own implementation of a function. **Always use the temp `.o` produced by `build-unit.py`** so the dump reflects your own -compilation and isn't overwritten by another parallel agent: +compilation and isn't overwritten by another concurrent temp build: ```sh TEMPOBJ=$(python tools/build-unit.py -u main/Speed/Indep/SourceLists/UNITNAME) @@ -151,7 +164,7 @@ dtk demangle 'AcceptScriptMsg__7CEntityF20EScriptObjectMessage9TUniqueIdR13CStat ### build-unit.py — Parallel-safe compilation Compile a single translation unit to a private temporary `.o` file that won't be -overwritten by other agents. Always prefer this over plain `ninja` when you need to +overwritten by other concurrent temp builds. Always prefer this over plain `ninja` when you need to diff or inspect your own compiled output: ```sh @@ -171,6 +184,21 @@ python tools/decomp-context.py -u main/Path/To/TU --base-obj "$TEMPOBJ" -f Fun dtk dwarf dump "$TEMPOBJ" -o /tmp/TU_check.nothpp ``` +### share_worktree_assets.py — Share stable assets across git worktrees + +Deduplicate immutable debug inputs and downloaded tool binaries across all git +worktrees while keeping per-worktree generated build files local: + +```sh +python tools/share_worktree_assets.py link --all +python tools/share_worktree_assets.py status --all +``` + +This shares extracted `orig/*` contents, `symbols/*`, root ELF / MAP files, and +downloaded tool binaries under `build/`. It does **not** share `build.ninja`, +`objdiff.json`, `compile_commands.json`, or per-worktree object outputs, so run +`python configure.py` inside each worktree after linking. + ## Code Conventions This is a **C++98** codebase compiled with ProDG (GCC under the hood). Key rules: @@ -205,24 +233,14 @@ Examples: Do not batch up multiple percentage milestones into one commit — commit as each improvement lands. -## Parallel Sub-Agent Matching - -When working on a translation unit with multiple non-matching functions, use sub-agents selectively for **simple, small, isolated** functions. The main agent should keep ownership of the harder matching work instead of delegating it away. Each sub-agent should focus on **exactly one function** — do not assign a sub-agent more than one function at a time. - -**Limit: never run more than 5 sub-agents concurrently.** Spawning too many at once causes resource contention and makes it harder to reason about progress. - -Guidelines: -- Prefer solving difficult, high-risk, or cross-cutting functions yourself. Use sub-agents only for straightforward functions with small, well-bounded edits. -- Spawn a sub-agent per function only when the functions are independent (no shared edits to the same source lines). -- Each sub-agent must use `build-unit.py` for parallel-safe compilation (never plain `ninja`). -- Do **not** sit idle waiting for sub-agents to finish. While they run, continue investigating or implementing other independent work in parallel. -- Before applying a sub-agent's result, re-read the touched area and make sure it still fits the current state of the TU. -- After a useful sub-agent result lands, check the updated match percentage and commit if it improved. - ## Matching Philosophy You should take the Ghidra decompiler output for the initial translation step, get it to compile, make sure that the dwarf of the function matches and only then look for binary matching problems in the assembly. Be aware Ghidra usually gets the order of branches incorrect in if statements (it inverts the logic and the two bodies are swapped), this needs to be fixed to achieve bytematching status. +You may use sub-agents to gather read-only context during this process, but they must not +edit files. Treat their output as analysis input for the main worker, not as a path to +delegate source changes. + The dwarf of your structs doesn't have to neccessarily match the original due to various reasons, just make sure that you copied everything correctly. Never dismiss a diff as "close enough" or "just register allocation." Every mismatched diff --git a/README.md b/README.md index 744c8c0af..b74f298fd 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,28 @@ sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' - PS2: Copy `NSF.ELF` to `./orig/SLES-53558-A124/` +- Sharing large assets across git worktrees + + If you use multiple git worktrees, you can deduplicate the large immutable inputs + and downloaded tool binaries while keeping each worktree's generated build files + separate: + + ```sh + python tools/share_worktree_assets.py link --all + ``` + + This shares the ignored debug/tool assets under the git common directory, including + extracted `orig/*` contents, `symbols/*`, root ELF / MAP files, and downloaded + tool binaries under `build/`. It intentionally does **not** share `build.ninja`, + `objdiff.json`, `compile_commands.json`, or per-worktree object outputs. + + After linking shared assets into a worktree, regenerate that worktree's local build + files with: + + ```sh + python configure.py + ``` + # Diffing Once the initial build succeeds, an `objdiff.json` should exist in the project root. diff --git a/tools/build-unit.py b/tools/build-unit.py index cdc78875b..dbfacf7b7 100644 --- a/tools/build-unit.py +++ b/tools/build-unit.py @@ -29,12 +29,15 @@ import subprocess import sys import tempfile -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple, Union script_dir = os.path.dirname(os.path.realpath(__file__)) root_dir = os.path.abspath(os.path.join(script_dir, "..")) OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") BUILD_NINJA = os.path.join(root_dir, "build.ninja") +COMPILE_COMMANDS = os.path.join(root_dir, "compile_commands.json") + +Command = Union[str, List[str]] def load_objdiff() -> Dict[str, Any]: @@ -51,8 +54,24 @@ def find_unit_source(config: Dict[str, Any], unit_name: str) -> Optional[str]: return None +def find_unit_target(config: Dict[str, Any], unit_name: str) -> Optional[str]: + """Return the build target path for a unit from objdiff.json, or None.""" + for unit in config.get("units", []): + if unit["name"] == unit_name: + target = unit.get("base_path") or unit.get("target_path") + return str(target) if target else None + return None + + def get_compdb() -> Optional[List[Dict[str, Any]]]: - """Run `ninja -t compdb` and return the parsed compilation database.""" + """Load compile_commands.json, falling back to `ninja -t compdb` if needed.""" + if os.path.exists(COMPILE_COMMANDS): + try: + with open(COMPILE_COMMANDS) as f: + return json.load(f) + except json.JSONDecodeError as e: + print(f"Failed to parse compile_commands.json: {e}", file=sys.stderr) + result = subprocess.run( ["ninja", "-t", "compdb"], capture_output=True, @@ -71,27 +90,72 @@ def get_compdb() -> Optional[List[Dict[str, Any]]]: return None +def get_build_command(target_path: str) -> Optional[str]: + """Return the final ninja command used to build target_path.""" + result = subprocess.run( + ["ninja", "-t", "commands", target_path], + capture_output=True, + cwd=root_dir, + ) + if result.returncode != 0: + print( + f"ninja -t commands failed:\n{result.stderr.decode(errors='replace')}", + file=sys.stderr, + ) + return None + + commands = [line.strip() for line in result.stdout.decode().splitlines() if line.strip()] + return commands[-1] if commands else None + + def find_entry( compdb: List[Dict[str, Any]], source_path: str ) -> Optional[Dict[str, Any]]: - """Find the compdb entry whose 'file' matches source_path.""" + """Find the compdb entry whose 'file' matches source_path. + + Prefers entries whose output is a .o file (actual compiler invocations) + over auxiliary entries (e.g. hash generation). + """ abs_source = os.path.normcase(os.path.abspath(os.path.join(root_dir, source_path))) + candidates = [] for entry in compdb: file_val = entry.get("file", "") if not os.path.isabs(file_val): entry_dir = entry.get("directory", root_dir) file_val = os.path.abspath(os.path.join(entry_dir, file_val)) if os.path.normcase(file_val) == abs_source: + candidates.append(entry) + for entry in candidates: + out = entry.get("output", "") + if out.endswith(".o") or out.endswith(".obj"): return entry - return None + return candidates[0] if candidates else None + + +def get_command(entry: Dict[str, Any]) -> Command: + command = entry.get("command") + if isinstance(command, str): + return command + arguments = entry.get("arguments") + if isinstance(arguments, list): + return arguments[:] -def strip_transform_dep(command: str) -> str: + print( + "Compilation entry is missing both 'command' and 'arguments'", + file=sys.stderr, + ) + sys.exit(1) + + +def strip_transform_dep(command: Command) -> Command: """Remove the `&& python transform_dep.py ...` step from a compile command. The dependency file transformation is only needed for incremental ninja builds; it is safe to skip for one-off temp compilations. """ + if isinstance(command, list): + return command return re.sub( r"\s*&&\s*\S*python3?\S*\s+\S*transform_dep\.py\s+\S+\s+\S+", "", @@ -99,43 +163,58 @@ def strip_transform_dep(command: str) -> str: ) -def redirect_output(command: str, source_path: str, new_output: str) -> str: +def find_output_argument(command: Command) -> Optional[Tuple[int, str]]: + if isinstance(command, list): + for i in range(len(command) - 1): + if command[i] == "-o": + return i + 1, command[i + 1] + return None + + m = re.search(r"(? Command: """Replace the compiler output path in command with new_output. Handles two styles: - Direct file output (-o path/to/file.o): ngccc/ProDG, MSVC, EE-GCC - Directory output (-o path/to/dir): mwcc (MWCC outputs to a dir) """ - m = re.search(r"(?/.o automatically. - new_basedir = os.path.dirname(new_output) - return command[: m.start(1)] + new_basedir + command[m.end(1) :] + replacement = os.path.dirname(new_output) + if isinstance(command, list): + new_command = command[:] + new_command[index] = replacement + return new_command -def actual_output_path(command: str, source_path: str, new_output: str) -> str: + return command[:index] + replacement + command[index + len(o_arg) :] + + +def actual_output_path(command: Command, source_path: str, new_output: str) -> str: """Return the path where the compiled .o actually lands. For direct-file compilers this is new_output. For directory-output compilers it is /.o. """ - m = re.search(r"(? str: config = load_objdiff() source_path = find_unit_source(config, unit_name) + target_path = find_unit_target(config, unit_name) if not source_path: print( f"No source_path found for unit '{unit_name}' in objdiff.json.\n" @@ -158,6 +238,12 @@ def compile_unit(unit_name: str, output_path: str) -> str: file=sys.stderr, ) sys.exit(1) + if not target_path: + print( + f"No target_path found for unit '{unit_name}' in objdiff.json.", + file=sys.stderr, + ) + sys.exit(1) if not os.path.exists(BUILD_NINJA): print( @@ -166,21 +252,15 @@ def compile_unit(unit_name: str, output_path: str) -> str: ) sys.exit(1) - compdb = get_compdb() - if compdb is None: - sys.exit(1) - - entry = find_entry(compdb, source_path) - if entry is None: + command = get_build_command(target_path) + if command is None: print( - f"No compilation entry found for '{source_path}'.\n" - "Make sure the source file exists and `ninja all_source` has been run.", + f"No build command found for target '{target_path}'.\n" + "Make sure the unit exists and `python configure.py` has been run.", file=sys.stderr, ) sys.exit(1) - command = entry["command"] - # 1. Strip the dependency-file transform step — not needed for temp builds. command = strip_transform_dep(command) @@ -196,7 +276,7 @@ def compile_unit(unit_name: str, output_path: str) -> str: command = redirect_output(command, source_path, output_path) # 5. Run the compile. - result = subprocess.run(command, shell=True, cwd=root_dir) + result = subprocess.run(command, shell=isinstance(command, str), cwd=root_dir) if result.returncode != 0: print( f"Compilation failed (exit code {result.returncode})", file=sys.stderr diff --git a/tools/decomp-context.py b/tools/decomp-context.py index de8bafc0a..0ced57453 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -260,7 +260,7 @@ def check_ghidra() -> None: # Try a minimal command that lists available programs try: result = subprocess.run( - [ghidra_cmd, "list", "programs"], + [ghidra_cmd, "program", "list"], capture_output=True, timeout=15, ) @@ -275,9 +275,9 @@ def check_ghidra() -> None: print(f" Run: ghidra set-default project NeedForSpeed") if result.returncode != 0 and stderr: - print(f"WARN ghidra list programs exited {result.returncode}: {stderr}") + print(f"WARN ghidra program list exited {result.returncode}: {stderr}") except subprocess.TimeoutExpired: - print("WARN ghidra list programs timed out — Ghidra may be slow to start") + print("WARN ghidra program list timed out — Ghidra may be slow to start") except Exception as e: print(f"WARN could not verify programs: {e}") diff --git a/tools/share_worktree_assets.py b/tools/share_worktree_assets.py new file mode 100644 index 000000000..542f4b407 --- /dev/null +++ b/tools/share_worktree_assets.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 + +""" +Share stable debug/tool assets across git worktrees. + +This keeps branch-specific generated files (`build.ninja`, `objdiff.json`, +`compile_commands.json`, object files, etc.) local to each worktree while +deduplicating large immutable assets such as extracted game files, symbol dumps, +and downloaded tool binaries under the git common directory. + +Examples: + python tools/share_worktree_assets.py status + python tools/share_worktree_assets.py status --all + python tools/share_worktree_assets.py link --all +""" + +import argparse +import filecmp +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional, Set + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.abspath(os.path.join(script_dir, "..")) + +SHARED_ROOT_NAME = "worktree-shared" + + +@dataclass(frozen=True) +class AssetSpec: + relpath: str + kind: str + + +FIXED_ASSETS = ( + AssetSpec("NFSMWRELEASE.ELF", "file"), + AssetSpec("NFS.ELF", "file"), + AssetSpec("NFS.MAP", "file"), + AssetSpec(os.path.join("build", "tools"), "dir"), + AssetSpec(os.path.join("build", "compilers"), "dir"), + AssetSpec(os.path.join("build", "ppc_binutils"), "dir"), +) + + +def run_git(args: List[str], cwd: str) -> str: + result = subprocess.run( + ["git", *args], + cwd=cwd, + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(result.stderr.strip(), file=sys.stderr) + sys.exit(result.returncode) + return result.stdout + + +def git_common_dir(cwd: str) -> str: + common = run_git(["rev-parse", "--git-common-dir"], cwd).strip() + if os.path.isabs(common): + return common + return os.path.abspath(os.path.join(cwd, common)) + + +def list_worktrees(cwd: str) -> List[str]: + output = run_git(["worktree", "list", "--porcelain"], cwd) + worktrees = [] + for line in output.splitlines(): + if line.startswith("worktree "): + worktrees.append(line.split(" ", 1)[1]) + return worktrees + + +def tracked_paths(cwd: str) -> Set[str]: + output = run_git(["ls-files"], cwd) + return {line.strip() for line in output.splitlines() if line.strip()} + + +def lexists(path: str) -> bool: + return os.path.lexists(path) + + +def same_symlink(path: str, target: str) -> bool: + return os.path.islink(path) and os.path.realpath(path) == os.path.realpath(target) + + +def remove_path(path: str) -> None: + if os.path.islink(path) or os.path.isfile(path): + os.unlink(path) + elif os.path.isdir(path): + shutil.rmtree(path) + + +def ensure_parent(path: str) -> None: + parent = os.path.dirname(path) + if parent: + os.makedirs(parent, exist_ok=True) + + +def merge_file(src: str, dst: str, relpath: str) -> None: + ensure_parent(dst) + if not os.path.exists(dst): + shutil.copy2(src, dst) + return + if not filecmp.cmp(src, dst, shallow=False): + raise RuntimeError(f"Conflicting file contents for {relpath}") + + +def merge_symlink(src: str, dst: str, relpath: str) -> None: + resolved = os.path.realpath(src) + if os.path.isdir(resolved): + merge_tree(resolved, dst, relpath) + elif os.path.isfile(resolved): + merge_file(resolved, dst, relpath) + else: + raise RuntimeError(f"Broken symlink encountered while merging {relpath}: {src}") + + +def merge_tree(src: str, dst: str, relpath: str) -> None: + for current_root, dirnames, filenames in os.walk(src): + dirnames.sort() + filenames.sort() + rel_dir = os.path.relpath(current_root, src) + target_root = dst if rel_dir == "." else os.path.join(dst, rel_dir) + os.makedirs(target_root, exist_ok=True) + + next_dirnames = [] + for dirname in dirnames: + src_dir = os.path.join(current_root, dirname) + if os.path.islink(src_dir): + dst_dir = os.path.join(target_root, dirname) + rel_entry = os.path.join(relpath, os.path.relpath(src_dir, src)) + merge_symlink(src_dir, dst_dir, rel_entry) + continue + next_dirnames.append(dirname) + dirnames[:] = next_dirnames + + for filename in filenames: + src_file = os.path.join(current_root, filename) + if os.path.islink(src_file): + dst_file = os.path.join(target_root, filename) + rel_entry = os.path.join(relpath, os.path.relpath(src_file, src)) + merge_symlink(src_file, dst_file, rel_entry) + continue + dst_file = os.path.join(target_root, filename) + rel_file = os.path.join(relpath, os.path.relpath(src_file, src)) + merge_file(src_file, dst_file, rel_file) + + +def is_tracked_path(relpath: str, tracked: Set[str]) -> bool: + prefix = relpath + os.sep + return any(path == relpath or path.startswith(prefix) for path in tracked) + + +def discover_child_assets( + worktrees: Iterable[str], + parent_relpath: str, + skip_names: Iterable[str], + tracked: Set[str], +) -> Dict[str, AssetSpec]: + specs: Dict[str, AssetSpec] = {} + skip = set(skip_names) + for worktree in worktrees: + parent = os.path.join(worktree, parent_relpath) + if not os.path.isdir(parent): + continue + for dirpath, dirnames, filenames in os.walk(parent): + dirnames.sort() + filenames.sort() + rel_dir = os.path.relpath(dirpath, parent) + if rel_dir == ".": + children = list(dirnames) + list(filenames) + for name in children: + if name in skip: + continue + child = os.path.join(dirpath, name) + relpath = os.path.join(parent_relpath, name) + if is_tracked_path(relpath, tracked): + continue + kind = "dir" if os.path.isdir(child) else "file" + specs[relpath] = AssetSpec(relpath, kind) + dirnames[:] = [] + else: + dirnames[:] = [] + return specs + + +def discover_assets(worktrees: Iterable[str], shared_root: str) -> List[AssetSpec]: + tracked: Set[str] = set() + for worktree in worktrees: + tracked.update(tracked_paths(worktree)) + + specs: Dict[str, AssetSpec] = {} + for spec in FIXED_ASSETS: + if not is_tracked_path(spec.relpath, tracked): + specs[spec.relpath] = spec + for source_root in list(worktrees) + [shared_root]: + if os.path.isdir(os.path.join(source_root, "symbols")): + specs.update( + discover_child_assets( + [source_root], "symbols", skip_names=(), tracked=tracked + ).items() + ) + if os.path.isdir(os.path.join(source_root, "orig")): + for worktree in [source_root]: + orig_root = os.path.join(worktree, "orig") + if not os.path.isdir(orig_root): + continue + for version in sorted(os.listdir(orig_root)): + version_path = os.path.join(orig_root, version) + if not os.path.isdir(version_path): + continue + child_specs = discover_child_assets( + [worktree], + os.path.join("orig", version), + skip_names=(".gitkeep",), + tracked=tracked, + ) + specs.update(child_specs.items()) + return [specs[key] for key in sorted(specs)] + + +def asset_status(path: str, shared_path: str) -> str: + if same_symlink(path, shared_path): + return "shared" + if os.path.islink(path): + return "other-symlink" + if os.path.isdir(path): + return "local-dir" + if os.path.isfile(path): + return "local-file" + return "missing" + + +def ensure_shared_asset(spec: AssetSpec, worktrees: Iterable[str], shared_root: str) -> Optional[str]: + shared_path = os.path.join(shared_root, spec.relpath) + if spec.kind == "dir": + os.makedirs(shared_path, exist_ok=True) + found = False + for worktree in worktrees: + local_path = os.path.join(worktree, spec.relpath) + if same_symlink(local_path, shared_path) or not os.path.isdir(local_path): + continue + merge_tree(local_path, shared_path, spec.relpath) + found = True + if found or os.listdir(shared_path): + return shared_path + return None + + found = False + for worktree in worktrees: + local_path = os.path.join(worktree, spec.relpath) + if same_symlink(local_path, shared_path) or not os.path.isfile(local_path): + continue + merge_file(local_path, shared_path, spec.relpath) + found = True + if found or os.path.isfile(shared_path): + return shared_path + return None + + +def link_asset(worktree: str, spec: AssetSpec, shared_path: str) -> str: + local_path = os.path.join(worktree, spec.relpath) + if same_symlink(local_path, shared_path): + return "already-shared" + + if spec.kind == "dir": + if os.path.isdir(local_path): + merge_tree(local_path, shared_path, spec.relpath) + remove_path(local_path) + elif os.path.isfile(local_path): + raise RuntimeError(f"{spec.relpath}: expected directory in {worktree}") + elif os.path.islink(local_path): + remove_path(local_path) + else: + if os.path.isfile(local_path): + merge_file(local_path, shared_path, spec.relpath) + remove_path(local_path) + elif os.path.isdir(local_path): + raise RuntimeError(f"{spec.relpath}: expected file in {worktree}") + elif os.path.islink(local_path): + remove_path(local_path) + + ensure_parent(local_path) + os.symlink(shared_path, local_path) + return "linked" + + +def print_status(worktrees: List[str], shared_root: str) -> int: + assets = discover_assets(worktrees, shared_root) + print(f"Shared asset root: {shared_root}") + for worktree in worktrees: + print(f"\n[{worktree}]") + for spec in assets: + shared_path = os.path.join(shared_root, spec.relpath) + shared_state = "seeded" if lexists(shared_path) else "unseeded" + local_state = asset_status(os.path.join(worktree, spec.relpath), shared_path) + print(f" {spec.relpath:<40} {local_state:<12} {shared_state}") + return 0 + + +def link_assets(worktrees: List[str], shared_root: str) -> int: + os.makedirs(shared_root, exist_ok=True) + assets = discover_assets(worktrees, shared_root) + for spec in assets: + shared_path = ensure_shared_asset(spec, worktrees, shared_root) + if shared_path is None: + continue + for worktree in worktrees: + status = link_asset(worktree, spec, shared_path) + print(f"{worktree}: {spec.relpath} -> {status}") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Share stable debug/tool assets across git worktrees while keeping " + "generated build outputs local to each worktree." + ) + ) + parser.add_argument( + "command", + choices=("status", "link"), + help="Inspect or create shared asset symlinks.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Operate on all worktrees for this repository (default: current worktree only).", + ) + args = parser.parse_args() + + common_dir = git_common_dir(root_dir) + shared_root = os.path.join(common_dir, SHARED_ROOT_NAME) + worktrees = list_worktrees(root_dir) if args.all else [root_dir] + + try: + if args.command == "status": + return print_status(worktrees, shared_root) + return link_assets(worktrees, shared_root) + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From ea02c5c4edb1bfe1c2def5655ebc13b59dea7ff1 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 20:08:09 +0100 Subject: [PATCH 12/71] AGENTS: forbid touching comparison inputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index cdf83bc28..b278fa09f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,22 @@ Sub-agents must **not** write or edit code files, headers, configs, or other rep All persistent file changes, decomp implementations, scaffolding, and follow-up fixes must be done by the main worker after reviewing the read-only findings. +## Forbidden Changes + +Do **not** edit or otherwise touch the comparison and configuration inputs that define the +project's match metrics: + +- `config/GOWE69/symbols.txt` +- `config/GOWE69/splits.txt` +- `configure.py` + +Treat these files as read-only unless the user explicitly asks for a task that is specifically +about maintaining that infrastructure. + +Do **not** try to cheat objdiff, progress, or match metrics in any way. The goal is to improve +the real decompilation output, not to manipulate the comparison setup, hide mismatches, or make +progress numbers look better without actually matching the original code. + ### lookup.py — Symbol lookup from the debug dump Query structs, enums, functions, globals, and typedefs directly from the pre-extracted From a0a93fe4dea5addca07604a4bdc59f4d091e5eb2 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 23:46:28 +0100 Subject: [PATCH 13/71] Add decomp workflow wrapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 16 ++ .github/skills/implement/SKILL.md | 13 +- AGENTS.md | 16 ++ tools/decomp-workflow.py | 357 ++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 tools/decomp-workflow.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index b82c8b223..839a485d1 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -52,6 +52,14 @@ Determine the file path (e.g. `src/Speed/Indep/SourceLists/zWorld2`). The game u ### 1b. Get the full function list +Preferred shortcut: + +```sh +python tools/decomp-workflow.py unit -u main/Path/To/TU +``` + +Manual equivalent: + ```sh python tools/decomp-diff.py -u main/Path/To/TU ``` @@ -74,6 +82,14 @@ After scaffolding, rebuild and re-check the function list. Use `build-unit.py` to compile to a private temp `.o` so the status check isn't polluted by another concurrent temp build: +Preferred shortcut: + +```sh +python tools/decomp-workflow.py unit -u main/Path/To/TU +``` + +Manual equivalent: + ```sh ninja # full build to update shared state (progress, sha1) TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 52f30fefb..09b8f7690 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -13,6 +13,14 @@ Collect data from **all** of these sources in parallel where possible. ### 1a. decomp-context.py +Preferred shortcut: + +```sh +python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName +``` + +Equivalent manual form: + ```sh python tools/decomp-context.py -u main/Path/To/TU -f FunctionName ``` @@ -89,7 +97,10 @@ The game uses stlport, so you'll often encounter \_STL, but in the code it must ### Initial build -Compile to a private temp `.o` so your output isn't overwritten by other concurrent builds: +Compile to a private temp `.o` so your output isn't overwritten by other concurrent builds. +If you just need the standard context + temp-build flow, prefer +`python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName` and drop +down to the manual loop below when you need tighter control over repeated diff iterations: ```sh TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) diff --git a/AGENTS.md b/AGENTS.md index b278fa09f..7e829bea8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,22 @@ TEMPOBJ=$(python tools/build-unit.py -u main/Speed/Indep/SourceLists/zAnim) python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --base-obj "$TEMPOBJ" ``` +### decomp-workflow.py — Wrapper for common agent workflows + +Prefer this wrapper for routine agent-driven flows instead of manually chaining +`build-unit.py`, `decomp-context.py`, `decomp-diff.py`, and `decomp-status.py`: + +```sh +python tools/decomp-workflow.py health +python tools/decomp-workflow.py health --smoke-build-unit main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin +python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim +``` + +The wrapper keeps the existing tools as the source of truth. It is intended to reduce +repeated command chaining and to standardize temp-object handling and worktree preflight +checks for agents. + ### find-symbol.py — Check for existing definitions before declaring new types Before declaring any new struct, class, enum, global, or typedef, run this to check whether diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py new file mode 100644 index 000000000..ecde20867 --- /dev/null +++ b/tools/decomp-workflow.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 + +""" +Wrapper for common decomp workflows. + +This script keeps the existing tools as the source of truth and orchestrates the +most common agent flows: + + python tools/decomp-workflow.py health + python tools/decomp-workflow.py health --smoke-build-unit main/Speed/Indep/SourceLists/zCamera + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-source + python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zCamera +""" + +import argparse +import os +import subprocess +import sys +from typing import List, Optional, Sequence, Tuple + + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) +TOOLS_DIR = os.path.join(ROOT_DIR, "tools") + +BUILD_NINJA = os.path.join(ROOT_DIR, "build.ninja") +OBJDIFF_JSON = os.path.join(ROOT_DIR, "objdiff.json") +PS2_TYPES = os.path.join(ROOT_DIR, "symbols", "PS2", "PS2_types.nothpp") + +DEFAULT_SMOKE_UNIT = "main/Speed/Indep/SourceLists/zCamera" + +SHARED_ASSET_REQUIREMENTS = [ + ("NFSMWRELEASE.ELF", "GameCube ELF"), + ("NFS.ELF", "PS2 ELF"), + ("NFS.MAP", "PS2 MAP"), + (os.path.join("build", "tools"), "downloaded tooling"), + (os.path.join("orig", "GOWE69", "NFSMWRELEASE.ELF"), "GameCube original ELF"), + (os.path.join("orig", "SLES-53558-A124", "NFS.ELF"), "PS2 original ELF"), + (os.path.join("symbols", "Dwarf"), "DWARF dump"), + (os.path.join("symbols", "mw_dwarfdump.nothpp"), "combined dwarf dump"), +] + + +class WorkflowError(RuntimeError): + pass + + +def tool_path(name: str) -> str: + return os.path.join(TOOLS_DIR, name) + + +def python_tool(name: str, *args: str) -> List[str]: + return [sys.executable, tool_path(name), *args] + + +def print_section(title: str) -> None: + print(flush=True) + print("=" * 60, flush=True) + print(f" {title}", flush=True) + print("=" * 60, flush=True) + + +def format_failure( + cmd: Sequence[str], returncode: int, stdout: str = "", stderr: str = "" +) -> str: + message = [f"Command failed (exit {returncode}): {' '.join(cmd)}"] + stdout = stdout.strip() + stderr = stderr.strip() + if stdout: + message.append(f"stdout:\n{stdout}") + if stderr: + message.append(f"stderr:\n{stderr}") + return "\n".join(message) + + +def run_capture(cmd: Sequence[str]) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + cmd, + cwd=ROOT_DIR, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise WorkflowError( + format_failure(cmd, result.returncode, result.stdout, result.stderr) + ) + return result + + +def run_stream(cmd: Sequence[str]) -> None: + sys.stdout.flush() + sys.stderr.flush() + result = subprocess.run(cmd, cwd=ROOT_DIR, text=True) + if result.returncode != 0: + raise WorkflowError(format_failure(cmd, result.returncode)) + + +def ensure_exists(path: str, hint: str) -> None: + if not os.path.exists(path): + raise WorkflowError(f"Missing {path}\nHint: {hint}") + + +def ensure_decomp_prereqs() -> None: + ensure_exists( + BUILD_NINJA, + "Run: python configure.py", + ) + ensure_exists( + OBJDIFF_JSON, + "Run: python configure.py", + ) + + +def build_temp_obj(unit_name: str) -> str: + result = run_capture(python_tool("build-unit.py", "-u", unit_name)) + lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] + if not lines: + raise WorkflowError( + "build-unit.py succeeded but did not print an output path to stdout" + ) + actual = lines[-1] + if not os.path.exists(actual): + raise WorkflowError(f"build-unit.py reported a missing output path: {actual}") + return actual + + +def maybe_remove(path: Optional[str]) -> None: + if not path: + return + try: + if os.path.exists(path): + os.remove(path) + except OSError as e: + print(f"Warning: failed to remove temp object {path}: {e}", file=sys.stderr) + + +def describe_path(path: str) -> str: + if os.path.islink(path): + return "shared-symlink" + return "present" + + +def command_health(args: argparse.Namespace) -> None: + failures = 0 + + print_section("Worktree Health") + print(f"Root: {ROOT_DIR}") + + def report(ok: bool, label: str, detail: str) -> None: + nonlocal failures + status = "OK " if ok else "FAIL" + print(f"{status} {label}: {detail}", flush=True) + if not ok: + failures += 1 + + report( + os.path.exists(BUILD_NINJA), + "build.ninja", + BUILD_NINJA if os.path.exists(BUILD_NINJA) else "missing (run: python configure.py)", + ) + report( + os.path.exists(OBJDIFF_JSON), + "objdiff.json", + OBJDIFF_JSON if os.path.exists(OBJDIFF_JSON) else "missing (run: python configure.py)", + ) + + print_section("Shared Assets") + for rel_path, label in SHARED_ASSET_REQUIREMENTS: + abs_path = os.path.join(ROOT_DIR, rel_path) + report( + os.path.exists(abs_path), + label, + describe_path(abs_path) if os.path.exists(abs_path) else f"missing ({rel_path})", + ) + + print_section("Tool Checks") + try: + run_capture(python_tool("decomp-context.py", "--ghidra-check")) + report(True, "ghidra", "GC + PS2 programs available") + except WorkflowError as e: + report(False, "ghidra", str(e)) + + try: + run_capture(python_tool("lookup.py", "--file", PS2_TYPES, "struct", "Camera")) + report(True, "ps2-lookup", "Camera found in PS2 dump") + except WorkflowError as e: + report(False, "ps2-lookup", str(e)) + + if args.smoke_build_unit: + print_section("Build Smoke Test") + temp_obj = None + try: + temp_obj = build_temp_obj(args.smoke_build_unit) + report(True, "build-unit", temp_obj) + except WorkflowError as e: + report(False, "build-unit", str(e)) + finally: + maybe_remove(temp_obj) + + if failures: + raise WorkflowError(f"Health check failed with {failures} issue(s)") + + +def resolve_base_obj( + unit_name: str, base_obj: Optional[str], no_build: bool, keep_temp: bool +) -> Tuple[Optional[str], bool]: + if base_obj: + return os.path.abspath(base_obj), False + if no_build: + return None, False + temp_obj = build_temp_obj(unit_name) + print(f"Using temp object: {temp_obj}", flush=True) + return temp_obj, not keep_temp + + +def command_function(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + temp_obj = None + cleanup = False + try: + temp_obj, cleanup = resolve_base_obj( + args.unit, args.base_obj, args.no_build, args.keep_temp_obj + ) + print_section(f"Function Workflow: {args.function}") + cmd = python_tool("decomp-context.py", "-u", args.unit, "-f", args.function) + if args.no_source: + cmd.append("--no-source") + if args.no_ghidra: + cmd.append("--no-ghidra") + if temp_obj: + cmd.extend(["--base-obj", temp_obj]) + run_stream(cmd) + finally: + if cleanup: + maybe_remove(temp_obj) + + +def command_unit(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + temp_obj = None + cleanup = False + try: + temp_obj, cleanup = resolve_base_obj( + args.unit, args.base_obj, args.no_build, args.keep_temp_obj + ) + + print_section(f"Unit Status: {args.unit}") + run_stream(python_tool("decomp-status.py", "--unit", args.unit)) + + common_args: List[str] = ["-u", args.unit, "-t", "function"] + if temp_obj: + common_args.extend(["--base-obj", temp_obj]) + + print_section("Missing Functions") + run_stream(python_tool("decomp-diff.py", *common_args, "-s", "missing")) + + print_section("Nonmatching Functions") + run_stream(python_tool("decomp-diff.py", *common_args, "-s", "nonmatching")) + finally: + if cleanup: + maybe_remove(temp_obj) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Wrapper for common decomp workflows built on top of the existing project tools." + ) + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + health = subparsers.add_parser( + "health", + help="Check whether the current worktree is ready for GC and PS2 decomp work", + ) + health.add_argument( + "--smoke-build-unit", + metavar="UNIT", + nargs="?", + const=DEFAULT_SMOKE_UNIT, + help=( + "Also run build-unit.py as a smoke test. If UNIT is omitted, uses " + f"{DEFAULT_SMOKE_UNIT}" + ), + ) + health.set_defaults(func=command_health) + + function = subparsers.add_parser( + "function", + help="Build a temp object if needed and run decomp-context.py for one function", + ) + function.add_argument("-u", "--unit", required=True, help="Translation unit name") + function.add_argument("-f", "--function", required=True, help="Function name to inspect") + function.add_argument( + "--base-obj", + help="Use an explicit object file instead of building a temp object", + ) + function.add_argument( + "--no-build", + action="store_true", + help="Do not build a temp object when --base-obj is not provided", + ) + function.add_argument( + "--keep-temp-obj", + action="store_true", + help="Keep the auto-built temp object instead of deleting it afterwards", + ) + function.add_argument( + "--no-source", + action="store_true", + help="Pass through to decomp-context.py", + ) + function.add_argument( + "--no-ghidra", + action="store_true", + help="Pass through to decomp-context.py", + ) + function.set_defaults(func=command_function) + + unit = subparsers.add_parser( + "unit", + help="Show a compact unit workflow summary using decomp-status.py and decomp-diff.py", + ) + unit.add_argument("-u", "--unit", required=True, help="Translation unit name") + unit.add_argument( + "--base-obj", + help="Use an explicit object file instead of building a temp object", + ) + unit.add_argument( + "--no-build", + action="store_true", + help="Do not build a temp object when --base-obj is not provided", + ) + unit.add_argument( + "--keep-temp-obj", + action="store_true", + help="Keep the auto-built temp object instead of deleting it afterwards", + ) + unit.set_defaults(func=command_unit) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + try: + args.func(args) + except WorkflowError as e: + print(e, file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() From 50ea0e57409d23ff1add41643d328c70c8e171dc Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 12 Mar 2026 00:04:01 +0100 Subject: [PATCH 14/71] Refactor decomp workflow tooling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 7 +- .github/skills/implement/SKILL.md | 12 ++- AGENTS.md | 6 ++ tools/_common.py | 152 ++++++++++++++++++++++++++++++ tools/build-unit.py | 54 ++++------- tools/decomp-context.py | 97 +++---------------- tools/decomp-diff.py | 124 +++--------------------- tools/decomp-status.py | 23 ++--- tools/decomp-workflow.py | 129 +++++++++++++++++++++---- 9 files changed, 331 insertions(+), 273 deletions(-) create mode 100644 tools/_common.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 839a485d1..3a8fafbe3 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -61,7 +61,10 @@ python tools/decomp-workflow.py unit -u main/Path/To/TU Manual equivalent: ```sh -python tools/decomp-diff.py -u main/Path/To/TU +python tools/decomp-status.py --unit main/Path/To/TU +TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) +python tools/decomp-diff.py -u main/Path/To/TU -s missing -t function --base-obj "$TEMPOBJ" +python tools/decomp-diff.py -u main/Path/To/TU -s nonmatching -t function --base-obj "$TEMPOBJ" ``` This shows all symbols with their match status. Note the total count of missing, @@ -93,8 +96,8 @@ Manual equivalent: ```sh ninja # full build to update shared state (progress, sha1) TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) -python tools/decomp-diff.py -u main/Path/To/TU -s nonmatching -t function --base-obj "$TEMPOBJ" python tools/decomp-diff.py -u main/Path/To/TU -s missing -t function --base-obj "$TEMPOBJ" +python tools/decomp-diff.py -u main/Path/To/TU -s nonmatching -t function --base-obj "$TEMPOBJ" ``` ### 3c. Implement each function sequentially diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 09b8f7690..0e68e78c2 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -17,6 +17,7 @@ Preferred shortcut: ```sh python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName +python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName ``` Equivalent manual form: @@ -99,8 +100,15 @@ The game uses stlport, so you'll often encounter \_STL, but in the code it must Compile to a private temp `.o` so your output isn't overwritten by other concurrent builds. If you just need the standard context + temp-build flow, prefer -`python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName` and drop -down to the manual loop below when you need tighter control over repeated diff iterations: +`python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName`. +If you only need a temp build or a standardized diff run, use: + +```sh +python tools/decomp-workflow.py build -u main/Path/To/TU +python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName +``` + +Drop down to the manual loop below when you need tighter control over repeated diff iterations: ```sh TEMPOBJ=$(python tools/build-unit.py -u main/Path/To/TU) diff --git a/AGENTS.md b/AGENTS.md index 7e829bea8..609f8a06b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,6 +140,8 @@ Prefer this wrapper for routine agent-driven flows instead of manually chaining ```sh python tools/decomp-workflow.py health python tools/decomp-workflow.py health --smoke-build-unit main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py build -u main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim ``` @@ -148,6 +150,10 @@ The wrapper keeps the existing tools as the source of truth. It is intended to r repeated command chaining and to standardize temp-object handling and worktree preflight checks for agents. +On a newly updated or unusual worktree, run `python tools/decomp-workflow.py health` first. +If it reports missing generated files such as `objdiff.json` or `build.ninja`, run +`python configure.py` in that worktree before using the decomp wrappers. + ### find-symbol.py — Check for existing definitions before declaring new types Before declaring any new struct, class, enum, global, or typedef, run this to check whether diff --git a/tools/_common.py b/tools/_common.py new file mode 100644 index 000000000..773f7f12a --- /dev/null +++ b/tools/_common.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +import json +import os +import shutil +import subprocess +import sys +import tempfile +from typing import Any, Dict, Optional, Sequence + + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) +BUILD_NINJA = os.path.join(ROOT_DIR, "build.ninja") +OBJDIFF_JSON = os.path.join(ROOT_DIR, "objdiff.json") + + +class ToolError(RuntimeError): + pass + + +def fail(message: str) -> None: + print(message, file=sys.stderr) + sys.exit(1) + + +def format_subprocess_error( + cmd: Sequence[str], returncode: int, stdout: str = "", stderr: str = "" +) -> str: + message = [f"Command failed (exit {returncode}): {' '.join(cmd)}"] + stdout = stdout.strip() + stderr = stderr.strip() + if stdout: + message.append(f"stdout:\n{stdout}") + if stderr: + message.append(f"stderr:\n{stderr}") + return "\n".join(message) + + +def ensure_exists(path: str, hint: str) -> None: + if not os.path.exists(path): + raise ToolError(f"Missing {path}\nHint: {hint}") + + +def ensure_project_prereqs(require_build_ninja: bool = False) -> None: + ensure_exists(OBJDIFF_JSON, "Run: python configure.py") + if require_build_ninja: + ensure_exists(BUILD_NINJA, "Run: python configure.py") + + +def load_json_file(path: str, description: str) -> Any: + try: + with open(path) as f: + return json.load(f) + except FileNotFoundError: + raise ToolError(f"Missing {description}: {path}") + except json.JSONDecodeError as e: + raise ToolError(f"Failed to parse {description}: {e}") + + +def load_objdiff_config() -> Dict[str, Any]: + ensure_project_prereqs() + data = load_json_file(OBJDIFF_JSON, "objdiff.json") + if not isinstance(data, dict): + raise ToolError("objdiff.json does not contain a JSON object") + return data + + +def make_abs(path: Optional[str], base: str = ROOT_DIR) -> Optional[str]: + if path is None: + return None + if os.path.isabs(str(path)): + return str(path) + return os.path.abspath(os.path.join(base, str(path))) + + +def apply_base_obj_override( + config: Dict[str, Any], unit_name: str, base_obj: str, root_dir: str = ROOT_DIR +) -> bool: + found = False + for unit in config.get("units", []): + target_path = make_abs(unit.get("target_path"), root_dir) + if target_path is not None: + unit["target_path"] = target_path + + if unit.get("name") == unit_name: + unit["base_path"] = os.path.abspath(base_obj) + found = True + else: + base_path = make_abs(unit.get("base_path"), root_dir) + if base_path is not None: + unit["base_path"] = base_path + + metadata = unit.get("metadata") or {} + source_path = make_abs(metadata.get("source_path"), root_dir) + if source_path is not None: + metadata["source_path"] = source_path + + scratch = unit.get("scratch") or {} + ctx_path = make_abs(scratch.get("ctx_path"), root_dir) + if ctx_path is not None: + scratch["ctx_path"] = ctx_path + + return found + + +def run_objdiff_json( + objdiff_cli: str, + unit_name: str, + *, + base_obj: Optional[str] = None, + extra_args: Optional[Sequence[str]] = None, + root_dir: str = ROOT_DIR, +) -> Dict[str, Any]: + ensure_project_prereqs() + + cmd = [objdiff_cli, "diff"] + if extra_args: + cmd.extend(extra_args) + cmd.extend(["-u", unit_name, "-o", "-", "--format", "json"]) + + cwd = root_dir + tmpdir = None + if base_obj is not None: + config = load_objdiff_config() + if not apply_base_obj_override(config, unit_name, base_obj, root_dir=root_dir): + raise ToolError(f"Unit not found in objdiff.json: {unit_name}") + + tmpdir = tempfile.mkdtemp(prefix="nfsmw_objdiff_") + tmp_config = os.path.join(tmpdir, "objdiff.json") + with open(tmp_config, "w") as f: + json.dump(config, f) + cwd = tmpdir + + try: + result = subprocess.run( + cmd, + cwd=cwd, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise ToolError( + format_subprocess_error(cmd, result.returncode, result.stdout, result.stderr) + ) + try: + return json.loads(result.stdout) + except json.JSONDecodeError as e: + raise ToolError(f"objdiff-cli returned invalid JSON: {e}") + finally: + if tmpdir is not None: + shutil.rmtree(tmpdir, ignore_errors=True) diff --git a/tools/build-unit.py b/tools/build-unit.py index dbfacf7b7..29a440d8b 100644 --- a/tools/build-unit.py +++ b/tools/build-unit.py @@ -31,18 +31,16 @@ import tempfile from typing import Any, Dict, List, Optional, Tuple, Union -script_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(script_dir, "..")) -OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") -BUILD_NINJA = os.path.join(root_dir, "build.ninja") -COMPILE_COMMANDS = os.path.join(root_dir, "compile_commands.json") +from _common import BUILD_NINJA, OBJDIFF_JSON, ROOT_DIR, ToolError, fail, load_objdiff_config + +root_dir = ROOT_DIR +COMPILE_COMMANDS = os.path.join(ROOT_DIR, "compile_commands.json") Command = Union[str, List[str]] def load_objdiff() -> Dict[str, Any]: - with open(OBJDIFF_JSON) as f: - return json.load(f) + return load_objdiff_config() def find_unit_source(config: Dict[str, Any], unit_name: str) -> Optional[str]: @@ -221,45 +219,25 @@ def actual_output_path(command: Command, source_path: str, new_output: str) -> s def compile_unit(unit_name: str, output_path: str) -> str: """Compile unit to output_path and return the actual .o path.""" - if not os.path.exists(OBJDIFF_JSON): - print( - "objdiff.json not found. Run: python configure.py && ninja all_source", - file=sys.stderr, - ) - sys.exit(1) - config = load_objdiff() source_path = find_unit_source(config, unit_name) target_path = find_unit_target(config, unit_name) if not source_path: - print( + fail( f"No source_path found for unit '{unit_name}' in objdiff.json.\n" - "The unit may not have a source file yet (missing implementation).", - file=sys.stderr, + "The unit may not have a source file yet (missing implementation)." ) - sys.exit(1) if not target_path: - print( - f"No target_path found for unit '{unit_name}' in objdiff.json.", - file=sys.stderr, - ) - sys.exit(1) - + fail(f"No target_path found for unit '{unit_name}' in objdiff.json.") if not os.path.exists(BUILD_NINJA): - print( - "build.ninja not found. Run: python configure.py && ninja all_source", - file=sys.stderr, - ) - sys.exit(1) + fail(f"Missing {BUILD_NINJA}\nHint: Run: python configure.py") command = get_build_command(target_path) if command is None: - print( + fail( f"No build command found for target '{target_path}'.\n" - "Make sure the unit exists and `python configure.py` has been run.", - file=sys.stderr, + "Make sure the unit exists and `python configure.py` has been run." ) - sys.exit(1) # 1. Strip the dependency-file transform step — not needed for temp builds. command = strip_transform_dep(command) @@ -278,10 +256,7 @@ def compile_unit(unit_name: str, output_path: str) -> str: # 5. Run the compile. result = subprocess.run(command, shell=isinstance(command, str), cwd=root_dir) if result.returncode != 0: - print( - f"Compilation failed (exit code {result.returncode})", file=sys.stderr - ) - sys.exit(1) + fail(f"Compilation failed (exit code {result.returncode})") return actual @@ -316,7 +291,10 @@ def main() -> None: ) os.close(fd) - actual = compile_unit(args.unit, output_path) + try: + actual = compile_unit(args.unit, output_path) + except ToolError as e: + fail(str(e)) print(actual) diff --git a/tools/decomp-context.py b/tools/decomp-context.py index 0ced57453..981bed4a1 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -22,16 +22,15 @@ import os import re import shutil -import tempfile import subprocess import sys from typing import Any, Dict, List, Optional, Tuple +from _common import ROOT_DIR, ToolError, fail, load_objdiff_config, run_objdiff_json script_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(script_dir, "..")) +root_dir = ROOT_DIR OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") -OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") DTK = os.path.join(root_dir, "build", "tools", "dtk") GC_SYMBOLS_FILE = os.path.join(root_dir, "config", "GOWE69", "symbols.txt") PS2_SYMBOLS_FILE = os.path.join(root_dir, "config", "SLES-53558-A124", "symbols.txt") @@ -44,8 +43,7 @@ def load_project_config() -> Dict[str, Any]: """Load objdiff.json.""" - with open(OBJDIFF_JSON) as f: - return json.load(f) + return load_objdiff_config() def find_unit(config: Dict[str, Any], unit_name: str) -> Optional[Dict[str, Any]]: @@ -57,86 +55,12 @@ def find_unit(config: Dict[str, Any], unit_name: str) -> Optional[Dict[str, Any] def run_objdiff(unit_name: str, base_obj: Optional[str] = None) -> Optional[Dict[str, Any]]: - """Run objdiff-cli diff and return parsed JSON. - - If base_obj is given, a temporary objdiff.json is used that overrides the - base_path for this unit so parallel agents don't interfere with each other. - """ - if base_obj is not None: - return _run_objdiff_with_base_obj(unit_name, base_obj) - - result = subprocess.run( - [OBJDIFF_CLI, "diff", "-u", unit_name, "-o", "-", "--format", "json"], - capture_output=True, - cwd=root_dir, + return run_objdiff_json( + OBJDIFF_CLI, + unit_name, + base_obj=base_obj, + root_dir=root_dir, ) - if result.returncode != 0: - return None - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return None - - -def _make_abs(path: Optional[str], base: str) -> Optional[str]: - if path is None: - return None - if os.path.isabs(str(path)): - return str(path) - return os.path.abspath(os.path.join(base, str(path))) - - -def _run_objdiff_with_base_obj(unit_name: str, base_obj: str) -> Optional[Dict[str, Any]]: - """Run objdiff-cli using a temporary config pointing base_path at base_obj.""" - with open(OBJDIFF_JSON) as f: - config = json.load(f) - - found = False - for unit in config.get("units", []): - tp = _make_abs(unit.get("target_path"), root_dir) - if tp is not None: - unit["target_path"] = tp - - if unit["name"] == unit_name: - unit["base_path"] = os.path.abspath(base_obj) - found = True - else: - bp = _make_abs(unit.get("base_path"), root_dir) - if bp is not None: - unit["base_path"] = bp - - meta = unit.get("metadata") or {} - sp = _make_abs(meta.get("source_path"), root_dir) - if sp is not None: - meta["source_path"] = sp - - scratch = unit.get("scratch") or {} - cp = _make_abs(scratch.get("ctx_path"), root_dir) - if cp is not None: - scratch["ctx_path"] = cp - - if not found: - return None - - tmpdir = tempfile.mkdtemp(prefix="nfsmw_objdiff_") - try: - tmp_config = os.path.join(tmpdir, "objdiff.json") - with open(tmp_config, "w") as f: - json.dump(config, f) - - result = subprocess.run( - [OBJDIFF_CLI, "diff", "-u", unit_name, "-o", "-", "--format", "json"], - capture_output=True, - cwd=tmpdir, - ) - if result.returncode != 0: - return None - try: - return json.loads(result.stdout) - except json.JSONDecodeError: - return None - finally: - shutil.rmtree(tmpdir, ignore_errors=True) def find_symbol_in_diff( @@ -569,4 +493,7 @@ def main(): if __name__ == "__main__": - main() + try: + main() + except ToolError as e: + fail(str(e)) diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index db62b7e24..034397155 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -20,126 +20,23 @@ import argparse import json import os -import shutil import subprocess import sys -import tempfile from typing import Any, Dict, List, Optional, Tuple +from _common import ROOT_DIR, ToolError, fail, run_objdiff_json -script_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(script_dir, "..")) - +root_dir = ROOT_DIR OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") -OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") - - -def _make_abs(path: Optional[str], base: str) -> Optional[str]: - """Return an absolute version of path relative to base, or None.""" - if path is None: - return None - if os.path.isabs(str(path)): - return str(path) - return os.path.abspath(os.path.join(base, str(path))) - - -def _absolutize_config(config: Dict[str, Any], unit_name: str, base_obj: str) -> bool: - """Rewrite all file paths in config to absolute paths and override base_path - for unit_name with base_obj. Returns True if the unit was found.""" - found = False - for unit in config.get("units", []): - tp = _make_abs(unit.get("target_path"), root_dir) - if tp is not None: - unit["target_path"] = tp - - if unit["name"] == unit_name: - unit["base_path"] = os.path.abspath(base_obj) - found = True - else: - bp = _make_abs(unit.get("base_path"), root_dir) - if bp is not None: - unit["base_path"] = bp - - meta = unit.get("metadata") or {} - sp = _make_abs(meta.get("source_path"), root_dir) - if sp is not None: - meta["source_path"] = sp - - scratch = unit.get("scratch") or {} - cp = _make_abs(scratch.get("ctx_path"), root_dir) - if cp is not None: - scratch["ctx_path"] = cp - - return found def run_objdiff(unit: str, base_obj: Optional[str] = None) -> Dict[str, Any]: - """Run objdiff-cli diff and return parsed JSON. - - If base_obj is given, a temporary objdiff.json is created that overrides the - base_path for this unit so parallel agents don't interfere with each other. - """ - if base_obj is not None: - return _run_objdiff_with_base_obj(unit, base_obj) - - result = subprocess.run( - [ - OBJDIFF_CLI, - "diff", - "-c", - "functionRelocDiffs=none", - "-u", - unit, - "-o", - "-", - "--format", - "json", - ], - capture_output=True, - cwd=root_dir, + return run_objdiff_json( + OBJDIFF_CLI, + unit, + base_obj=base_obj, + extra_args=["-c", "functionRelocDiffs=none"], + root_dir=root_dir, ) - if result.returncode != 0: - print(f"objdiff-cli error: {result.stderr.decode()}", file=sys.stderr) - sys.exit(1) - return json.loads(result.stdout) - - -def _run_objdiff_with_base_obj(unit: str, base_obj: str) -> Dict[str, Any]: - """Run objdiff-cli using a temporary config that points base_path at base_obj.""" - with open(OBJDIFF_JSON) as f: - config = json.load(f) - - if not _absolutize_config(config, unit, base_obj): - print(f"Unit not found in objdiff.json: {unit}", file=sys.stderr) - sys.exit(1) - - tmpdir = tempfile.mkdtemp(prefix="nfsmw_objdiff_") - try: - tmp_config = os.path.join(tmpdir, "objdiff.json") - with open(tmp_config, "w") as f: - json.dump(config, f) - - result = subprocess.run( - [ - OBJDIFF_CLI, - "diff", - "-c", - "functionRelocDiffs=none", - "-u", - unit, - "-o", - "-", - "--format", - "json", - ], - capture_output=True, - cwd=tmpdir, - ) - if result.returncode != 0: - print(f"objdiff-cli error: {result.stderr.decode()}", file=sys.stderr) - sys.exit(1) - return json.loads(result.stdout) - finally: - shutil.rmtree(tmpdir, ignore_errors=True) def classify_symbol(sym: Dict[str, Any]) -> str: @@ -571,7 +468,10 @@ def main(): args = parser.parse_args() - data = run_objdiff(args.unit, base_obj=args.base_obj) + try: + data = run_objdiff(args.unit, base_obj=args.base_obj) + except ToolError as e: + fail(str(e)) if args.diff: build_diff(data, args.diff, args) diff --git a/tools/decomp-status.py b/tools/decomp-status.py index 0f4058497..69917e7dd 100644 --- a/tools/decomp-status.py +++ b/tools/decomp-status.py @@ -16,35 +16,25 @@ import argparse import json import os -import subprocess import sys from typing import Any, Dict, List, Optional +from _common import ROOT_DIR, ToolError, fail, load_objdiff_config, run_objdiff_json -script_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(script_dir, "..")) +root_dir = ROOT_DIR OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") -OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") def load_project_config() -> Dict[str, Any]: """Load objdiff.json project configuration.""" - with open(OBJDIFF_JSON) as f: - return json.load(f) + return load_objdiff_config() def run_objdiff(unit_name: str) -> Optional[Dict[str, Any]]: """Run objdiff-cli diff for a unit and return parsed JSON.""" - result = subprocess.run( - [OBJDIFF_CLI, "diff", "-u", unit_name, "-o", "-", "--format", "json"], - capture_output=True, - cwd=root_dir, - ) - if result.returncode != 0: - return None try: - return json.loads(result.stdout) - except json.JSONDecodeError: + return run_objdiff_json(OBJDIFF_CLI, unit_name, root_dir=root_dir) + except ToolError: return None @@ -141,8 +131,7 @@ def main(): categorized.setdefault(cat, []).append(unit) if not categorized: - print("No units match the given filters.", file=sys.stderr) - sys.exit(1) + fail("No units match the given filters.") # Process each unit results = {} diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index ecde20867..8c6da989f 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -18,14 +18,11 @@ import subprocess import sys from typing import List, Optional, Sequence, Tuple +from _common import BUILD_NINJA, OBJDIFF_JSON, ROOT_DIR, ToolError, ensure_exists SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) -ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) TOOLS_DIR = os.path.join(ROOT_DIR, "tools") - -BUILD_NINJA = os.path.join(ROOT_DIR, "build.ninja") -OBJDIFF_JSON = os.path.join(ROOT_DIR, "objdiff.json") PS2_TYPES = os.path.join(ROOT_DIR, "symbols", "PS2", "PS2_types.nothpp") DEFAULT_SMOKE_UNIT = "main/Speed/Indep/SourceLists/zCamera" @@ -96,20 +93,12 @@ def run_stream(cmd: Sequence[str]) -> None: raise WorkflowError(format_failure(cmd, result.returncode)) -def ensure_exists(path: str, hint: str) -> None: - if not os.path.exists(path): - raise WorkflowError(f"Missing {path}\nHint: {hint}") - - def ensure_decomp_prereqs() -> None: - ensure_exists( - BUILD_NINJA, - "Run: python configure.py", - ) - ensure_exists( - OBJDIFF_JSON, - "Run: python configure.py", - ) + try: + ensure_exists(BUILD_NINJA, "Run: python configure.py") + ensure_exists(OBJDIFF_JSON, "Run: python configure.py") + except ToolError as e: + raise WorkflowError(str(e)) def build_temp_obj(unit_name: str) -> str: @@ -262,6 +251,52 @@ def command_unit(args: argparse.Namespace) -> None: maybe_remove(temp_obj) +def command_build(args: argparse.Namespace) -> None: + cmd = python_tool("build-unit.py", "-u", args.unit) + if args.output: + cmd.extend(["-o", os.path.abspath(args.output)]) + run_stream(cmd) + + +def command_diff(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + temp_obj = None + cleanup = False + try: + temp_obj, cleanup = resolve_base_obj( + args.unit, args.base_obj, args.no_build, args.keep_temp_obj + ) + + title = f"Diff Workflow: {args.unit}" + if args.diff: + title += f" / {args.diff}" + print_section(title) + + cmd: List[str] = python_tool("decomp-diff.py", "-u", args.unit) + if args.diff: + cmd.extend(["-d", args.diff]) + if args.type: + cmd.extend(["-t", args.type]) + if args.status: + cmd.extend(["-s", args.status]) + if args.section: + cmd.extend(["--section", args.section]) + if args.search: + cmd.extend(["--search", args.search]) + if args.context is not None: + cmd.extend(["-C", str(args.context)]) + if args.range: + cmd.extend(["--range", args.range]) + if args.no_collapse: + cmd.append("--no-collapse") + if temp_obj: + cmd.extend(["--base-obj", temp_obj]) + run_stream(cmd) + finally: + if cleanup: + maybe_remove(temp_obj) + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( @@ -339,6 +374,66 @@ def build_parser() -> argparse.ArgumentParser: ) unit.set_defaults(func=command_unit) + build = subparsers.add_parser( + "build", + help="Run build-unit.py with wrapper-friendly defaults", + ) + build.add_argument("-u", "--unit", required=True, help="Translation unit name") + build.add_argument( + "-o", + "--output", + help="Explicit output .o path (default: auto-generated temp file)", + ) + build.set_defaults(func=command_build) + + diff = subparsers.add_parser( + "diff", + help="Build a temp object if needed and run decomp-diff.py", + ) + diff.add_argument("-u", "--unit", required=True, help="Translation unit name") + diff.add_argument( + "-d", + "--diff", + metavar="SYMBOL", + help="Show diff for a specific symbol instead of overview mode", + ) + diff.add_argument("-t", "--type", help="Filter by type: function, object") + diff.add_argument( + "-s", + "--status", + help="Filter by status: missing, matching, nonmatching, extra", + ) + diff.add_argument("--section", help="Filter by section name") + diff.add_argument("--search", help="Fuzzy search on demangled symbol name") + diff.add_argument( + "-C", + "--context", + type=int, + default=3, + help="Context lines around mismatches (default: 3)", + ) + diff.add_argument("--range", help="Instruction offset range (hex, e.g. 100-200)") + diff.add_argument( + "--no-collapse", + action="store_true", + help="Don't collapse matching instruction runs", + ) + diff.add_argument( + "--base-obj", + help="Use an explicit object file instead of building a temp object", + ) + diff.add_argument( + "--no-build", + action="store_true", + help="Do not build a temp object when --base-obj is not provided", + ) + diff.add_argument( + "--keep-temp-obj", + action="store_true", + help="Keep the auto-built temp object instead of deleting it afterwards", + ) + diff.set_defaults(func=command_diff) + return parser From a83bcdb2a04921ddd1d916ab2554ed92f4a7d57e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 12 Mar 2026 01:57:18 +0100 Subject: [PATCH 15/71] Tune decomp workflow context helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 2 +- .github/skills/implement/SKILL.md | 14 + AGENTS.md | 25 +- tools/_common.py | 46 +- tools/decomp-context.py | 745 ++++++++++++++++++++++++++++-- tools/decomp-diff.py | 30 +- tools/decomp-status.py | 18 +- tools/decomp-workflow.py | 179 ++++++- 8 files changed, 1009 insertions(+), 50 deletions(-) diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 3a8fafbe3..66443927e 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -55,7 +55,7 @@ Determine the file path (e.g. `src/Speed/Indep/SourceLists/zWorld2`). The game u Preferred shortcut: ```sh -python tools/decomp-workflow.py unit -u main/Path/To/TU +python tools/decomp-workflow.py unit -u main/Path/To/TU --limit 20 ``` Manual equivalent: diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 0e68e78c2..66580e0c5 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -17,9 +17,19 @@ Preferred shortcut: ```sh python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName +python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName --brief python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName ``` +If you only need one Ghidra view, add `--ghidra-version gc` or `--ghidra-version ps2` +to keep the context run faster and shorter. + +The wrapper defaults to compact GC DWARF signatures. Add `--lookup-mode full` when you +need the full DWARF body with locals and nested inline info. + +Add `--brief` when you want a shorter helper view; it trims suggested commands and +related-source hints while keeping the core source/status/diff context. + Equivalent manual form: ```sh @@ -29,6 +39,10 @@ python tools/decomp-context.py -u main/Path/To/TU -f FunctionName This provides in one shot: - Current source code (if any exists) +- A fallback source excerpt from the GC debug-line-mapped repo file when the metadata + source path is empty or otherwise unhelpful +- Related source-file hints when the unit metadata source is empty or unhelpful +- Compact GC DWARF signature by default, or full DWARF body with `--lookup-mode full` - objdiff status and instruction-level diff - Ghidra decompilation of the original diff --git a/AGENTS.md b/AGENTS.md index 609f8a06b..22f60e3cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,19 +140,40 @@ Prefer this wrapper for routine agent-driven flows instead of manually chaining ```sh python tools/decomp-workflow.py health python tools/decomp-workflow.py health --smoke-build-unit main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py health --smoke-dtk main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py build -u main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin -python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --brief +python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --ghidra-version gc +python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --lookup-mode full +python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim --search FindIOWin --limit 20 ``` The wrapper keeps the existing tools as the source of truth. It is intended to reduce repeated command chaining and to standardize temp-object handling and worktree preflight checks for agents. +`function` is the preferred context-gathering entrypoint: it bundles source excerpt, +objdiff status/diff, compact GC DWARF function lookup, and Ghidra output in one run. +If the unit metadata points at an empty or otherwise useless source-list file, it also +falls back to the GC debug-line-mapped repo source file when that file exists and has +real content. +Add `--brief` when you want to keep the helper sections compact; it trims suggested +commands and related-source hints without hiding the core status/diff/source data. + +When working with these tools, do not just work around recurring friction silently. If you +notice a clear, safe workflow or tooling improvement that would make future decomp work +faster, shorter, or more reliable, prefer implementing that improvement as part of the task +instead of leaving the paper cut in place. Favor small, surgical tuning to wrappers, shared +helpers, error messages, output shaping, and context-gathering defaults when they remove +repeated manual steps for future agents. + On a newly updated or unusual worktree, run `python tools/decomp-workflow.py health` first. If it reports missing generated files such as `objdiff.json` or `build.ninja`, run -`python configure.py` in that worktree before using the decomp wrappers. +`python configure.py` in that worktree before using the decomp wrappers. `health` also +checks the debug-symbol side of the setup now: GC/PS2 `symbols.txt`, GC DWARF lookup, +PS2 type lookup, and the GC debug line mapping. ### find-symbol.py — Check for existing definitions before declaring new types diff --git a/tools/_common.py b/tools/_common.py index 773f7f12a..a479a3e57 100644 --- a/tools/_common.py +++ b/tools/_common.py @@ -2,6 +2,7 @@ import json import os +import re import shutil import subprocess import sys @@ -66,6 +67,13 @@ def load_objdiff_config() -> Dict[str, Any]: return data +def find_objdiff_unit(config: Dict[str, Any], unit_name: str) -> Optional[Dict[str, Any]]: + for unit in config.get("units", []): + if unit.get("name") == unit_name: + return unit + return None + + def make_abs(path: Optional[str], base: str = ROOT_DIR) -> Optional[str]: if path is None: return None @@ -140,8 +148,44 @@ def run_objdiff_json( capture_output=True, ) if result.returncode != 0: + stderr = result.stderr + hint_lines = [] + missing_path = None + + if "No such file or directory" in stderr: + match = re.search(r"Failed:\s+Loading\s+(.+)", stderr) + if match: + missing_path = match.group(1).strip() + + if missing_path is not None: + if base_obj is not None: + hint_lines.extend( + [ + f"Hint: the requested base object is missing: {missing_path}", + f"Rebuild it with: python tools/build-unit.py -u {unit_name}", + ] + ) + else: + hint_lines.extend( + [ + f"Hint: the shared build output for {unit_name} is missing: {missing_path}", + "Fastest one-off fix for direct tools:", + f" TEMPOBJ=$(python tools/build-unit.py -u {unit_name})", + " rerun your diff/context command with --base-obj \"$TEMPOBJ\"", + "Wrapper flows that auto-build temp objects:", + f" python tools/decomp-workflow.py unit -u {unit_name}", + f" python tools/decomp-workflow.py diff -u {unit_name} ...", + "Or rebuild shared outputs with: ninja all_source", + ] + ) + + message = format_subprocess_error( + cmd, result.returncode, result.stdout, result.stderr + ) + if hint_lines: + message += "\n" + "\n".join(hint_lines) raise ToolError( - format_subprocess_error(cmd, result.returncode, result.stdout, result.stderr) + message ) try: return json.loads(result.stdout) diff --git a/tools/decomp-context.py b/tools/decomp-context.py index 981bed4a1..3ec255d97 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -3,12 +3,15 @@ """ Function context gatherer for decomp work. -Collects some things an agent needs to work on matching a function: objdiff status/diff and Ghidra decompile. -Source code and dwarf info should be queried from the lookup script instead. +Collects the common function context an agent needs for matching work: +source excerpt, objdiff status/diff, GC DWARF lookup, and Ghidra decompile. Usage: python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --no-ghidra + python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --ghidra-version gc + python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --lookup-mode signature + python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --no-lookup python tools/decomp-context.py -u main/Speed/Indep/SourceLists/zAnim -f __9CAnimBank --no-source python tools/decomp-context.py --ghidra-check @@ -34,11 +37,16 @@ DTK = os.path.join(root_dir, "build", "tools", "dtk") GC_SYMBOLS_FILE = os.path.join(root_dir, "config", "GOWE69", "symbols.txt") PS2_SYMBOLS_FILE = os.path.join(root_dir, "config", "SLES-53558-A124", "symbols.txt") +GC_DWARF_FILE = os.path.join(root_dir, "symbols", "mw_dwarfdump.nothpp") +DEBUG_LINES_FILE = os.path.join(root_dir, "symbols", "debug_lines.txt") GC_GHIDRA_PROGRAM = "NFSMWRELEASE.ELF" PS2_GHIDRA_PROGRAM = "NFS.ELF" # Number of lines of context to show before/after the matched function in source SOURCE_CONTEXT_LINES = 5 +RELATED_SOURCE_LIMIT = 8 +BRIEF_RELATED_SOURCE_LIMIT = 3 +BRIEF_SUGGESTED_COMMAND_LIMIT = 2 def load_project_config() -> Dict[str, Any]: @@ -98,6 +106,16 @@ def find_symbol_in_diff( return None, None +def describe_symbol_presence( + left_sym: Optional[Dict[str, Any]], right_sym: Optional[Dict[str, Any]] +) -> str: + if left_sym is not None and right_sym is None: + return "missing in decomp output" + if left_sym is None and right_sym is not None: + return "extra in decomp output" + return "present on both sides" + + def lookup_symbol_address(file: str, mangled_name: str) -> Optional[str]: """Look up a symbol's address from symbols.txt.""" if not os.path.exists(file): @@ -113,6 +131,53 @@ def lookup_symbol_address(file: str, mangled_name: str) -> Optional[str]: return None +def format_hex_address(address: str) -> str: + return address if address.lower().startswith("0x") else f"0x{address}" + + +def lookup_function_dwarf(query: str) -> Tuple[Optional[str], Optional[str]]: + """Query the combined GC DWARF dump for one function.""" + if not os.path.exists(GC_DWARF_FILE): + return None, f"DWARF dump not found: {GC_DWARF_FILE}" + + cmd = [ + sys.executable, + os.path.join(script_dir, "lookup.py"), + "--file", + GC_DWARF_FILE, + "function", + query, + ] + result = subprocess.run(cmd, capture_output=True, cwd=root_dir, text=True) + if result.returncode == 0: + return result.stdout.strip(), None + + detail = result.stderr.strip() or result.stdout.strip() or "lookup failed" + return None, detail + + +def compact_dwarf_function_text(text: str) -> str: + lines = [line.rstrip() for line in text.splitlines()] + kept: List[str] = [] + signature = None + + for line in lines: + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("//"): + if stripped.startswith("// Range:") or stripped.startswith("// this:"): + kept.append(line) + continue + signature = line + break + + if signature is not None: + kept.append(signature) + + return "\n".join(kept) if kept else text + + def search_symbols_file(file: str, name: str) -> List[Tuple[str, str, str]]: """Search symbols.txt for entries matching a name. Returns [(mangled, section, address)].""" if not os.path.exists(file): @@ -243,6 +308,9 @@ def extract_source_for_function( with open(full_path) as f: all_lines = f.readlines() + if not all_lines: + return "" + # Collect all source line numbers referenced by the decomp symbol's instructions line_numbers: List[int] = [] if right_sym: @@ -264,13 +332,441 @@ def extract_source_for_function( # the function body, giving context. start = max(1, first_line - SOURCE_CONTEXT_LINES) end = min(len(all_lines), last_line + SOURCE_CONTEXT_LINES) + if start > end: + return "" # 1-indexed lines -> 0-indexed slice excerpt = all_lines[start - 1 : end] + if not excerpt: + return "" header = f"// Lines {start}–{end} of {source_path}\n" return header + "".join(excerpt) +def extract_source_around_line( + source_path: str, line_number: int, context_lines: int = SOURCE_CONTEXT_LINES +) -> Optional[str]: + full_path = os.path.join(root_dir, source_path) + if not os.path.exists(full_path): + return None + + with open(full_path) as f: + all_lines = f.readlines() + + if not all_lines: + return "" + + target_line = min(max(1, line_number), len(all_lines)) + start = max(1, target_line - context_lines) + end = min(len(all_lines), target_line + context_lines) + excerpt = all_lines[start - 1 : end] + header = ( + f"// Lines {start}–{end} of {source_path} " + f"(GC debug-line fallback, target line {line_number})\n" + ) + return header + "".join(excerpt) + + +def definition_name_variants(function_name: str) -> Tuple[List[str], Optional[str]]: + qualified = function_name.split("(", 1)[0].strip() + scoped_parts = [part.strip() for part in qualified.split("::") if part.strip()] + + qualified_variants: List[str] = [] + if qualified: + qualified_variants.append(qualified) + if len(scoped_parts) >= 2: + tail_qualified = "::".join(scoped_parts[-2:]) + if tail_qualified not in qualified_variants: + qualified_variants.append(tail_qualified) + + bare_name = scoped_parts[-1] if scoped_parts else (qualified or None) + return qualified_variants, bare_name + + +def line_has_nearby_open_brace(lines: List[str], line_number: int) -> bool: + start = max(1, line_number) + end = min(len(lines), line_number + 2) + for idx in range(start, end + 1): + if "{" in lines[idx - 1]: + return True + return False + + +def find_function_definition_line( + lines: List[str], function_name: str, target_line: Optional[int] = None +) -> Optional[int]: + qualified_variants, bare_name = definition_name_variants(function_name) + candidates: List[Tuple[int, int]] = [] + + for index, raw_line in enumerate(lines, start=1): + line = raw_line.strip() + if not line: + continue + + score = 0 + for i, qualified in enumerate(qualified_variants): + if re.search(rf"\b{re.escape(qualified)}\s*\(", line): + score = max(score, 260 - (i * 40)) + + if bare_name and re.search(rf"\b{re.escape(bare_name)}\s*\(", line): + score = max(score, 120) + + if score == 0: + continue + + if line.endswith(";"): + score -= 120 + if "::" in line: + score += 20 + if line_has_nearby_open_brace(lines, index): + score += 35 + + if target_line is not None: + score -= min(abs(index - target_line), 120) + + candidates.append((score, index)) + + if not candidates: + return None + + if target_line is None: + candidates.sort(key=lambda item: (-item[0], item[1])) + else: + candidates.sort(key=lambda item: (-item[0], abs(item[1] - target_line), item[1])) + return candidates[0][1] + + +def extract_source_for_definition( + source_path: str, definition_line: int, target_line: Optional[int] = None +) -> Optional[str]: + full_path = os.path.join(root_dir, source_path) + if not os.path.exists(full_path): + return None + + with open(full_path) as f: + all_lines = f.readlines() + + if not all_lines: + return "" + + start = definition_line + while start > 1: + previous = all_lines[start - 2].strip() + if not previous or previous.endswith(";") or previous == "}": + break + if previous.startswith("//"): + break + start -= 1 + if definition_line - start >= 3: + break + + brace_depth = 0 + saw_open_brace = False + end: Optional[int] = None + for index in range(start, len(all_lines) + 1): + line = all_lines[index - 1] + if "{" in line: + saw_open_brace = True + if saw_open_brace: + brace_depth += line.count("{") + brace_depth -= line.count("}") + if brace_depth <= 0: + end = index + break + + if end is None: + return extract_source_around_line( + source_path, + target_line if target_line is not None else definition_line, + ) + + excerpt = all_lines[start - 1 : end] + detail = "GC debug-line fallback, matched definition" + if target_line is not None: + detail += f", target line {target_line}" + header = f"// Lines {start}–{end} of {source_path} ({detail})\n" + return header + "".join(excerpt) + + +def extract_source_from_debug_hint( + source_path: str, function_name: str, target_line: int +) -> Optional[str]: + full_path = os.path.join(root_dir, source_path) + if not os.path.exists(full_path): + return None + + with open(full_path) as f: + all_lines = f.readlines() + + if not all_lines: + return "" + + definition_line = find_function_definition_line( + all_lines, function_name, target_line=target_line + ) + if definition_line is not None: + return extract_source_for_definition( + source_path, definition_line, target_line=target_line + ) + + return extract_source_around_line(source_path, target_line) + + +def resolve_repo_path_from_debug_path(debug_path: str) -> Optional[str]: + normalized = debug_path.replace("\\", "/").strip() + lowered = normalized.lower() + + suffixes: List[str] = [] + if "/speed/indep/" in lowered: + suffixes.append(lowered.split("/speed/indep/", 1)[1].lstrip("/")) + if "/src/" in lowered: + suffixes.append("src/" + lowered.split("/src/", 1)[1].lstrip("/")) + basename = os.path.basename(lowered) + if basename: + suffixes.append(basename) + + matches: List[str] = [] + source_root = os.path.join(root_dir, "src") + for dirpath, _, filenames in os.walk(source_root): + for filename in filenames: + rel_path = os.path.relpath(os.path.join(dirpath, filename), root_dir) + rel_lower = rel_path.replace("\\", "/").lower() + if any(rel_lower.endswith(suffix) for suffix in suffixes): + matches.append(rel_path) + + if not matches: + return None + matches.sort(key=lambda path: (len(path), path)) + return matches[0] + + +def lookup_debug_line_source(address: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]: + if not os.path.exists(DEBUG_LINES_FILE): + return None, f"debug lines file not found: {DEBUG_LINES_FILE}" + + try: + target = int(address, 16) + except ValueError: + return None, f"invalid address for debug line lookup: {address}" + + pattern = re.compile(r"^\s*0x([0-9A-Fa-f]+):\s*(.+?)\s+\(line\s+(\d+)\)\s*$") + best: Optional[Dict[str, Any]] = None + + with open(DEBUG_LINES_FILE) as f: + for raw_line in f: + line = raw_line.rstrip("\n") + match = pattern.match(line) + if match is None: + continue + + entry_addr = int(match.group(1), 16) + diff = abs(entry_addr - target) + candidate = { + "address": f"0x{entry_addr:08X}", + "debug_path": match.group(2), + "line_number": int(match.group(3)), + "exact": diff == 0, + "diff": diff, + } + if best is None or diff < int(best["diff"]): + best = candidate + if diff == 0: + break + + if best is None: + return None, f"no entries found in {DEBUG_LINES_FILE}" + + best["repo_path"] = resolve_repo_path_from_debug_path(str(best["debug_path"])) + return best, None + + +def split_function_search_terms(function_name: str) -> Tuple[List[str], List[str]]: + qualified = function_name.split("(", 1)[0].strip() + scoped_parts = [part.strip() for part in qualified.split("::") if part.strip()] + + phrases: List[str] = [] + tokens: List[str] = [] + + if qualified: + phrases.append(qualified) + + for part in scoped_parts: + if part and part not in tokens: + tokens.append(part) + + bare = re.sub(r"[^A-Za-z0-9_:]", " ", function_name) + for token in bare.replace("::", " ").split(): + if len(token) >= 3 and token not in tokens: + tokens.append(token) + + return phrases, tokens + + +def find_related_source_files( + function_name: str, exclude_path: Optional[str] = None +) -> List[Tuple[int, str, List[str]]]: + source_root = os.path.join(root_dir, "src") + if not os.path.isdir(source_root): + return [] + + phrases, tokens = split_function_search_terms(function_name) + if not phrases and not tokens: + return [] + + class_token = tokens[0] if tokens else None + method_token = tokens[-1] if tokens else None + + candidates: List[Tuple[int, str, List[str]]] = [] + for dirpath, _, filenames in os.walk(source_root): + for filename in filenames: + if not filename.endswith((".cpp", ".hpp", ".h")): + continue + + abs_path = os.path.join(dirpath, filename) + rel_path = os.path.relpath(abs_path, root_dir) + if exclude_path and rel_path == exclude_path: + continue + path_lower = rel_path.lower() + score = 0 + reasons: List[str] = [] + is_cpp = rel_path.endswith(".cpp") + is_header = rel_path.endswith((".hpp", ".h")) + + try: + file_size = os.path.getsize(abs_path) + except OSError: + continue + + if is_cpp: + score += 35 + reasons.append("implementation file") + elif is_header: + score += 5 + + if file_size == 0: + score -= 140 + reasons.append("empty file") + + if "/generated/" in path_lower: + score -= 40 + + if class_token and class_token.lower() in path_lower: + score += 90 + reasons.append(f"path mentions {class_token}") + if method_token and method_token.lower() in path_lower and method_token != class_token: + score += 30 + reasons.append(f"path mentions {method_token}") + + basename = os.path.splitext(os.path.basename(rel_path))[0] + if class_token and basename.lower() == class_token.lower(): + score += 90 if is_cpp else 45 + reasons.append(f"basename matches {class_token}") + + try: + with open(abs_path, errors="ignore") as f: + text = f.read() + except OSError: + continue + + if class_token and method_token: + qualified = f"{class_token}::{method_token}" + if qualified in text: + score += 320 + reasons.append(f"contains {qualified}") + + for phrase in phrases: + if phrase and phrase in text: + score += 220 + reasons.append(f"contains {phrase}") + break + + if class_token: + if re.search(rf"\b(class|struct)\s+{re.escape(class_token)}\b", text): + score += 140 + reasons.append(f"declares {class_token}") + if f"{class_token}::" in text: + score += 80 + reasons.append(f"mentions {class_token}::") + + if method_token and re.search(rf"\b{re.escape(method_token)}\s*\(", text): + score += 35 + reasons.append(f"mentions {method_token}(") + + if score > 0: + deduped_reasons = [] + for reason in reasons: + if reason not in deduped_reasons: + deduped_reasons.append(reason) + candidates.append((score, rel_path, deduped_reasons[:3])) + + candidates.sort(key=lambda item: (-item[0], item[1])) + return candidates[:RELATED_SOURCE_LIMIT] + + +def format_related_source_files( + function_name: str, + source_path: Optional[str], + gc_addr: Optional[str] = None, + debug_hint: Optional[Dict[str, Any]] = None, + debug_err: Optional[str] = None, + brief: bool = False, +) -> str: + lines = [] + if source_path: + full_source_path = os.path.join(root_dir, source_path) + if not os.path.exists(full_source_path): + lines.append(f"Primary metadata source is missing: {source_path}") + elif os.path.getsize(full_source_path) == 0: + lines.append(f"Primary metadata source is empty: {source_path}") + else: + lines.append(f"Primary metadata source was not useful: {source_path}") + lines.append("") + + if debug_hint is None and debug_err is None and gc_addr: + debug_hint, debug_err = lookup_debug_line_source(gc_addr) + + if gc_addr: + if debug_hint is not None: + display_path = str(debug_hint.get("repo_path") or debug_hint.get("debug_path")) + suffix = "" + repo_path = debug_hint.get("repo_path") + if repo_path: + full_repo_path = os.path.join(root_dir, str(repo_path)) + if os.path.exists(full_repo_path) and os.path.getsize(full_repo_path) == 0: + suffix = ", file is currently empty in repo" + if debug_hint.get("exact"): + relation = f"exact address match for {format_hex_address(gc_addr)}" + else: + relation = ( + f"closest debug line to {format_hex_address(gc_addr)} " + f"(off by {debug_hint['diff']} / 0x{int(debug_hint['diff']):X})" + ) + lines.append("GC debug line mapping suggests:") + lines.append( + f"- {display_path}:{debug_hint['line_number']} ({relation}{suffix})" + ) + lines.append("") + elif debug_err: + lines.append(f"GC debug line mapping unavailable: {debug_err}") + lines.append("") + + hints = find_related_source_files(function_name, exclude_path=source_path) + if not hints: + lines.append(f"No related source files found for {function_name}.") + return "\n".join(lines) + + lines.append(f"Likely related files for {function_name}:") + shown_hints = hints[:BRIEF_RELATED_SOURCE_LIMIT] if brief else hints + for score, rel_path, reasons in shown_hints: + lines.append(f"- {rel_path} ({', '.join(reasons)})") + if brief and len(hints) > len(shown_hints): + omitted = len(hints) - len(shown_hints) + lines.append( + f"- ... {omitted} more omitted in brief mode (rerun without --brief)" + ) + return "\n".join(lines) + + def strip_ansi(text: str) -> str: """Remove ANSI escape codes from text.""" return re.sub(r"\x1b\[[0-9;]*m", "", text) @@ -327,6 +823,92 @@ def print_ghidra_decompilation( ) +def print_gc_dwarf_lookup( + function: str, mangled_function: str, lookup_mode: str = "full" +) -> None: + addr = lookup_symbol_address(GC_SYMBOLS_FILE, mangled_function) + + lookup_queries = [] + if addr: + lookup_queries.append(f"0x{addr}") + lookup_queries.append(function) + + seen_queries = set() + for query in lookup_queries: + if query in seen_queries: + continue + seen_queries.add(query) + dwarf_text, err = lookup_function_dwarf(query) + if dwarf_text is not None: + title = "GOWE69: DWARF Function" + if query.startswith("0x"): + title += f" ({query})" + if lookup_mode == "signature": + title += " (signature)" + dwarf_text = compact_dwarf_function_text(dwarf_text) + print_section(title, dwarf_text) + return + + if addr: + print( + f"\nDWARF lookup failed for {function} / 0x{addr}: {err}", + file=sys.stderr, + ) + else: + print(f"\nDWARF lookup failed for {function}: {err}", file=sys.stderr) + + +def format_suggested_commands( + unit_name: str, + function_query: str, + symbol_name: str, + left_sym: Optional[Dict[str, Any]], + right_sym: Optional[Dict[str, Any]], + brief: bool = False, +) -> str: + commands = [] + + if left_sym is not None and right_sym is None: + commands.extend( + [ + f"python tools/decomp-workflow.py function -u {unit_name} -f '{function_query}' --lookup-mode full", + f"python tools/decomp-workflow.py function -u {unit_name} -f '{function_query}' --ghidra-version gc", + f"python tools/decomp-workflow.py unit -u {unit_name} --search '{function_query}' --limit 20", + ] + ) + elif left_sym is not None and right_sym is not None: + commands.extend( + [ + f"python tools/decomp-workflow.py diff -u {unit_name} -d '{function_query}' -C 8", + f"python tools/decomp-workflow.py function -u {unit_name} -f '{function_query}' --lookup-mode full", + f"python tools/decomp-workflow.py function -u {unit_name} -f '{function_query}' --ghidra-version gc", + ] + ) + elif left_sym is None and right_sym is not None: + commands.extend( + [ + f"python tools/decomp-workflow.py function -u {unit_name} -f '{function_query}' --lookup-mode full", + f"python tools/find-symbol.py '{symbol_name}'", + f"python tools/decomp-workflow.py unit -u {unit_name} --search '{function_query}' --limit 20", + ] + ) + else: + commands.extend( + [ + f"python tools/decomp-workflow.py unit -u {unit_name} --search '{function_query}' --limit 20", + f"python tools/decomp-workflow.py diff -u {unit_name} --search '{function_query}' --limit 20", + ] + ) + + shown_commands = commands[:BRIEF_SUGGESTED_COMMAND_LIMIT] if brief else commands + lines = [f"- {command}" for command in shown_commands] + if brief and len(commands) > len(shown_commands): + lines.append( + f"- ... {len(commands) - len(shown_commands)} more omitted in brief mode" + ) + return "\n".join(lines) + + def main(): parser = argparse.ArgumentParser( description="Gather context for decomp function matching" @@ -341,6 +923,26 @@ def main(): parser.add_argument( "--no-ghidra", action="store_true", help="Skip Ghidra decompile" ) + parser.add_argument( + "--ghidra-version", + choices=["both", "gc", "ps2"], + default="both", + help="Choose which Ghidra decompile(s) to show (default: both)", + ) + parser.add_argument( + "--no-lookup", action="store_true", help="Skip GC DWARF function lookup" + ) + parser.add_argument( + "--lookup-mode", + choices=["full", "signature"], + default="full", + help="Choose how much GC DWARF function detail to show (default: full)", + ) + parser.add_argument( + "--brief", + action="store_true", + help="Trim helper sections like related-source hints and suggested commands", + ) parser.add_argument( "--ghidra-check", action="store_true", @@ -382,26 +984,65 @@ def main(): if diff_data: left_sym, right_sym = find_symbol_in_diff(diff_data, args.function) + context_name = args.function + gc_addr = None + if left_sym or right_sym: + sym = left_sym if left_sym is not None else right_sym + assert sym is not None + context_name = sym.get("demangled_name", sym.get("name", args.function)) + gc_addr = lookup_symbol_address(GC_SYMBOLS_FILE, sym.get("name", "")) + debug_hint = None + debug_hint_err = None + if gc_addr: + debug_hint, debug_hint_err = lookup_debug_line_source(gc_addr) + # === Source File (scoped to function if line info available) === - if not args.no_source and source_path: - excerpt = extract_source_for_function(source_path, right_sym) - if excerpt is not None: - label = "Source" - if right_sym and right_sym.get("instructions"): - # Check if we actually got line info - has_lines = any( - inst.get("instruction", {}).get("line_number") is not None - for inst in right_sym.get("instructions", []) - ) - if has_lines: - label = "Source (function excerpt)" - else: - label = f"Source (full file — no line info): {source_path}" - print_section(label, excerpt) - else: - print( - f"\nSource file not found: {os.path.join(root_dir, source_path)}", - file=sys.stderr, + source_was_useful = False + if not args.no_source: + if source_path: + excerpt = extract_source_for_function(source_path, right_sym) + if excerpt is not None and excerpt.strip(): + label = "Source" + if right_sym and right_sym.get("instructions"): + # Check if we actually got line info + has_lines = any( + inst.get("instruction", {}).get("line_number") is not None + for inst in right_sym.get("instructions", []) + ) + if has_lines: + label = "Source (function excerpt)" + else: + label = f"Source (full file — no line info): {source_path}" + print_section(label, excerpt) + source_was_useful = True + + if ( + not source_was_useful + and debug_hint is not None + and debug_hint.get("repo_path") is not None + ): + fallback_source = str(debug_hint["repo_path"]) + fallback_line = int(debug_hint.get("line_number", 0)) + fallback_excerpt = extract_source_from_debug_hint( + fallback_source, + context_name, + fallback_line, + ) + if fallback_excerpt is not None and fallback_excerpt.strip(): + print_section("Source (GC debug-line fallback)", fallback_excerpt) + source_was_useful = True + + if not source_was_useful: + print_section( + "Related Source Files", + format_related_source_files( + context_name, + source_path, + gc_addr=gc_addr, + debug_hint=debug_hint, + debug_err=debug_hint_err, + brief=args.brief, + ), ) # === objdiff Status === @@ -418,10 +1059,11 @@ def main(): status_lines.append(f"Function: {name}") status_lines.append(f"Mangled: {mangled}") status_lines.append(f"Size: {size} bytes") - if mp is not None: + status_lines.append( + f"Status: {describe_symbol_presence(left_sym, right_sym)}" + ) + if left_sym and right_sym and mp is not None: status_lines.append(f"Match: {mp:.1f}%") - else: - status_lines.append("Match: N/A (not in both sides)") # Quick diff summary if left_sym and right_sym: @@ -437,8 +1079,29 @@ def main(): print_section("objdiff Status", "\n".join(status_lines)) - # Run decomp-diff.py for the actual diff if not 100% - if mp is not None and mp < 100.0: + print_section( + "Suggested Next Commands", + format_suggested_commands( + args.unit, args.function, name, left_sym, right_sym, brief=args.brief + ), + ) + + if not source_was_useful and args.no_source: + print_section( + "Related Source Files", + format_related_source_files( + name, + source_path, + gc_addr=gc_addr, + debug_hint=debug_hint, + debug_err=debug_hint_err, + brief=args.brief, + ), + ) + + # Run decomp-diff.py for the actual diff unless the function is already fully matched. + show_diff = not (left_sym and right_sym and mp is not None and mp >= 100.0) + if show_diff: diff_cmd = [ sys.executable, os.path.join(script_dir, "decomp-diff.py"), @@ -475,21 +1138,31 @@ def main(): print(f"\nFailed to run objdiff for {args.unit}", file=sys.stderr) # === Ghidra Decompile === + if not args.no_lookup and (left_sym or right_sym): + sym = left_sym if left_sym is not None else right_sym + assert sym is not None + name = sym.get("demangled_name", args.function) + mangled = sym.get("name", "") + + print_gc_dwarf_lookup(name, mangled, lookup_mode=args.lookup_mode) + if not args.no_ghidra and (left_sym or right_sym): sym = left_sym if left_sym is not None else right_sym assert sym is not None mangled = sym.get("name", "") - print_ghidra_decompilation( - "GOWE69", GC_SYMBOLS_FILE, GC_GHIDRA_PROGRAM, args.function, mangled - ) - print_ghidra_decompilation( - "SLES-53558-A124", - PS2_SYMBOLS_FILE, - PS2_GHIDRA_PROGRAM, - args.function, - mangled, - ) + if args.ghidra_version in ("both", "gc"): + print_ghidra_decompilation( + "GOWE69", GC_SYMBOLS_FILE, GC_GHIDRA_PROGRAM, args.function, mangled + ) + if args.ghidra_version in ("both", "ps2"): + print_ghidra_decompilation( + "SLES-53558-A124", + PS2_SYMBOLS_FILE, + PS2_GHIDRA_PROGRAM, + args.function, + mangled, + ) if __name__ == "__main__": diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index 034397155..fda358711 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -78,6 +78,24 @@ def fuzzy_match(pattern: str, name: str) -> bool: return pattern.lower() in name.lower() +def describe_pair_status( + left_sym: Optional[Dict[str, Any]], right_sym: Optional[Dict[str, Any]] +) -> str: + if left_sym is not None and right_sym is None: + return "missing in decomp" + if left_sym is None and right_sym is not None: + return "extra in decomp" + + sym = left_sym if left_sym is not None else right_sym + if sym is None: + return "not found" + + mp = sym.get("match_percent") + if mp is not None: + return f"{mp:.1f}% match" + return "paired" + + def build_overview(data: Dict[str, Any], args) -> None: """Print overview of all symbols in a unit.""" left_syms = data.get("left", {}).get("symbols", []) @@ -147,6 +165,9 @@ def build_overview(data: Dict[str, Any], args) -> None: if args.search: rows = [r for r in rows if fuzzy_match(args.search, r[5])] + if args.limit is not None: + rows = rows[: args.limit] + if not rows: print("No symbols match the given filters.") return @@ -280,8 +301,8 @@ def build_diff(data: Dict[str, Any], symbol_name: str, args) -> None: right_insts = (right_sym or {}).get("instructions", []) n_insts = max(len(left_insts), len(right_insts)) - mp_str = f"{mp:.1f}%" if mp is not None else "N/A" - print(f"{display_name}: {mp_str} match ({size}B, {n_insts} instructions)") + status_str = describe_pair_status(left_sym, right_sym) + print(f"{display_name}: {status_str} ({size}B, {n_insts} instructions)") print() if n_insts == 0: @@ -436,6 +457,11 @@ def main(): ) parser.add_argument("--section", help="Filter by section name (e.g. .text)") parser.add_argument("--search", help="Fuzzy search on demangled symbol name") + parser.add_argument( + "--limit", + type=int, + help="Limit overview output to the first N matching rows", + ) # Diff options parser.add_argument( diff --git a/tools/decomp-status.py b/tools/decomp-status.py index 69917e7dd..8741b9675 100644 --- a/tools/decomp-status.py +++ b/tools/decomp-status.py @@ -17,7 +17,7 @@ import json import os import sys -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from _common import ROOT_DIR, ToolError, fail, load_objdiff_config, run_objdiff_json root_dir = ROOT_DIR @@ -30,12 +30,12 @@ def load_project_config() -> Dict[str, Any]: return load_objdiff_config() -def run_objdiff(unit_name: str) -> Optional[Dict[str, Any]]: +def run_objdiff(unit_name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]: """Run objdiff-cli diff for a unit and return parsed JSON.""" try: - return run_objdiff_json(OBJDIFF_CLI, unit_name, root_dir=root_dir) - except ToolError: - return None + return run_objdiff_json(OBJDIFF_CLI, unit_name, root_dir=root_dir), None + except ToolError as e: + return None, str(e) def analyze_unit(diff_data: Dict[str, Any]) -> Dict[str, Any]: @@ -156,13 +156,14 @@ def main(): entry["status"] = "complete" entry["text_match"] = 100.0 elif has_target and has_base: - diff_data = run_objdiff(name) + diff_data, error_message = run_objdiff(name) if diff_data: stats = analyze_unit(diff_data) entry.update(stats) entry["status"] = "incomplete" else: entry["status"] = "error" + entry["error_message"] = error_message elif has_target and not has_base: entry["status"] = "no_source" else: @@ -217,6 +218,11 @@ def main(): print(f" {display_name:<50s} no source file") elif status == "error": print(f" {display_name:<50s} error running objdiff") + if args.unit: + error_message = entry.get("error_message") + if error_message: + for line in error_message.splitlines(): + print(f" {line}") # Add complete units to totals complete_count = sum(1 for e in entries if e.get("status") == "complete") diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index 8c6da989f..6b1e92e10 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -9,14 +9,20 @@ python tools/decomp-workflow.py health python tools/decomp-workflow.py health --smoke-build-unit main/Speed/Indep/SourceLists/zCamera python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --brief + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --ghidra-version gc + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --lookup-mode full + python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-lookup python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-source python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zCamera """ import argparse +import re import os import subprocess import sys +import tempfile from typing import List, Optional, Sequence, Tuple from _common import BUILD_NINJA, OBJDIFF_JSON, ROOT_DIR, ToolError, ensure_exists @@ -24,8 +30,16 @@ SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) TOOLS_DIR = os.path.join(ROOT_DIR, "tools") PS2_TYPES = os.path.join(ROOT_DIR, "symbols", "PS2", "PS2_types.nothpp") +DTK = os.path.join(ROOT_DIR, "build", "tools", "dtk") +GC_SYMBOLS = os.path.join(ROOT_DIR, "config", "GOWE69", "symbols.txt") +PS2_SYMBOLS = os.path.join(ROOT_DIR, "config", "SLES-53558-A124", "symbols.txt") +GC_DWARF = os.path.join(ROOT_DIR, "symbols", "mw_dwarfdump.nothpp") +DEBUG_LINES = os.path.join(ROOT_DIR, "symbols", "debug_lines.txt") DEFAULT_SMOKE_UNIT = "main/Speed/Indep/SourceLists/zCamera" +DEBUG_SYMBOL_PROBE_MANGLED = "UpdateAll__6Cameraf" +DEBUG_SYMBOL_PROBE_DEMANGLED = "Camera::UpdateAll(float)" +DEBUG_SYMBOL_PROBE_GC_ADDR = "0x80065A84" SHARED_ASSET_REQUIREMENTS = [ ("NFSMWRELEASE.ELF", "GameCube ELF"), @@ -124,12 +138,57 @@ def maybe_remove(path: Optional[str]) -> None: print(f"Warning: failed to remove temp object {path}: {e}", file=sys.stderr) +def dtk_dwarf_dump(obj_path: str) -> str: + fd, output_path = tempfile.mkstemp(prefix="nfsmw_dtk_", suffix=".nothpp") + os.close(fd) + maybe_remove(output_path) + + result = subprocess.run( + [DTK, "dwarf", "dump", obj_path, "-o", output_path], + cwd=ROOT_DIR, + text=True, + capture_output=True, + ) + if result.returncode != 0: + maybe_remove(output_path) + raise WorkflowError( + format_failure([DTK, "dwarf", "dump", obj_path, "-o", output_path], result.returncode, result.stdout, result.stderr) + ) + + tool_output = "\n".join( + part.strip() for part in [result.stdout, result.stderr] if part.strip() + ) + if "ERROR " in tool_output or tool_output.startswith("ERROR"): + maybe_remove(output_path) + raise WorkflowError(f"dtk reported an error while dumping DWARF:\n{tool_output}") + + if not os.path.exists(output_path): + raise WorkflowError("dtk dwarf dump succeeded but did not write an output file") + + return output_path + + def describe_path(path: str) -> str: if os.path.islink(path): return "shared-symlink" return "present" +def lookup_symbol_address(symbols_file: str, mangled_name: str) -> Optional[str]: + if not os.path.exists(symbols_file): + return None + + pattern = re.compile( + r"^" + re.escape(mangled_name) + r"\s*=\s*(?:\.(\w+):)?0x([0-9A-Fa-f]+)" + ) + with open(symbols_file) as f: + for line in f: + match = pattern.match(line.strip()) + if match: + return "0x" + match.group(2) + return None + + def command_health(args: argparse.Namespace) -> None: failures = 0 @@ -170,11 +229,56 @@ def report(ok: bool, label: str, detail: str) -> None: except WorkflowError as e: report(False, "ghidra", str(e)) + print_section("Debug Symbol Checks") + try: + gc_addr = lookup_symbol_address(GC_SYMBOLS, DEBUG_SYMBOL_PROBE_MANGLED) + report( + gc_addr == DEBUG_SYMBOL_PROBE_GC_ADDR, + "gc-symbols", + gc_addr or f"missing ({DEBUG_SYMBOL_PROBE_MANGLED})", + ) + except Exception as e: + report(False, "gc-symbols", str(e)) + + try: + ps2_addr = lookup_symbol_address(PS2_SYMBOLS, DEBUG_SYMBOL_PROBE_MANGLED) + report( + ps2_addr is not None, + "ps2-symbols", + ps2_addr or f"missing ({DEBUG_SYMBOL_PROBE_MANGLED})", + ) + except Exception as e: + report(False, "ps2-symbols", str(e)) + + try: + run_capture( + python_tool( + "lookup.py", + "--file", + GC_DWARF, + "function", + DEBUG_SYMBOL_PROBE_DEMANGLED, + ) + ) + report(True, "gc-dwarf", f"{DEBUG_SYMBOL_PROBE_DEMANGLED} found") + except WorkflowError as e: + report(False, "gc-dwarf", str(e)) + try: run_capture(python_tool("lookup.py", "--file", PS2_TYPES, "struct", "Camera")) - report(True, "ps2-lookup", "Camera found in PS2 dump") + report(True, "ps2-types", "Camera found in PS2 dump") except WorkflowError as e: - report(False, "ps2-lookup", str(e)) + report(False, "ps2-types", str(e)) + + try: + result = run_capture( + python_tool("line_lookup.py", DEBUG_LINES, DEBUG_SYMBOL_PROBE_GC_ADDR) + ) + ok = "Exact match found" in result.stdout and "Camera.cpp" in result.stdout + detail = "Camera.cpp exact match" if ok else "unexpected line lookup output" + report(ok, "debug-lines", detail) + except WorkflowError as e: + report(False, "debug-lines", str(e)) if args.smoke_build_unit: print_section("Build Smoke Test") @@ -187,6 +291,20 @@ def report(ok: bool, label: str, detail: str) -> None: finally: maybe_remove(temp_obj) + if args.smoke_dtk: + print_section("DTK Smoke Test") + temp_obj = None + dump_path = None + try: + temp_obj = build_temp_obj(args.smoke_dtk) + dump_path = dtk_dwarf_dump(temp_obj) + report(True, "dtk", dump_path) + except WorkflowError as e: + report(False, "dtk", str(e)) + finally: + maybe_remove(dump_path) + maybe_remove(temp_obj) + if failures: raise WorkflowError(f"Health check failed with {failures} issue(s)") @@ -215,8 +333,16 @@ def command_function(args: argparse.Namespace) -> None: cmd = python_tool("decomp-context.py", "-u", args.unit, "-f", args.function) if args.no_source: cmd.append("--no-source") + if args.no_lookup: + cmd.append("--no-lookup") + else: + cmd.extend(["--lookup-mode", args.lookup_mode]) if args.no_ghidra: cmd.append("--no-ghidra") + else: + cmd.extend(["--ghidra-version", args.ghidra_version]) + if args.brief: + cmd.append("--brief") if temp_obj: cmd.extend(["--base-obj", temp_obj]) run_stream(cmd) @@ -240,6 +366,10 @@ def command_unit(args: argparse.Namespace) -> None: common_args: List[str] = ["-u", args.unit, "-t", "function"] if temp_obj: common_args.extend(["--base-obj", temp_obj]) + if args.search: + common_args.extend(["--search", args.search]) + if args.limit is not None: + common_args.extend(["--limit", str(args.limit)]) print_section("Missing Functions") run_stream(python_tool("decomp-diff.py", *common_args, "-s", "missing")) @@ -283,6 +413,8 @@ def command_diff(args: argparse.Namespace) -> None: cmd.extend(["--section", args.section]) if args.search: cmd.extend(["--search", args.search]) + if args.limit is not None: + cmd.extend(["--limit", str(args.limit)]) if args.context is not None: cmd.extend(["-C", str(args.context)]) if args.range: @@ -319,6 +451,16 @@ def build_parser() -> argparse.ArgumentParser: f"{DEFAULT_SMOKE_UNIT}" ), ) + health.add_argument( + "--smoke-dtk", + metavar="UNIT", + nargs="?", + const=DEFAULT_SMOKE_UNIT, + help=( + "Also run a dtk dwarf dump smoke test. If UNIT is omitted, uses " + f"{DEFAULT_SMOKE_UNIT}" + ), + ) health.set_defaults(func=command_health) function = subparsers.add_parser( @@ -351,6 +493,28 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Pass through to decomp-context.py", ) + function.add_argument( + "--ghidra-version", + choices=["both", "gc", "ps2"], + default="both", + help="Pass through to decomp-context.py (default: both)", + ) + function.add_argument( + "--no-lookup", + action="store_true", + help="Pass through to decomp-context.py", + ) + function.add_argument( + "--lookup-mode", + choices=["signature", "full"], + default="signature", + help="Pass through to decomp-context.py (default: signature)", + ) + function.add_argument( + "--brief", + action="store_true", + help="Trim helper sections like related-source hints and suggested commands", + ) function.set_defaults(func=command_function) unit = subparsers.add_parser( @@ -372,6 +536,12 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Keep the auto-built temp object instead of deleting it afterwards", ) + unit.add_argument("--search", help="Fuzzy search on demangled symbol name") + unit.add_argument( + "--limit", + type=int, + help="Limit each symbol list to the first N matching rows", + ) unit.set_defaults(func=command_unit) build = subparsers.add_parser( @@ -405,6 +575,11 @@ def build_parser() -> argparse.ArgumentParser: ) diff.add_argument("--section", help="Filter by section name") diff.add_argument("--search", help="Fuzzy search on demangled symbol name") + diff.add_argument( + "--limit", + type=int, + help="Limit overview output to the first N matching rows", + ) diff.add_argument( "-C", "--context", From 8c384ccfadecb180350214cc125841cfd6363e64 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 12 Mar 2026 21:06:33 +0100 Subject: [PATCH 16/71] Improve decomp workflow tooling and agent guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 35 +++- .github/skills/implement/SKILL.md | 26 ++- .github/skills/refiner/SKILL.md | 31 ++-- AGENTS.md | 32 +++- README.md | 22 ++- tools/_common.py | 166 ++++++++++++++++- tools/decomp-context.py | 173 +++++++++++++++++- tools/decomp-diff.py | 153 ++++++---------- tools/decomp-status.py | 101 +++++++++-- tools/decomp-workflow.py | 291 +++++++++++++++++++++++++++++- tools/project.py | 9 +- 11 files changed, 875 insertions(+), 164 deletions(-) diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index c6ebe9e22..c4366f2a6 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -55,14 +55,25 @@ Determine the file path (e.g. `src/Speed/Indep/SourceLists/zWorld2`). The game u Preferred shortcut: ```sh +python tools/decomp-workflow.py next --unit main/Path/To/TU --limit 10 python tools/decomp-workflow.py unit -u main/Path/To/TU --limit 20 ``` -If you need the raw tools instead of the wrapper, run `decomp-status.py` and -`decomp-diff.py` directly against the shared build output. +Use `next` first when you want the wrapper to rank the most useful targets instead of +following raw objdiff order. `--strategy balanced` is the default and is usually the best +starting point because it now favors large remaining gains and penalizes near-finished +cleanup work. Use `--strategy impact` when you only care about the biggest unmatched-byte +wins. Use `--strategy quick-wins` when you want lower-match functions where the first big +chunk of progress is likely to come faster than late cleanup. + +Stay in the wrapper flow by default. Only drop to raw `decomp-status.py` / `decomp-diff.py` +when you need an option the wrapper does not expose yet. -This shows all symbols with their match status. Note the total count of missing, -nonmatching, and matching functions. +If the shared unit object is missing, the wrapper now rebuilds it automatically before +running `next --unit` / `unit`. + +If you need the raw tools instead of the wrapper, run `decomp-status.py` and +`decomp-diff.py` directly against the shared build output as a fallback, not the default. ## Phase 2: Scaffold (if needed) @@ -84,7 +95,7 @@ python tools/decomp-workflow.py unit -u main/Path/To/TU ``` If you need the raw tools, rebuild normally and then run `decomp-diff.py` -directly on the unit. +directly on the unit only as a fallback. ### 3c. Implement each function sequentially @@ -129,7 +140,8 @@ view for one function. After every few functions, re-run the full status check: ```sh -python tools/decomp-diff.py -u main/Path/To/TU +python tools/decomp-workflow.py unit -u main/Path/To/TU +python tools/decomp-workflow.py next --unit main/Path/To/TU --limit 10 ``` Review progress and decide whether to: @@ -143,16 +155,19 @@ Review progress and decide whether to: When all functions have been attempted: ```sh -# Full status -python tools/decomp-diff.py -u main/Path/To/TU +# Wrapper-first unit summary +python tools/decomp-workflow.py unit -u main/Path/To/TU -# Check for any remaining mismatches -python tools/decomp-diff.py -u main/Path/To/TU -s nonmatching +# Focused remaining mismatches +python tools/decomp-workflow.py diff -u main/Path/To/TU -s nonmatching -t function # Verify no regressions ninja changes ``` +If you need a raw full-symbol dump beyond that, use `decomp-diff.py` only as a final +fallback. + For any remaining nonmatching functions, make one final pass using the implementation or refiner workflow with all context accumulated during the session. diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 52c063e0b..a81bb47c1 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -11,6 +11,19 @@ Your goal is to decompile a specific function: writing C++ source that compiles Collect data from **all** of these sources in parallel where possible. +If the function was not already chosen for you, pick it with the ranking wrapper first: + +```sh +python tools/decomp-workflow.py next --unit main/Path/To/TU --limit 10 +python tools/decomp-workflow.py next --category game --limit 10 +``` + +Prefer low-match, high-remaining targets here. Do not default to near-finished cleanup +functions unless the user explicitly wants a cleanup/refiner pass. + +Use the wrapper flow first throughout this skill. Drop to raw `decomp-context.py` or +`decomp-diff.py` only when the wrapper is missing a specific flag or you are debugging. + ### 1a. decomp-context.py Preferred shortcut: @@ -21,6 +34,9 @@ python tools/decomp-workflow.py function -u main/Path/To/TU -f FunctionName --br python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName ``` +If the shared unit object is missing, the wrapper now rebuilds it automatically before +running `next --unit` / `function` / `diff`. + If you only need one Ghidra view, add `--ghidra-version gc` or `--ghidra-version ps2` to keep the context run faster and shorter. @@ -30,7 +46,7 @@ need the full DWARF body with locals and nested inline info. Add `--brief` when you want a shorter helper view; it trims suggested commands and related-source hints while keeping the core source/status/diff context. -Equivalent manual form: +Equivalent manual fallback: ```sh python tools/decomp-context.py -u main/Path/To/TU -f FunctionName @@ -94,7 +110,7 @@ and assembly: Utilize the dwarf information that you get from the lookup skill heavily. -Don't add any comments. +Don't add explanatory comments during implementation unless you need to document a remaining DWARF mismatch. Don't use any temporary local variables that don't exist in the dwarf. @@ -128,10 +144,10 @@ If the build fails, fix compilation errors first. ```sh # Quick status -python tools/decomp-diff.py -u main/Path/To/TU --search FunctionName +python tools/decomp-workflow.py diff -u main/Path/To/TU --search FunctionName --limit 20 # Full instruction diff -python tools/decomp-diff.py -u main/Path/To/TU -d FunctionName +python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName ``` ### Interpreting the diff @@ -168,7 +184,7 @@ Repeat the build-diff cycle until the diff shows 100% match with no `~` lines: ```sh python tools/decomp-workflow.py build -u main/Path/To/TU -python tools/decomp-diff.py -u main/Path/To/TU -d FunctionName +python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName ``` Every mismatched instruction is a signal — don't settle for "close enough". diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md index 761a92e37..82b2a2608 100644 --- a/.github/skills/refiner/SKILL.md +++ b/.github/skills/refiner/SKILL.md @@ -18,7 +18,19 @@ approaches that were tried before — instead, apply systematic lateral analysis ## Phase 1: Read the full diff without collapsing -First rebuild the unit normally, then diff: +Preferred shortcut: + +```sh +python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName --no-collapse +``` + +If the shared unit object is missing, the wrapper now rebuilds it automatically before +running `diff`. + +Stay in the wrapper flow for refiner passes unless you hit a wrapper limitation and need a +backend-only option. + +If you need the raw backend form instead of the wrapper, rebuild the unit and then run: ```sh python tools/decomp-workflow.py build -u main/Path/To/TU @@ -37,8 +49,7 @@ Read every instruction pair. Categorize each mismatch: | **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order | | **Virtual vs direct call** | `bl` vs indirect through vtable | Check const-qualifier; use `GetFoo()` vs `Foo()` | | **Inline vs outlined** | Extra call to helper vs inlined sequence | Force inline by rewriting the expression without calling the helper | -| **Missing `this->` dereference** | Wrong address in load/store | Ensure member access goes through the correct `this` pointer | -| **Loop structure** | `do/while` vs `for` vs `while` | Try all three forms; compiler emits different branch sequences | +| **Loop structure** | Guarded `do/while` from Ghidra or mismatched loop branches | Rewrite to the natural source form suggested by the control flow; in particular, a guarded `do/while` often needs to become a plain `for` loop | ## Phase 2: Systematic permutation strategies @@ -90,22 +101,18 @@ python tools/lookup.py ./symbols/Dwarf struct bMath Replace hand-rolled sequences with the correct inline call. -### 2e. Initializer list order +### 2e. Constructor initialization placement -Constructors compiled with GCC are sensitive to initializer list order. The DWARF -shows the canonical member order. If yours differs, reorder. +Only do this for constructors. Compare which members are initialized in the +initializer list versus the function body, and in what order. Initializer-list use +often stabilizes store order, but forcing every member into the initializer list can +also make the match worse. ### 2f. Cast type `static_cast` vs `static_cast` produces different assembly sequences on PPC (see `xoris` pattern in AGENTS.md). Check all casts. -### 2g. Compiler flag hint - -If none of the above resolve the mismatch, note the function address and consider -running `flag_permuter.py`. This is a last resort — only do this for a single -isolated function, not as a general strategy. - ## Phase 3: DWARF verification After any instruction match, verify the DWARF also matches. diff --git a/AGENTS.md b/AGENTS.md index 74c446bc7..b95d62190 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,6 +137,8 @@ Prefer this wrapper for routine agent-driven flows instead of manually chaining python tools/decomp-workflow.py health python tools/decomp-workflow.py health --smoke-build main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py health --smoke-dtk main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py next --category game --limit 10 +python tools/decomp-workflow.py next --unit main/Speed/Indep/SourceLists/zAnim --limit 5 python tools/decomp-workflow.py build -u main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin @@ -148,6 +150,31 @@ python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim --sea The wrapper keeps the existing tools as the source of truth. It is intended to reduce repeated command chaining and to standardize routine worktree preflight checks for agents. +`next --unit`, `function`, `unit`, and `diff` now also auto-build the unit's shared `.o` +once when that output is missing, so wrapper-first inspection works more often on +half-prepared worktrees. + +In normal agent work, use the wrapper commands first. Drop to the raw backend tools only +when you specifically need a backend-only flag, are debugging a wrapper/backend discrepancy, +or are doing a final exhaustive check that the wrapper does not expose directly. + +When you do not already have a specific target in mind, start with `next` or `unit` +instead of picking functions in raw objdiff order. `next` is the fastest way to answer +"what should I work on now?": + +- `--strategy balanced` favors functions with large remaining gains, penalizes + high-match cleanup work, de-prioritizes obvious init/setup sinkholes, and prefers + targets with usable source context. +- `--strategy impact` is the blunt "largest unmatched byte loss first" view. +- `--strategy quick-wins` biases toward low-match functions where getting the first + 40-60% tends to be much faster than squeezing a polished function from 95% to 100%. + It should not be treated as a cleanup/polish mode. + +When choosing what to work on next, bias toward low-match, high-remaining functions. +As a rule of thumb, getting a function from 0% to 80% is usually much faster and higher +leverage than pushing a function from 90% to 100%. +Leave 85%+ cleanup and refiner-style polish for deliberate cleanup passes unless the +user explicitly wants that work or the function is directly blocking something else. `function` is the preferred context-gathering entrypoint: it bundles source excerpt, objdiff status/diff, compact GC DWARF function lookup, and Ghidra output in one run. @@ -167,8 +194,9 @@ repeated manual steps for future agents. On a newly updated or unusual worktree, run `python tools/decomp-workflow.py health` first. If it reports missing generated files such as `objdiff.json` or `build.ninja`, run `python configure.py` in that worktree before using the decomp wrappers. `health` also -checks the debug-symbol side of the setup now: GC/PS2 `symbols.txt`, GC DWARF lookup, -PS2 type lookup, and the GC debug line mapping. +checks the debug-symbol side of the setup now, plus the wrapper binaries themselves: +`objdiff-cli`, `dtk`, GC/PS2 `symbols.txt`, GC DWARF lookup, PS2 type lookup, and the +GC debug line mapping. ### find-symbol.py — Check for existing definitions before declaring new types diff --git a/README.md b/README.md index b74f298fd..70bb79dde 100644 --- a/README.md +++ b/README.md @@ -87,15 +87,15 @@ sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' ``` - Extracting the binaries - - GC: Extract `NFSMWRELEASE.ELF`, copy it into `orig/GOWE69` and convert it into a DOL using the following command: + - GC: Extract `NFSMWRELEASE.ELF`, copy it into `orig/GOWE69`, and convert it into a DOL using the following command: ```sh - ./build/tools/dtk elf2dol ./orig/GOWE69/NFSMWRELEASE.elf ./orig/GOWE69/sys/main.dol + ./build/tools/dtk elf2dol ./orig/GOWE69/NFSMWRELEASE.ELF ./orig/GOWE69/sys/main.dol ``` - Xbox 360: simply rename `NfsMWEuropeGerMilestone.exe` to `NfsMWEuropeGerMilestone.xex` and copy it to `./orig/EUROPEGERMILESTONE/` - - PS2: Copy `NSF.ELF` to `./orig/SLES-53558-A124/` + - PS2: Copy `NFS.ELF` to `./orig/SLES-53558-A124/` - Sharing large assets across git worktrees @@ -108,8 +108,8 @@ sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' ``` This shares the ignored debug/tool assets under the git common directory, including - extracted `orig/*` contents, `symbols/*`, root ELF / MAP files, and downloaded - tool binaries under `build/`. It intentionally does **not** share `build.ninja`, + extracted `orig/*` contents, `symbols/*`, and downloaded tool binaries under + `build/`. It intentionally does **not** share `build.ninja`, `objdiff.json`, `compile_commands.json`, or per-worktree object outputs. After linking shared assets into a worktree, regenerate that worktree's local build @@ -160,7 +160,7 @@ For PS2 binaries the deprecated version gives nicer results. ## symbols/mw_dwarfdump.nothpp ``` -./build/tools/dtk dwarf dump ./orig/NFSMWRELEASE.ELF -o ./symbols/mw_dwarfdump.nothpp +./build/tools/dtk dwarf dump ./orig/GOWE69/NFSMWRELEASE.ELF -o ./symbols/mw_dwarfdump.nothpp ``` This is the dwarf dump of the whole GC version of the game. The `.nothpp` extension is to make sure that the IDE doesn't parse it on weak laptops. This should be your main source of information. It even shows which inlines a function calls. Namespaces only show up in generics. For regular functions and variables you can search `symbols.txt` for the right name. @@ -204,13 +204,19 @@ This file contains bChunk chunk IDs. - Run ``` - ./build/tools/dtk dwarf dump ./orig/NFSMWRELEASE.ELF -o ./symbols/mw_dwarfdump.nothpp + ./build/tools/dtk dwarf dump ./orig/GOWE69/NFSMWRELEASE.ELF -o ./symbols/mw_dwarfdump.nothpp python ./tools/split_dwarf_info.py ./symbols/mw_dwarfdump.nothpp ./symbols/Dwarf ``` - Set up the project and Ghidra as described above (take the Ghidra repo from the decomp.dev server, you'll have to request access). -- In Ghidra, checkout `mw/GOWE69/NFSMWRELEASE.ELF` and `mw/SLES-53558/NFS.ELF.fixed` and copy them both into the root of the project. Rename `NFS.ELF.fixed` to `NFS.ELF`. +- Import the ELF files from `orig/` into the Ghidra project so the program names stay + `NFSMWRELEASE.ELF` and `NFS.ELF`: + + ```sh + ghidra import ./orig/GOWE69/NFSMWRELEASE.ELF + ghidra import ./orig/SLES-53558-A124/NFS.ELF + ``` - Download [ghidra-cli](https://github.com/akiselev/ghidra-cli) and put it into your path. diff --git a/tools/_common.py b/tools/_common.py index 976fcd649..db992bd8b 100644 --- a/tools/_common.py +++ b/tools/_common.py @@ -7,13 +7,19 @@ import subprocess import sys import tempfile -from typing import Any, Dict, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) BUILD_NINJA = os.path.join(ROOT_DIR, "build.ninja") OBJDIFF_JSON = os.path.join(ROOT_DIR, "objdiff.json") +OBJDIFF_DEFAULT_CONFIG_ARGS = [ + "-c", + "functionRelocDiffs=none", + "-c", + "ppc.calculatePoolRelocations=false", +] class ToolError(RuntimeError): @@ -112,6 +118,144 @@ def apply_base_obj_override( return found +def classify_objdiff_symbol(sym: Dict[str, Any]) -> str: + """Classify an objdiff symbol as 'function', 'object', or 'section'.""" + kind = sym.get("kind", "") + if kind == "SYMBOL_FUNCTION": + return "function" + if kind == "SYMBOL_OBJECT": + return "object" + if kind == "SYMBOL_SECTION": + return "section" + if "instructions" in sym: + return "function" + if "data_diff" in sym: + return "object" + return "unknown" + + +def objdiff_symbol_section(sym: Dict[str, Any], sections: List[Dict[str, Any]]) -> str: + """Determine which section a symbol belongs to.""" + name = sym.get("name", "") + if name.startswith("[."): + return name[1:].split("-")[0].rstrip("]") + if classify_objdiff_symbol(sym) == "function": + return ".text" + for sec in sections: + kind = sec.get("kind", "") + if kind in ("SECTION_DATA", "SECTION_BSS"): + return sec["name"] + return ".data" + + +def estimate_unmatched_bytes( + size: int, match_percent: Optional[float], status: str +) -> int: + """Estimate remaining unmatched bytes for a symbol.""" + size = max(int(size), 0) + if size == 0: + return 0 + if status in ("missing", "extra", "no_target", "no_source"): + return size + if status in ("match", "matching", "complete"): + return 0 + if match_percent is None: + return size + + clamped = max(0.0, min(float(match_percent), 100.0)) + if clamped >= 100.0: + return 0 + + unmatched = int(round(size * (100.0 - clamped) / 100.0)) + unmatched = max(1, unmatched) + return min(size, unmatched) + + +def build_objdiff_symbol_rows(diff_data: Dict[str, Any]) -> List[Dict[str, Any]]: + """Build normalized overview rows from objdiff JSON for both left and right symbols.""" + left_syms = diff_data.get("left", {}).get("symbols", []) + right_syms = diff_data.get("right", {}).get("symbols", []) + left_sections = diff_data.get("left", {}).get("sections", []) + right_sections = diff_data.get("right", {}).get("sections", []) + + rows: List[Dict[str, Any]] = [] + + for sym in left_syms: + sym_type = classify_objdiff_symbol(sym) + if sym_type in ("section", "unknown"): + continue + + size = int(sym.get("size", "0")) + if size == 0: + continue + + name = sym.get("demangled_name", sym.get("name", "?")) + section = objdiff_symbol_section(sym, left_sections) + target_symbol = sym.get("target_symbol") + match_percent = sym.get("match_percent") + + if target_symbol is None: + status = "missing" + elif match_percent is not None and match_percent >= 100.0: + status = "match" + elif match_percent is not None: + status = "nonmatching" + else: + status = "missing" + + rows.append( + { + "status": status, + "match_percent": match_percent, + "size": size, + "unmatched_bytes_est": estimate_unmatched_bytes( + size, match_percent, status + ), + "section": section, + "type": sym_type, + "name": name, + "symbol_name": sym.get("name", "?"), + "side": "left", + "left_symbol": sym, + "right_symbol": right_syms[target_symbol] + if target_symbol is not None and target_symbol < len(right_syms) + else None, + } + ) + + for sym in right_syms: + if sym.get("target_symbol") is not None: + continue + + sym_type = classify_objdiff_symbol(sym) + if sym_type in ("section", "unknown"): + continue + + size = int(sym.get("size", "0")) + if size == 0: + continue + + name = sym.get("demangled_name", sym.get("name", "?")) + section = objdiff_symbol_section(sym, right_sections) + rows.append( + { + "status": "extra", + "match_percent": None, + "size": size, + "unmatched_bytes_est": estimate_unmatched_bytes(size, None, "extra"), + "section": section, + "type": sym_type, + "name": name, + "symbol_name": sym.get("name", "?"), + "side": "right", + "left_symbol": None, + "right_symbol": sym, + } + ) + + return rows + + def run_objdiff_json( objdiff_cli: str, unit_name: str, @@ -123,6 +267,7 @@ def run_objdiff_json( ensure_project_prereqs() cmd = [objdiff_cli, "diff"] + cmd.extend(OBJDIFF_DEFAULT_CONFIG_ARGS) if extra_args: cmd.extend(extra_args) cmd.extend(["-u", unit_name, "-o", "-", "--format", "json"]) @@ -141,12 +286,19 @@ def run_objdiff_json( cwd = tmpdir try: - result = subprocess.run( - cmd, - cwd=cwd, - text=True, - capture_output=True, - ) + try: + result = subprocess.run( + cmd, + cwd=cwd, + text=True, + capture_output=True, + ) + except FileNotFoundError: + raise ToolError( + f"Missing objdiff-cli: {objdiff_cli}\n" + "Hint: ensure build/tools is populated in this worktree " + "(for example via the shared worktree assets setup)." + ) if result.returncode != 0: stderr = result.stderr hint_lines = [] diff --git a/tools/decomp-context.py b/tools/decomp-context.py index 637c8f35f..32a35fcae 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -24,7 +24,14 @@ import subprocess import sys from typing import Any, Dict, List, Optional, Tuple -from _common import ROOT_DIR, ToolError, fail, load_objdiff_config, run_objdiff_json +from _common import ( + ROOT_DIR, + ToolError, + build_objdiff_symbol_rows, + fail, + load_objdiff_config, + run_objdiff_json, +) script_dir = os.path.dirname(os.path.realpath(__file__)) root_dir = ROOT_DIR @@ -43,6 +50,10 @@ RELATED_SOURCE_LIMIT = 8 BRIEF_RELATED_SOURCE_LIMIT = 3 BRIEF_SUGGESTED_COMMAND_LIMIT = 2 +LOW_UNMATCHED_HINT_THRESHOLD = 192 +HIGH_MATCH_HINT_THRESHOLD = 85.0 +LARGER_TARGET_RATIO = 3 +LARGER_TARGET_MIN_BYTES = 192 def load_project_config() -> Dict[str, Any]: @@ -338,6 +349,42 @@ def extract_source_for_function( return header + "".join(excerpt) +def source_excerpt_is_useful(source_path: str, excerpt: str) -> bool: + lines = [line.strip() for line in excerpt.splitlines()] + content_lines = [ + line + for line in lines + if line and not line.startswith("// Lines ") + ] + if not content_lines: + return False + + include_like = sum( + 1 + for line in content_lines + if line.startswith("#include") + or line.startswith("#pragma") + or line.startswith("#if") + or line.startswith("#endif") + or line.startswith("#define") + ) + + source_list_path = source_path.replace("\\", "/") + if "SourceLists/" in source_list_path: + if include_like == len(content_lines): + return False + if include_like >= max(2, len(content_lines) - 1): + return False + + useful_tokens = ("{", "}", "if ", "for ", "while ", "::", "return ", "=") + if include_like == len(content_lines) and not any( + token in excerpt for token in useful_tokens + ): + return False + + return True + + def extract_source_around_line( source_path: str, line_number: int, context_lines: int = SOURCE_CONTEXT_LINES ) -> Optional[str]: @@ -904,6 +951,114 @@ def format_suggested_commands( return "\n".join(lines) +def unit_progress_category(unit: Dict[str, Any]) -> Optional[str]: + categories = unit.get("metadata", {}).get("progress_categories", []) + if not categories: + return None + if len(categories) > 1: + return str(categories[1]) + return str(categories[0]) + + +def format_priority_guidance( + unit_name: str, + unit: Dict[str, Any], + diff_data: Optional[Dict[str, Any]], + current_symbol_name: Optional[str], + brief: bool = False, +) -> Optional[str]: + if diff_data is None or current_symbol_name is None: + return None + + function_rows = [ + row + for row in build_objdiff_symbol_rows(diff_data) + if row["side"] == "left" + and row["type"] == "function" + and row["status"] in ("missing", "nonmatching") + and row["unmatched_bytes_est"] > 0 + ] + if not function_rows: + return None + + function_rows.sort( + key=lambda row: (-row["unmatched_bytes_est"], -row["size"], row["name"].lower()) + ) + + current_row = None + for row in function_rows: + if row["symbol_name"] == current_symbol_name: + current_row = row + break + if current_row is None: + return None + + current_unmatched = int(current_row["unmatched_bytes_est"]) + current_match = current_row.get("match_percent") + if ( + current_unmatched > LOW_UNMATCHED_HINT_THRESHOLD + and (current_match is None or float(current_match) < HIGH_MATCH_HINT_THRESHOLD) + ): + return None + + unit_top = function_rows[0] + larger_unit_target = None + for row in function_rows: + if row["symbol_name"] == current_symbol_name: + continue + if ( + int(row["unmatched_bytes_est"]) >= LARGER_TARGET_MIN_BYTES + and int(row["unmatched_bytes_est"]) >= current_unmatched * LARGER_TARGET_RATIO + ): + larger_unit_target = row + break + + lines: List[str] = [] + if current_match is not None and float(current_match) >= HIGH_MATCH_HINT_THRESHOLD: + lines.append( + f"- Current function is already in cleanup/polish territory " + f"(~{current_unmatched}B remaining, {float(current_match):.1f}% matched)." + ) + else: + lines.append( + f"- Current function is already low-byte cleanup territory (~{current_unmatched}B remaining)." + ) + + if larger_unit_target is not None: + larger_match = larger_unit_target.get("match_percent") + larger_match_detail = "" + if larger_match is not None: + larger_match_detail = f", {float(larger_match):.1f}% matched" + lines.append( + f"- This unit still has a much larger target: " + f"{larger_unit_target['name']} " + f"(~{larger_unit_target['unmatched_bytes_est']}B remaining{larger_match_detail})." + ) + lines.append( + f"- Try: python tools/decomp-workflow.py function -u {unit_name} " + f"-f '{larger_unit_target['name']}'" + ) + else: + lines.append( + f"- This unit's largest remaining function is only ~{unit_top['unmatched_bytes_est']}B " + f"({unit_top['name']})." + ) + category = unit_progress_category(unit) + next_cmd = "python tools/decomp-workflow.py next --strategy balanced --limit 10" + if category: + next_cmd = ( + "python tools/decomp-workflow.py next " + f"--category {category} --strategy balanced --limit 10" + ) + lines.append(f"- For larger gains elsewhere, rerun: {next_cmd}") + + if brief: + if larger_unit_target is not None: + return "\n".join([lines[0], lines[2]]) + return "\n".join([lines[0], lines[2]]) + return "\n".join(lines) + + def main(): parser = argparse.ArgumentParser( description="Gather context for decomp function matching" @@ -992,7 +1147,11 @@ def main(): if not args.no_source: if source_path: excerpt = extract_source_for_function(source_path, right_sym) - if excerpt is not None and excerpt.strip(): + if ( + excerpt is not None + and excerpt.strip() + and source_excerpt_is_useful(source_path, excerpt) + ): label = "Source" if right_sym and right_sym.get("instructions"): # Check if we actually got line info @@ -1077,6 +1236,16 @@ def main(): ), ) + priority_guidance = format_priority_guidance( + args.unit, + unit, + diff_data, + mangled, + brief=args.brief, + ) + if priority_guidance: + print_section("Higher-impact targets right now", priority_guidance) + if not source_was_useful and args.no_source: print_section( "Related Source Files", diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index 0f3dba9e8..9f4653147 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -14,12 +14,16 @@ """ import argparse -import json import os -import subprocess import sys from typing import Any, Dict, List, Optional, Tuple -from _common import ROOT_DIR, ToolError, fail, run_objdiff_json +from _common import ( + ROOT_DIR, + ToolError, + build_objdiff_symbol_rows, + fail, + run_objdiff_json, +) root_dir = ROOT_DIR OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") @@ -30,45 +34,9 @@ def run_objdiff(unit: str, base_obj: Optional[str] = None) -> Dict[str, Any]: OBJDIFF_CLI, unit, base_obj=base_obj, - extra_args=["-c", "functionRelocDiffs=none"], root_dir=root_dir, ) - -def classify_symbol(sym: Dict[str, Any]) -> str: - """Classify a symbol as 'function', 'object', or 'section'.""" - kind = sym.get("kind", "") - if kind == "SYMBOL_FUNCTION": - return "function" - if kind == "SYMBOL_OBJECT": - return "object" - if kind == "SYMBOL_SECTION": - return "section" - # Fallback for external/relocation-only symbols (empty kind) - if "instructions" in sym: - return "function" - if "data_diff" in sym: - return "object" - return "unknown" - - -def symbol_section(sym: Dict[str, Any], sections: List[Dict[str, Any]]) -> str: - """Determine which section a symbol belongs to.""" - # For named section data symbols like [.rodata-0] - name = sym.get("name", "") - if name.startswith("[."): - return name[1:].split("-")[0].rstrip("]") - # Use content type as best indicator - if classify_symbol(sym) == "function": - return ".text" - # Check sections for data - for sec in sections: - kind = sec.get("kind", "") - if kind in ("SECTION_DATA", "SECTION_BSS"): - return sec["name"] - return ".data" - - def fuzzy_match(pattern: str, name: str) -> bool: """Case-insensitive substring match.""" return pattern.lower() in name.lower() @@ -94,72 +62,43 @@ def describe_pair_status( def build_overview(data: Dict[str, Any], args) -> None: """Print overview of all symbols in a unit.""" - left_syms = data.get("left", {}).get("symbols", []) - right_syms = data.get("right", {}).get("symbols", []) - left_sections = data.get("left", {}).get("sections", []) - right_sections = data.get("right", {}).get("sections", []) - - rows = [] - - # Process left (original/target) symbols - for i, sym in enumerate(left_syms): - sym_type = classify_symbol(sym) - # Skip section symbols and external references - if sym_type in ("section", "unknown"): - continue - # Skip symbols without size - size = int(sym.get("size", "0")) - if size == 0: - continue - - name = sym.get("demangled_name", sym.get("name", "?")) - section = symbol_section(sym, left_sections) - ts = sym.get("target_symbol") - mp = sym.get("match_percent") - - if ts is None: - status = "missing" - match_str = "-" - elif mp is not None and mp >= 100.0: - status = "match" - match_str = f"{mp:.1f}%" - elif mp is not None: - status = "nonmatching" - match_str = f"{mp:.1f}%" - else: - status = "missing" - match_str = "-" - - rows.append((status, match_str, size, section, sym_type, name, "left")) - - # Process right (decomp/base) symbols that aren't targeted (extra) - for i, sym in enumerate(right_syms): - if sym.get("target_symbol") is not None: - continue # Already covered via left side - sym_type = classify_symbol(sym) - if sym_type in ("section", "unknown"): - continue - size = int(sym.get("size", "0")) - if size == 0: - continue - name = sym.get("demangled_name", sym.get("name", "?")) - section = symbol_section(sym, right_sections) - rows.append(("extra", "-", size, section, sym_type, name, "right")) + rows = build_objdiff_symbol_rows(data) # Apply filters if args.type: types = set(t.strip() for t in args.type.split(",")) - rows = [r for r in rows if r[4] in types] + rows = [r for r in rows if r["type"] in types] if args.status: - statuses = set(s.strip() for s in args.status.split(",")) - rows = [r for r in rows if r[0] in statuses] + status_aliases = {"matching": "match", "matched": "match"} + statuses = set( + status_aliases.get(s.strip(), s.strip()) for s in args.status.split(",") + ) + rows = [r for r in rows if r["status"] in statuses] if args.section: - rows = [r for r in rows if r[3] == args.section] + rows = [r for r in rows if r["section"] == args.section] if args.search: - rows = [r for r in rows if fuzzy_match(args.search, r[5])] + rows = [r for r in rows if fuzzy_match(args.search, r["name"])] + + if args.sort == "unmatched": + rows.sort( + key=lambda r: (-r["unmatched_bytes_est"], -r["size"], r["name"].lower()) + ) + elif args.sort == "size": + rows.sort(key=lambda r: (-r["size"], r["name"].lower())) + elif args.sort == "match": + rows.sort( + key=lambda r: ( + r["match_percent"] is None, + r["match_percent"] if r["match_percent"] is not None else 101.0, + -r["size"], + r["name"].lower(), + ) + ) + elif args.sort == "name": + rows.sort(key=lambda r: r["name"].lower()) if args.limit is not None: rows = rows[: args.limit] @@ -169,10 +108,20 @@ def build_overview(data: Dict[str, Any], args) -> None: return # Print header - print(f"{'STATUS':<10} {'MATCH':>7} {'SIZE':>6} {'SECTION':<10} {'NAME'}") - print("-" * 80) - for status, match_str, size, section, sym_type, name, side in rows: - print(f"{status:<10} {match_str:>7} {size:>5}B {section:<10} {name}") + print( + f"{'STATUS':<10} {'MATCH':>7} {'UNMATCH':>8} {'SIZE':>6} {'SECTION':<10} {'NAME'}" + ) + print("-" * 96) + for row in rows: + match_str = ( + f"{row['match_percent']:.1f}%" + if row["match_percent"] is not None + else "-" + ) + print( + f"{row['status']:<10} {match_str:>7} {row['unmatched_bytes_est']:>7}B " + f"{row['size']:>5}B {row['section']:<10} {row['name']}" + ) def render_instruction( @@ -458,6 +407,12 @@ def main(): type=int, help="Limit overview output to the first N matching rows", ) + parser.add_argument( + "--sort", + choices=["objdiff", "unmatched", "size", "match", "name"], + default="objdiff", + help="Sort overview rows (default: objdiff order)", + ) # Diff options parser.add_argument( diff --git a/tools/decomp-status.py b/tools/decomp-status.py index 8741b9675..8dcf5d6c7 100644 --- a/tools/decomp-status.py +++ b/tools/decomp-status.py @@ -18,7 +18,14 @@ import os import sys from typing import Any, Dict, List, Optional, Tuple -from _common import ROOT_DIR, ToolError, fail, load_objdiff_config, run_objdiff_json +from _common import ( + ROOT_DIR, + ToolError, + build_objdiff_symbol_rows, + fail, + load_objdiff_config, + run_objdiff_json, +) root_dir = ROOT_DIR @@ -41,9 +48,17 @@ def run_objdiff(unit_name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str] def analyze_unit(diff_data: Dict[str, Any]) -> Dict[str, Any]: """Analyze a unit's diff data and return summary statistics.""" left = diff_data.get("left", {}) - right = diff_data.get("right", {}) - left_syms = left.get("symbols", []) left_sections = left.get("sections", []) + symbol_rows = build_objdiff_symbol_rows(diff_data) + function_rows = [r for r in symbol_rows if r["type"] == "function" and r["side"] == "left"] + unmatched_function_rows = [ + r + for r in function_rows + if r["status"] in ("missing", "nonmatching") and r["unmatched_bytes_est"] > 0 + ] + unmatched_function_rows.sort( + key=lambda r: (-r["unmatched_bytes_est"], -r["size"], r["name"].lower()) + ) # Section-level stats section_stats = {} @@ -65,17 +80,17 @@ def analyze_unit(diff_data: Dict[str, Any]) -> Dict[str, Any]: matching_funcs = 0 total_code_size = 0 matching_code_size = 0 + remaining_code_size_est = 0 - for sym in left_syms: - if "instructions" not in sym: - continue - size = int(sym.get("size", "0")) + for row in function_rows: + size = row["size"] total_funcs += 1 total_code_size += size - mp = sym.get("match_percent") + mp = row["match_percent"] if mp is not None and mp >= 100.0: matching_funcs += 1 matching_code_size += size + remaining_code_size_est += row["unmatched_bytes_est"] text_section = section_stats.get(".text", {}) text_match = text_section.get("match_percent") @@ -86,9 +101,20 @@ def analyze_unit(diff_data: Dict[str, Any]) -> Dict[str, Any]: "matching_functions": matching_funcs, "total_code_size": total_code_size, "matching_code_size": matching_code_size, + "remaining_code_size_est": remaining_code_size_est, "text_match_percent": text_match, "text_size": text_size, "sections": section_stats, + "top_unmatched_functions": [ + { + "name": row["name"], + "status": row["status"], + "size": row["size"], + "match_percent": row["match_percent"], + "unmatched_bytes_est": row["unmatched_bytes_est"], + } + for row in unmatched_function_rows[:10] + ], } @@ -105,6 +131,12 @@ def main(): dest="json_output", help="Output as JSON", ) + parser.add_argument( + "--top-unmatched", + type=int, + metavar="N", + help="Show the top N unmatched functions by estimated unmatched bytes", + ) args = parser.parse_args() config = load_project_config() @@ -180,7 +212,9 @@ def main(): grand_matching_funcs = 0 grand_total_size = 0 grand_matching_size = 0 + grand_remaining_size_est = 0 cat_summaries = {} + top_unmatched_candidates = [] for cat, entries in sorted(results.items()): print(f"\n=== {cat} ===") @@ -206,13 +240,26 @@ def main(): mf = entry.get("matching_functions", 0) tm = entry.get("text_match_percent") tm_str = f"{tm:.1f}%" if tm is not None else "?" + remain = entry.get("remaining_code_size_est", 0) print( - f" {display_name:<50s} .text {tm_str:>6s} ({mf}/{tf} functions)" + f" {display_name:<50s} .text {tm_str:>6s} ~{remain:>6}B rem ({mf}/{tf} functions)" ) cat_funcs += tf cat_matching += mf cat_size += entry.get("total_code_size", 0) cat_matching_size += entry.get("matching_code_size", 0) + for candidate in entry.get("top_unmatched_functions", []): + top_unmatched_candidates.append( + { + "unit": name, + "display_unit": display_name, + "name": candidate["name"], + "status": candidate["status"], + "size": candidate["size"], + "match_percent": candidate["match_percent"], + "unmatched_bytes_est": candidate["unmatched_bytes_est"], + } + ) elif status == "no_source": if args.unit: print(f" {display_name:<50s} no source file") @@ -235,11 +282,17 @@ def main(): "matching": cat_matching, "complete_units": complete_count, "total_units": len(entries), + "remaining_code_size_est": sum( + e.get("remaining_code_size_est", 0) + for e in entries + if e.get("status") == "incomplete" + ), } grand_total_funcs += cat_funcs grand_matching_funcs += cat_matching grand_total_size += cat_size grand_matching_size += cat_matching_size + grand_remaining_size_est += cat_summaries[cat]["remaining_code_size_est"] if not args.unit: print(f"\n=== Summary ===") @@ -251,14 +304,40 @@ def main(): pct = f"{matching/total*100:.1f}%" if total > 0 else "N/A" print( f" {cat:<15s} {pct:>6s} ({matching}/{total} functions) " - f"[{complete}/{total_units} units complete]" + f"[{complete}/{total_units} units complete, ~{s['remaining_code_size_est']}B rem]" ) if grand_total_funcs > 0: grand_pct = grand_matching_funcs / grand_total_funcs * 100 print( - f"\n Total: {grand_pct:.1f}% ({grand_matching_funcs}/{grand_total_funcs} functions)" + f"\n Total: {grand_pct:.1f}% ({grand_matching_funcs}/{grand_total_funcs} functions, ~{grand_remaining_size_est}B rem)" ) + if args.top_unmatched: + top_unmatched_candidates.sort( + key=lambda r: (-r["unmatched_bytes_est"], -r["size"], r["name"].lower()) + ) + if args.top_unmatched > 0: + top_unmatched_candidates = top_unmatched_candidates[: args.top_unmatched] + + print("\n=== Top Unmatched Functions ===") + if not top_unmatched_candidates: + print("No unmatched functions found for the given filters.") + else: + print( + f"{'UNMATCH':>8} {'MATCH':>7} {'SIZE':>6} {'UNIT':<34} NAME" + ) + print("-" * 110) + for candidate in top_unmatched_candidates: + match_str = ( + f"{candidate['match_percent']:.1f}%" + if candidate["match_percent"] is not None + else "-" + ) + print( + f"{candidate['unmatched_bytes_est']:>7}B {match_str:>7} " + f"{candidate['size']:>5}B {candidate['display_unit']:<34} {candidate['name']}" + ) + if __name__ == "__main__": main() diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index c4dc08b39..bca622ec0 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -18,12 +18,14 @@ """ import argparse +import json import re import os +import shlex import subprocess import sys import tempfile -from typing import List, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence from _common import ( BUILD_NINJA, OBJDIFF_JSON, @@ -40,6 +42,7 @@ TOOLS_DIR = os.path.join(ROOT_DIR, "tools") PS2_TYPES = os.path.join(ROOT_DIR, "symbols", "PS2", "PS2_types.nothpp") DTK = os.path.join(ROOT_DIR, "build", "tools", "dtk") +OBJDIFF_CLI = os.path.join(ROOT_DIR, "build", "tools", "objdiff-cli") GC_SYMBOLS = os.path.join(ROOT_DIR, "config", "GOWE69", "symbols.txt") PS2_SYMBOLS = os.path.join(ROOT_DIR, "config", "SLES-53558-A124", "symbols.txt") GC_DWARF = os.path.join(ROOT_DIR, "symbols", "Dwarf") @@ -49,12 +52,15 @@ DEBUG_SYMBOL_PROBE_MANGLED = "UpdateAll__6Cameraf" DEBUG_SYMBOL_PROBE_DEMANGLED = "Camera::UpdateAll(float)" DEBUG_SYMBOL_PROBE_GC_ADDR = "0x80065A84" +LOW_MATCH_PRIORITY_THRESHOLD = 60.0 +VERY_LOW_MATCH_PRIORITY_THRESHOLD = 40.0 +HIGH_MATCH_CLEANUP_THRESHOLD = 85.0 +VERY_HIGH_MATCH_CLEANUP_THRESHOLD = 95.0 SHARED_ASSET_REQUIREMENTS = [ (os.path.join("build", "tools"), "downloaded tooling"), (os.path.join("orig", "GOWE69", "NFSMWRELEASE.ELF"), "GameCube original ELF"), (os.path.join("orig", "SLES-53558-A124", "NFS.ELF"), "PS2 original ELF"), - (os.path.join("orig", "SLES-53558-A124", "NFS.MAP"), "PS2 MAP"), (os.path.join("symbols", "Dwarf"), "DWARF dump"), ] @@ -138,13 +144,42 @@ def get_unit_build_output(unit_name: str) -> str: return make_abs(target) or target -def build_shared_unit(unit_name: str) -> str: +def build_shared_unit(unit_name: str, quiet: bool = False) -> str: ensure_decomp_prereqs() target = get_unit_build_target(unit_name) - run_stream(["ninja", target]) + cmd = ["ninja", target] + if quiet: + result = subprocess.run( + cmd, + cwd=ROOT_DIR, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise WorkflowError( + format_failure(cmd, result.returncode, result.stdout, result.stderr) + ) + else: + run_stream(cmd) return get_unit_build_output(unit_name) +def ensure_shared_unit_output(unit_name: str) -> str: + output_path = get_unit_build_output(unit_name) + if os.path.exists(output_path): + return output_path + + print(f"Shared build missing for {unit_name}; rebuilding...", flush=True) + try: + output_path = build_shared_unit(unit_name, quiet=True) + except WorkflowError as e: + raise WorkflowError( + f"Auto-build failed while preparing shared output for {unit_name}\n{e}" + ) + print(f"Shared build ready: {output_path}", flush=True) + return output_path + + def maybe_remove(path: Optional[str]) -> None: if not path: return @@ -240,6 +275,16 @@ def report(ok: bool, label: str, detail: str) -> None: ) print_section("Tool Checks") + report( + os.path.exists(OBJDIFF_CLI), + "objdiff-cli", + OBJDIFF_CLI if os.path.exists(OBJDIFF_CLI) else "missing (seed build/tools in this worktree)", + ) + report( + os.path.exists(DTK), + "dtk", + DTK if os.path.exists(DTK) else "missing (seed build/tools in this worktree)", + ) try: run_capture(python_tool("decomp-context.py", "--ghidra-check")) report(True, "ghidra", "GC + PS2 programs available") @@ -315,9 +360,133 @@ def report(ok: bool, label: str, detail: str) -> None: raise WorkflowError(f"Health check failed with {failures} issue(s)") +def build_next_candidates( + status_data: Dict[str, Any], strategy: str +) -> List[Dict[str, Any]]: + candidates: List[Dict[str, Any]] = [] + + for category, entries in status_data.items(): + for entry in entries: + unit_name = entry.get("name", "") + display_unit = unit_name.replace("main/", "") + has_source = bool(entry.get("has_source")) + + for func in entry.get("top_unmatched_functions", []): + function_name = func.get("name", "?") + unmatched = int(func.get("unmatched_bytes_est", 0)) + match_percent = func.get("match_percent") + status = func.get("status", "?") + size = int(func.get("size", 0)) + is_static_init = function_name.startswith( + "__static_initialization_and_destruction_0" + ) + is_initializer = "InitializeTables" in function_name or is_static_init + reason = "largest remaining byte win" + score = float(unmatched) + + if strategy == "balanced": + if status == "missing": + score *= 1.15 + reason = "whole implementation still missing; high remaining gain" + elif status == "nonmatching": + score *= 1.05 + reason = "large remaining win" + + if match_percent is not None: + if match_percent >= VERY_HIGH_MATCH_CLEANUP_THRESHOLD: + score *= 0.2 + reason = ( + "near-finished cleanup deprioritized in favor of larger remaining gains" + ) + elif match_percent >= HIGH_MATCH_CLEANUP_THRESHOLD: + score *= 0.45 + reason = ( + "high-match cleanup deprioritized in favor of larger remaining gains" + ) + elif match_percent <= VERY_LOW_MATCH_PRIORITY_THRESHOLD: + score *= 1.25 + reason = "low match % leaves a large amount of work and upside" + elif match_percent <= LOW_MATCH_PRIORITY_THRESHOLD: + score *= 1.1 + reason = "plenty of unmatched work remains here" + + if has_source: + score *= 1.08 + if "source available" not in reason and "deprioritized" not in reason: + reason += " with source available" + if is_initializer: + score *= 0.3 + reason = ( + "large remaining win, but likely lower-priority init/setup work" + ) + elif strategy == "quick-wins": + score = min(float(unmatched), 1024.0) + if status == "missing": + score *= 1.05 + reason = "whole implementation missing; early progress should come quickly" + elif status == "nonmatching": + score *= 1.1 + reason = "partial implementation exists, but this is still early-progress work" + + if match_percent is None: + score *= 1.35 + reason = "0% function; early implementation progress is usually fastest" + elif match_percent <= VERY_LOW_MATCH_PRIORITY_THRESHOLD: + score *= 1.35 + reason = "very low match % leaves fast early-progress gains" + elif match_percent <= LOW_MATCH_PRIORITY_THRESHOLD: + score *= 1.2 + reason = "low match % usually moves faster than cleanup" + elif match_percent >= VERY_HIGH_MATCH_CLEANUP_THRESHOLD: + score *= 0.12 + reason = "near-finished cleanup is slower than fresh early-progress work" + elif match_percent >= HIGH_MATCH_CLEANUP_THRESHOLD: + score *= 0.35 + reason = "high-match cleanup deprioritized; quicker gains exist earlier" + elif match_percent >= 70.0: + score *= 0.75 + reason = "mid/high-match work is less likely to be a quick win" + if has_source: + score *= 1.05 + if "source" not in reason: + reason += " with source available" + if is_initializer: + score *= 0.1 + reason = ( + "deprioritized init/setup work; likely not the fastest useful win" + ) + + candidates.append( + { + "category": category, + "unit": unit_name, + "display_unit": display_unit, + "function": function_name, + "status": status, + "size": size, + "match_percent": match_percent, + "unmatched_bytes_est": unmatched, + "score": score, + "reason": reason, + } + ) + + candidates.sort( + key=lambda c: ( + -c["score"], + c["match_percent"] if c["match_percent"] is not None else -1.0, + -c["unmatched_bytes_est"], + -c["size"], + c["function"].lower(), + ) + ) + return candidates + + def command_function(args: argparse.Namespace) -> None: ensure_decomp_prereqs() print_section(f"Function Workflow: {args.function}") + ensure_shared_unit_output(args.unit) cmd = python_tool("decomp-context.py", "-u", args.unit, "-f", args.function) if args.no_source: cmd.append("--no-source") @@ -337,13 +506,24 @@ def command_function(args: argparse.Namespace) -> None: def command_unit(args: argparse.Namespace) -> None: ensure_decomp_prereqs() print_section(f"Unit Status: {args.unit}") - run_stream(python_tool("decomp-status.py", "--unit", args.unit)) + ensure_shared_unit_output(args.unit) + top_unmatched_limit = args.limit if args.limit is not None else 5 + run_stream( + python_tool( + "decomp-status.py", + "--unit", + args.unit, + "--top-unmatched", + str(top_unmatched_limit), + ) + ) common_args: List[str] = ["-u", args.unit, "-t", "function"] if args.search: common_args.extend(["--search", args.search]) if args.limit is not None: common_args.extend(["--limit", str(args.limit)]) + common_args.extend(["--sort", "unmatched"]) print_section("Missing Functions") run_stream(python_tool("decomp-diff.py", *common_args, "-s", "missing")) @@ -352,6 +532,78 @@ def command_unit(args: argparse.Namespace) -> None: run_stream(python_tool("decomp-diff.py", *common_args, "-s", "nonmatching")) +def command_next(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + if args.unit: + ensure_shared_unit_output(args.unit) + + cmd = python_tool("decomp-status.py", "--json") + if args.category: + cmd.extend(["--category", args.category]) + if args.unit: + cmd.extend(["--unit", args.unit]) + + result = run_capture(cmd) + status_data = json.loads(result.stdout) + candidates = build_next_candidates(status_data, args.strategy) + if args.limit is not None: + candidates = candidates[: args.limit] + + if not candidates: + if args.unit: + for entries in status_data.values(): + for entry in entries: + if entry.get("name") != args.unit: + continue + status = entry.get("status") + if status == "error": + raise WorkflowError( + f"Unable to rank {args.unit}: {entry.get('error_message', 'objdiff failed')}" + ) + if status == "complete": + raise WorkflowError(f"{args.unit} is already complete.") + if status == "no_source": + raise WorkflowError( + f"{args.unit} has no decomp source configured in objdiff.json." + ) + if status == "no_target": + raise WorkflowError( + f"{args.unit} has no target object configured in objdiff.json." + ) + raise WorkflowError("No unmatched function candidates found for the given filters.") + + if args.command_only: + for candidate in candidates: + print( + "python tools/decomp-workflow.py function " + f"-u {shlex.quote(candidate['unit'])} " + f"-f {shlex.quote(candidate['function'])}" + ) + return + + print_section("Next Targets") + print( + f"{'UNMATCH':>8} {'MATCH':>7} {'SIZE':>6} {'UNIT':<34} {'FUNCTION'}" + ) + print("-" * 120) + for candidate in candidates: + match_str = ( + f"{candidate['match_percent']:.1f}%" + if candidate["match_percent"] is not None + else "-" + ) + print( + f"{candidate['unmatched_bytes_est']:>7}B {match_str:>7} {candidate['size']:>5}B " + f"{candidate['display_unit']:<34} {candidate['function']}" + ) + print(f" why: {candidate['reason']}") + print( + " next: python tools/decomp-workflow.py function " + f"-u {shlex.quote(candidate['unit'])} " + f"-f {shlex.quote(candidate['function'])}" + ) + + def command_build(args: argparse.Namespace) -> None: print(build_shared_unit(args.unit), flush=True) @@ -362,6 +614,7 @@ def command_diff(args: argparse.Namespace) -> None: if args.diff: title += f" / {args.diff}" print_section(title) + ensure_shared_unit_output(args.unit) cmd: List[str] = python_tool("decomp-diff.py", "-u", args.unit) if args.diff: @@ -472,6 +725,34 @@ def build_parser() -> argparse.ArgumentParser: ) unit.set_defaults(func=command_unit) + next_cmd = subparsers.add_parser( + "next", + help="Recommend the highest-impact next functions to work on", + ) + next_cmd.add_argument("--category", help="Filter by progress category") + next_cmd.add_argument("--unit", help="Restrict recommendations to one unit") + next_cmd.add_argument( + "--limit", + type=int, + default=10, + help="Limit the number of suggested targets (default: 10)", + ) + next_cmd.add_argument( + "--strategy", + choices=["impact", "balanced", "quick-wins"], + default="balanced", + help=( + "Ranking strategy for recommendations (default: balanced; quick-wins favors " + "low-match functions where early progress is fastest)" + ), + ) + next_cmd.add_argument( + "--command-only", + action="store_true", + help="Print only follow-up commands, one per line", + ) + next_cmd.set_defaults(func=command_next) + build = subparsers.add_parser( "build", help="Build a unit's shared output with its configured ninja target", diff --git a/tools/project.py b/tools/project.py index ee35a3fa9..6fd752059 100644 --- a/tools/project.py +++ b/tools/project.py @@ -226,9 +226,12 @@ def __init__(self) -> None: self.print_progress_categories: Union[bool, List[str]] = ( True # Print additional progress categories in the CLI progress output ) - self.progress_report_args: Optional[List[str]] = ( - None # Flags to `objdiff-cli report generate` - ) + self.progress_report_args: Optional[List[str]] = [ + "-c", + "functionRelocDiffs=none", + "-c", + "ppc.calculatePoolRelocations=false", + ] # Flags to `objdiff-cli report generate` # Progress fancy printing self.progress_use_fancy: bool = False From 85b45a3d2b72f2d7c3150902f1130068ade493de Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 12 Mar 2026 21:37:20 +0100 Subject: [PATCH 17/71] Bootstrap fresh worktree setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 30 ++------- tools/decomp-workflow.py | 13 +++- tools/share_worktree_assets.py | 114 ++++++++++++++++++++++++++++++--- 3 files changed, 121 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index d89e285a1..b10d186c0 100644 --- a/README.md +++ b/README.md @@ -112,34 +112,16 @@ sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' `build/`. It intentionally does **not** share `build.ninja`, `objdiff.json`, `compile_commands.json`, or per-worktree object outputs. - After linking shared assets into a worktree, regenerate that worktree's local build - files with: + After creating a fresh worktree, bootstrap its local generated files with: ```sh - python configure.py - ``` - -- Sharing large assets across git worktrees - - If you use multiple git worktrees, you can deduplicate the large immutable inputs - and downloaded tool binaries while keeping each worktree's generated build files - separate: - - ```sh - python tools/share_worktree_assets.py link --all + python tools/share_worktree_assets.py bootstrap ``` - This shares the ignored debug/tool assets under the git common directory, including - extracted `orig/*` contents, `symbols/*`, root ELF / MAP files, and downloaded - tool binaries under `build/`. It intentionally does **not** share `build.ninja`, - `objdiff.json`, `compile_commands.json`, or per-worktree object outputs. - - After linking shared assets into a worktree, regenerate that worktree's local build - files with: - - ```sh - python configure.py - ``` + `bootstrap` links the shared assets for the current worktree, runs `configure.py`, + generates the local split config when needed, and reruns `configure.py` so fresh + worktrees end up with `build.ninja`, `objdiff.json`, and `compile_commands.json` + without manual copying. # Diffing diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index bca622ec0..f9e9a0fc8 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -257,12 +257,16 @@ def report(ok: bool, label: str, detail: str) -> None: report( os.path.exists(BUILD_NINJA), "build.ninja", - BUILD_NINJA if os.path.exists(BUILD_NINJA) else "missing (run: python configure.py)", + BUILD_NINJA + if os.path.exists(BUILD_NINJA) + else "missing (run: python tools/share_worktree_assets.py bootstrap)", ) report( os.path.exists(OBJDIFF_JSON), "objdiff.json", - OBJDIFF_JSON if os.path.exists(OBJDIFF_JSON) else "missing (run: python configure.py)", + OBJDIFF_JSON + if os.path.exists(OBJDIFF_JSON) + else "missing (run: python tools/share_worktree_assets.py bootstrap)", ) print_section("Shared Assets") @@ -342,7 +346,10 @@ def report(ok: bool, label: str, detail: str) -> None: output_path = build_shared_unit(args.smoke_build) report(True, "build", output_path) except WorkflowError as e: - report(False, "build", str(e)) + detail = str(e) + if "objdiff.json" in detail or "build.ninja" in detail: + detail += "\nHint: Run: python tools/share_worktree_assets.py bootstrap" + report(False, "build", detail) if args.smoke_dtk: print_section("DTK Smoke Test") diff --git a/tools/share_worktree_assets.py b/tools/share_worktree_assets.py index f01f36145..374d168e5 100644 --- a/tools/share_worktree_assets.py +++ b/tools/share_worktree_assets.py @@ -12,6 +12,7 @@ python tools/share_worktree_assets.py status python tools/share_worktree_assets.py status --all python tools/share_worktree_assets.py link --all + python tools/share_worktree_assets.py bootstrap """ import argparse @@ -27,6 +28,7 @@ root_dir = os.path.abspath(os.path.join(script_dir, "..")) SHARED_ROOT_NAME = "worktree-shared" +DEFAULT_BOOTSTRAP_VERSION = "GOWE69" @dataclass(frozen=True) @@ -58,6 +60,24 @@ def run_git(args: List[str], cwd: str) -> str: return result.stdout +def run_command( + args: List[str], cwd: str, description: str, echo_success_output: bool = False +) -> subprocess.CompletedProcess[str]: + result = subprocess.run(args, cwd=cwd, capture_output=True, text=True) + if result.returncode != 0: + message = [f"{description} failed in {cwd}: {' '.join(args)}"] + if result.stdout.strip(): + message.append(f"stdout:\n{result.stdout.strip()}") + if result.stderr.strip(): + message.append(f"stderr:\n{result.stderr.strip()}") + raise RuntimeError("\n".join(message)) + if echo_success_output and result.stdout.strip(): + print(result.stdout.strip()) + if echo_success_output and result.stderr.strip(): + print(result.stderr.strip(), file=sys.stderr) + return result + + def git_common_dir(cwd: str) -> str: common = run_git(["rev-parse", "--git-common-dir"], cwd).strip() if os.path.isabs(common): @@ -302,19 +322,70 @@ def print_status(worktrees: List[str], shared_root: str) -> int: return 0 -def link_assets(worktrees: List[str], shared_root: str) -> int: +def link_assets( + target_worktrees: List[str], seed_worktrees: List[str], shared_root: str +) -> int: os.makedirs(shared_root, exist_ok=True) - assets = discover_assets(worktrees, shared_root) + assets = discover_assets(seed_worktrees, shared_root) for spec in assets: - shared_path = ensure_shared_asset(spec, worktrees, shared_root) + shared_path = ensure_shared_asset(spec, seed_worktrees, shared_root) if shared_path is None: continue - for worktree in worktrees: + for worktree in target_worktrees: status = link_asset(worktree, spec, shared_path) print(f"{worktree}: {spec.relpath} -> {status}") return 0 +def bootstrap_generated_files(worktree: str, version: str) -> None: + build_ninja = os.path.join(worktree, "build.ninja") + objdiff_json = os.path.join(worktree, "objdiff.json") + compile_commands = os.path.join(worktree, "compile_commands.json") + config_target = os.path.join("build", version, "config.json") + + print(f"{worktree}: running configure.py") + run_command([sys.executable, "configure.py"], worktree, "configure.py") + + if not os.path.isfile(build_ninja): + raise RuntimeError(f"{worktree}: configure.py did not create build.ninja") + + if not os.path.isfile(objdiff_json) or not os.path.isfile(compile_commands): + print(f"{worktree}: generating {config_target} for local objdiff metadata") + run_command(["ninja", config_target], worktree, f"ninja {config_target}") + print(f"{worktree}: rerunning configure.py") + run_command([sys.executable, "configure.py"], worktree, "configure.py") + + missing = [] + if not os.path.isfile(objdiff_json): + missing.append("objdiff.json") + if not os.path.isfile(compile_commands): + missing.append("compile_commands.json") + if missing: + raise RuntimeError( + f"{worktree}: bootstrap did not create {', '.join(missing)}" + ) + + +def bootstrap_worktrees( + target_worktrees: List[str], + seed_worktrees: List[str], + shared_root: str, + version: str, + run_health: bool, + smoke_build: Optional[str], +) -> int: + link_assets(target_worktrees, seed_worktrees, shared_root) + for worktree in target_worktrees: + bootstrap_generated_files(worktree, version) + if run_health or smoke_build: + cmd = [sys.executable, os.path.join("tools", "decomp-workflow.py"), "health"] + if smoke_build: + cmd.extend(["--smoke-build", smoke_build]) + print(f"{worktree}: running {' '.join(cmd)}") + run_command(cmd, worktree, "decomp-workflow health", echo_success_output=True) + return 0 + + def main() -> int: parser = argparse.ArgumentParser( description=( @@ -324,24 +395,49 @@ def main() -> int: ) parser.add_argument( "command", - choices=("status", "link"), - help="Inspect or create shared asset symlinks.", + choices=("status", "link", "bootstrap"), + help="Inspect, link, or fully bootstrap worktree-local setup.", ) parser.add_argument( "--all", action="store_true", help="Operate on all worktrees for this repository (default: current worktree only).", ) + parser.add_argument( + "--version", + default=DEFAULT_BOOTSTRAP_VERSION, + help="Version whose split config should be generated during bootstrap (default: GOWE69).", + ) + parser.add_argument( + "--health", + action="store_true", + help="Run `decomp-workflow.py health` after bootstrap completes.", + ) + parser.add_argument( + "--smoke-build", + metavar="UNIT", + help="Also run `decomp-workflow.py health --smoke-build UNIT` after bootstrap.", + ) args = parser.parse_args() common_dir = git_common_dir(root_dir) shared_root = os.path.join(common_dir, SHARED_ROOT_NAME) - worktrees = list_worktrees(root_dir) if args.all else [root_dir] + seed_worktrees = list_worktrees(root_dir) + target_worktrees = seed_worktrees if args.all else [root_dir] try: if args.command == "status": - return print_status(worktrees, shared_root) - return link_assets(worktrees, shared_root) + return print_status(target_worktrees, shared_root) + if args.command == "link": + return link_assets(target_worktrees, seed_worktrees, shared_root) + return bootstrap_worktrees( + target_worktrees, + seed_worktrees, + shared_root, + args.version, + args.health, + args.smoke_build, + ) except RuntimeError as e: print(f"Error: {e}", file=sys.stderr) return 1 From e610e214051e1ac51e069c5a07881ba56ece758b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 13:26:29 +0100 Subject: [PATCH 18/71] inline string --- src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h b/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h index a6037c36c..f47d09a39 100644 --- a/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h +++ b/src/Speed/Indep/Libs/Support/stlgc/stl/_alloc.h @@ -64,8 +64,6 @@ _STLP_BEGIN_NAMESPACE -static const char __stlp_node_alloc_name[] = {'S', 'T', 'L', '\0'}; - #if defined(_STLP_USE_RAW_SGI_ALLOCATORS) template struct __allocator; #endif @@ -235,7 +233,7 @@ template class __node_alloc { /* __n must be > 0 */ static void *_STLP_CALL allocate(size_t __n) { #ifndef CLANGD_DAMNIT - return gFastMem.Alloc(__n, __stlp_node_alloc_name); + return gFastMem.Alloc(__n, "STL"); #else return (__n > (size_t)_MAX_BYTES) ? __stl_new(__n) : _M_allocate(__n); #endif @@ -243,7 +241,7 @@ template class __node_alloc { /* __p may not be 0 */ static void _STLP_CALL deallocate(void *__p, size_t __n) { #ifndef CLANGD_DAMNIT - gFastMem.Free(__p, __n, __stlp_node_alloc_name); + gFastMem.Free(__p, __n, "STL"); #else if (__n > (size_t)_MAX_BYTES) __stl_delete(__p); From 4453407039a188cf62d4ee3677b0641f340eab85 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 13:30:30 +0100 Subject: [PATCH 19/71] tools: add style guidance and accuracy audits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .clang-format | 1 + .github/skills/code_style/SKILL.md | 171 +++++ .github/skills/implement/SKILL.md | 9 + .github/skills/scaffold/SKILL.md | 16 + AGENTS.md | 24 +- README.md | 50 ++ tools/_common.py | 15 +- tools/code_style.py | 965 +++++++++++++++++++++++++++++ tools/decomp-context.py | 23 +- tools/decomp-diff.py | 20 +- tools/decomp-workflow.py | 26 + 11 files changed, 1310 insertions(+), 10 deletions(-) create mode 100644 .github/skills/code_style/SKILL.md create mode 100644 tools/code_style.py diff --git a/.clang-format b/.clang-format index 29b540ad4..bf931d0ec 100644 --- a/.clang-format +++ b/.clang-format @@ -3,3 +3,4 @@ ColumnLimit: 150 AllowShortFunctionsOnASingleLine: Empty IndentWidth: 4 IndentCaseLabels: true +SortIncludes: Never diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md new file mode 100644 index 000000000..5aafbce35 --- /dev/null +++ b/.github/skills/code_style/SKILL.md @@ -0,0 +1,171 @@ +--- +name: code-style +description: Repo-specific code style and match-safe cleanup guidance for code-writing tasks. +--- + +# Code Style Workflow + +Use this skill when writing new code, polishing code you already touched, or doing a style review of a branch. + +This skill is about **code style only**: formatting, declaration placement, header layout, local readability, and repo-specific conventions. It is **not** a PR-response workflow and it is **not** a license to do broad cleanup sweeps. + +It also tracks the repo's written `STYLE_GUIDE.md` rules where they fit the decomp workflow. + +## Core Principle + +In this repo, style cleanup must preserve decomp progress. + +- In match-sensitive code, prefer the smallest local cleanup that keeps the code readable. +- If a style tweak changes codegen or match status, revert it. +- Extend this skill only from patterns you actually verified in the repo. + +## Quick Tooling + +Use the repo-local helper before doing a style pass: + +```sh +python tools/code_style.py audit --base origin/main +``` + +- `audit` classifies changed files into safe vs match-sensitive buckets and reports repo-specific findings. +- `audit` also checks touched `class` / `struct` declarations against known header declarations and, when no header exists, against the PS2 visibility rule. +- `audit` warns on touched local forward declarations when the repo already has a header for that type. +- `audit` warns on touched type members that look like invented padding or placeholder names such as `pad`, `unk`, or `field_1234`. +- `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, and missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's guard region is touched. +- `audit` groups repeated findings by file so branch-wide output stays readable. +- Use `audit --category safe-cpp` for frontend/support cleanup passes and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. +- `format --check` is an opt-in wrapper around the repo's `.clang-format`, but it only targets safe C/C++ files by default. +- Use `format --check --base origin/main --category safe-cpp` when you want a branch-level formatter probe instead of spelling every file path out. +- `format --check` labels whitespace-only formatter output separately from more invasive changes such as include reordering. +- `format` never targets `SourceLists/z*.cpp`; those files stay audit-only even when you opt into risky formatting. +- `format` skips files that use initializer-list guard comments (`//`) unless you explicitly override that, because clang-format fights this repo-specific convention. +- `clang-format` itself is optional. If it is not on `PATH`, install it locally or point the helper at it with `CLANG_FORMAT=/path/to/clang-format`. +- Do not pass `--include-match-sensitive` unless you are deliberately taking on verification work afterwards. + +## Phase 1: Classify the File Before Cleaning + +Decide which bucket the file belongs to: + +### 1a. Match-sensitive decomp code + +Examples: + +- gameplay / camera / physics / world translation-unit source +- utility headers and templates included by matched code +- headers with layout-sensitive inlines or emitted virtual methods + +For these files, style cleanup must be conservative and verified. + +### 1b. Safer support / frontend / tooling code + +Examples: + +- frontend interface shims +- scripts, tooling, and agent docs +- non-match-critical glue code + +These files can take normal readability cleanup, but still follow repo conventions. + +## Phase 2: Apply Repo-Specific Style Rules + +### Jumbo source-list files + +- Keep deliberate blank lines between `#include` entries when they help prevent clang-format from collapsing or reshuffling the jumbo list. +- Do not leave stray helper declarations in a `SourceLists/z*.cpp` file when they really belong near a use site in the underlying implementation file. + +### Constructors and initializer lists + +- Preserve the repo's multiline initializer-list style. +- Keep the trailing `//` markers on each initializer line except the last when that pattern is already being used to keep clang-format from collapsing the list. + +Example: + +```cpp +Foo::Foo() + : a(0), // + b(1), // + c(2) {} +``` + +### Casts, nulls, and low-level code + +- Use C++ casts instead of C-style casts. +- Spell casts without spaces inside the angle brackets: `static_cast(expr)`, not `static_cast< Type * >(expr)`. +- Use `nullptr` exclusively for null pointers. +- Prefer `if (ptr)` / `if (!ptr)` over explicit null comparisons when the change is local and verified safe. +- Inline assembly is acceptable when it is needed to preserve dead-code compares, ordering, or other compiler behavior that source alone cannot reproduce. + +### Forward declarations and local prototypes + +- Prefer including the owning repo header over adding a local forward declaration for a project type. +- If the repo already has a header declaration/definition for a type, include that header instead of redeclaring the type locally. +- Only keep a local forward declaration when no canonical repo header exists yet and you have verified that the ownership is still unresolved. +- Prefer moving helper template declarations next to their real use site instead of leaving them in an unrelated file. + +### Pointer style + +- Prefer `Type *name` over `Type* name`. +- Do not do broad pointer-style sweeps in match-sensitive files; change a small batch and verify the affected unit. + +### Header layout and data carriers + +- Use the repo's header guard form when writing headers: `#ifndef` / `#define` plus the `#ifdef EA_PRAGMA_ONCE_SUPPORTED` / `#pragma once` block. +- Keep member layout comments aligned and intact in decomp headers. +- Preserve the original `class` / `struct` kind from existing headers or Dwarf / PS2 evidence; do not treat it as a cosmetic style choice. +- Treat header declarations as the repo source of truth. If the repo only has local `.cpp` partial declarations, verify the kind with the PS2 dump instead of copying them blindly. +- Even forward declarations and local partial declarations should use the accurate keyword when known. +- Preserve the member naming style that DWARF shows. Some types use `mMember`, others use `m_member`; do not normalize them. +- Preserve recovered member names, types, order, and offset comments. Do not invent placeholder members named `pad`, `unk`, `unknown`, or `field_XXXX` for game code just to make a layout compile. +- If a member is genuinely unknown, stop and verify it with `find-symbol.py`, GC Dwarf, and PS2 data. If the layout is still incomplete, add a short TODO above the type instead of burying uncertainty in fake member names. +- Add offset / size comments when you are writing recovered type layouts from DWARF. +- Define inline member functions in headers only when DWARF shows that they are genuinely inlined in the binary. +- Use `struct` for POD-like data carriers with public fields; use `class` for behavior-heavy types only when that matches the recovered type information. +- Keep tiny placeholder methods as concise inline bodies when that is already the local pattern. + +### Namespaces and container aliases + +- Do not add `using` directives. +- Keep namespace-qualified types explicit at point of use. +- When introducing `UTL::Std::list` / `UTL::Std::vector` aliases that rely on a `_type_` helper, pair them with `DECLARE_CONTAINER_TYPE`. + +### Dense local code + +- Expand dense one-line helper structs, declaration blocks, and function bodies in non-match-sensitive files into normal multiline formatting. +- Prefer readable blocks over stacked one-line statements when behavior does not depend on exact source shape. + +### Uncertain ownership + +- If a declaration or global clearly compiles but its original home is uncertain, add a short TODO comment instead of inventing structure you cannot justify yet. +- When ownership matters, verify it with `decomp-workflow.py`, `decomp-context.py`, and `line-lookup` before moving code. + +## Phase 3: Things Not To "Clean Up" Blindly + +- Do not move an inline method out of a header just because it looks cleaner. +- Do not broad-format utility templates or virtual interfaces without checking who includes them. +- Do not rewrite expression structure in a near-matching function just to satisfy a style preference. +- Do not replace repo-specific formatting conventions with generic modern C++ preferences. + +## Phase 4: Verify Risky Style Changes + +For match-sensitive translation units: + +```sh +python tools/decomp-workflow.py build -u main/Path/To/TU +python tools/decomp-status.py --unit main/Path/To/TU +``` + +For safer but still compiled code: + +```sh +python tools/decomp-workflow.py build -u main/Path/To/OtherTU +``` + +Keep the cleanup only if the build succeeds and the relevant match status is unchanged. + +## Branch Patterns Confirmed So Far + +- Blank-line spacing in jumbo source-list include blocks is intentional and worth preserving. +- Helper template declarations should live near the file that actually uses them, not in the jumbo source-list file. +- The trailing `//` initializer-list markers are an intentional repo convention, not noise to remove. +- Small `if (ptr)` cleanup batches can be kept in match-sensitive code, but only after rebuilding the affected unit. +- Dense frontend shim files benefit from multiline struct/prototype/function formatting. diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index a81bb47c1..95ad95016 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -24,6 +24,11 @@ functions unless the user explicitly wants a cleanup/refiner pass. Use the wrapper flow first throughout this skill. Drop to raw `decomp-context.py` or `decomp-diff.py` only when the wrapper is missing a specific flag or you are debugging. +Before doing any local readability/style cleanup in code you are editing, consult +`.github/skills/code_style/SKILL.md`. Follow it for formatting, declaration placement, +pointer-style cleanup, and match-safe polish. Do not trade away match behavior for a +style preference. + ### 1a. decomp-context.py Preferred shortcut: @@ -70,6 +75,10 @@ Reference the skill for the usage. It gives info based on the virtual address of - Read the headers for class layout, member types, field offsets and the source files for existing implementations and includes (both are in `src/.../*.cpp`). - Check parent class headers for inherited members/methods used in the function +- Before adding any new declaration, partial declaration, or forward declaration, check whether the type already exists with `python tools/find-symbol.py `. +- If a repo header already exists for the type, include that header instead of introducing a local forward declaration. +- Preserve the original `class` vs `struct` kind. If the existing header is missing or incomplete, verify the type kind from GC Dwarf and PS2 info before writing a local declaration. +- Preserve real member names and field types too. Do not introduce `pad`, `unk`, or `field_XXXX` members as placeholders for guessed layout; verify the member list from GC Dwarf / PS2 data and leave a TODO when something is still uncertain. ### 1e. Assembly reference diff --git a/.github/skills/scaffold/SKILL.md b/.github/skills/scaffold/SKILL.md index 76abd7925..a2f062f50 100644 --- a/.github/skills/scaffold/SKILL.md +++ b/.github/skills/scaffold/SKILL.md @@ -31,6 +31,22 @@ Collect data from **all** of these sources in parallel where possible: Copy and cleanup the header that you got from running the `lookup` skill using the `symbols/Dwarf` folder. Fix visibility, function order and vtable related things based on using `lookup` on the PS2 types. +For formatting and local cleanup while writing the header, consult +`.github/skills/code_style/SKILL.md`. Use it for member-comment alignment, declaration +grouping, TODO placement, and other repo-specific style decisions. + +Preserve the real `class` / `struct` kind while scaffolding. Check existing headers first, +then use Dwarf plus PS2 visibility / vtable info to decide the type kind. Even temporary +forward declarations should match the known original kind. + +If the repo already has a header for a type you need, include that header instead of +adding a new local forward declaration. Only forward-declare when no canonical repo header +exists yet and you have verified that the ownership is still unresolved. + +Preserve real member names, types, order, and offset comments while scaffolding. Do not +fill gaps with invented `pad`, `unk`, or `field_XXXX` members for game types; verify the +layout from Dwarf / PS2 data and leave a TODO over the type if a field is still uncertain. + Only create headers if it's really necessary (the struct doesn't have inlines so you can't determine in which header file it goes and it's thematically very different from the other structs that use it), otherwise put it into the one you determined to be correct. The dwarf often has duplicated inlines, clean those up according to the order in the PS2 info. diff --git a/AGENTS.md b/AGENTS.md index b95d62190..0c400cca5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,17 @@ originates from, use this script against the compiler-generated debug line mappi See `.github/skills/line_lookup/SKILL.md` for the full workflow. +### code-style — Repo-local style guidance + +When you are writing code, polishing code you already touched, or doing a style-review pass, +consult `.github/skills/code_style/SKILL.md` first. It captures repo-specific formatting and +cleanup rules, including jumbo include spacing, initializer-list comment markers, declaration +placement, pointer style, and how to keep style work safe in match-sensitive code. + +Use `python tools/code_style.py audit --base origin/main` before a branch-wide style pass. +It classifies changed files, reports repo-specific findings, and only treats safer C/C++ files +as clang-format candidates by default. + ### decomp-diff.py — Diff & symbol overview Overview mode lists all symbols in a translation unit with match status: @@ -92,10 +103,12 @@ python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -s nonmatching -t function python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -s missing -t function python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim --search RemoveIOWin +python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin --reloc-diffs all ``` Filters: `-t function,object` (type), `-s missing|matching|nonmatching|extra` (status), -`--section .text`, `--search ` (fuzzy name match). +`--section .text`, `--search ` (fuzzy name match), `--reloc-diffs none|name_address|data_value|all` +(surface relocation-only mismatches when needed; default: `none`). Diff mode shows side-by-side instruction comparison: @@ -262,9 +275,14 @@ This is a **C++98** codebase compiled with ProDG GC 3.9.3 (GCC 2.95 under the ho - No `auto`, range-for, `enum class`, lambdas, or any C++11+ - Enum values use prefix: `enum EFoo { kF_Value1, kF_Value2 }` (not `enum class`) -- Use C++ casts (`static_cast< T >(expr)`) instead of C-style casts -- Header guards: `#ifndef _CLASSNAME` / `#define _CLASSNAME` (not `#pragma once`) +- Use C++ casts (`static_cast(expr)`) instead of C-style casts +- Header guards should use `#ifndef` / `#define` together with the `EA_PRAGMA_ONCE_SUPPORTED` block when writing repo headers - Constructors use initializer list style with leading `, ` on each line, add empty comments at the end of these lines (except the last) to stop clang-format from putting them all on the same line +- Inline assembly is acceptable when needed to reproduce dead code or compiler scheduling that source alone cannot express cleanly +- Preserve the original `class` vs `struct` kind. Check existing headers first, then Dwarf / PS2 info when needed. Even forward declarations and local partial declarations should use the accurate keyword when known. +- Prefer including the real repo header over introducing a local forward declaration for a project type. If a type already has a header in `src/`, include it instead of redeclaring it locally. +- Preserve original member names, types, order, and proven layout comments. Do not invent `pad`, `unk`, or `field_XXXX` members just to satisfy a guessed size or offset; verify the real members with `find-symbol.py`, GC Dwarf, and PS2 data, and leave a short TODO if a layout detail is still uncertain. +- Follow DWARF member naming exactly (`mMember` vs `m_member`) instead of normalizing names - Omit the `this` pointer. - Use `nullptr` and `override`. If they are missing, you need to include `types.h`. - Omit `struct` when declaring variables or parameters, we are not in C land. diff --git a/README.md b/README.md index b10d186c0..e36bd8a4e 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,56 @@ This file contains bChunk chunk IDs. Just tell your favourite clanker to reference `AGENTS.md` to decompile a translation unit of your choice, for example `main/Speed/Indep/SourceLists/zEAXSound`. +When introducing or forward-declaring a type, preserve the original `class` / `struct` +kind. Check existing headers first with `python tools/find-symbol.py `, then use +GC Dwarf and PS2 type info when the real declaration is missing or incomplete. + +Preserve real member names, types, order, and offset comments too. For recovered game +types, do not invent `pad`, `unk`, or `field_XXXX` members to force a guessed layout; use +the debug data and leave a short TODO when a field is still unresolved. + +If a project type already has a header in `src/`, include that header instead of adding a +local forward declaration. + +## Style tooling + +The repo ships with a decomp-aware style helper: + +```sh +python tools/code_style.py audit --base origin/main +``` + +Use `audit` to classify branch changes into safer vs match-sensitive buckets and to flag repo-specific issues such as jumbo include spacing, stray top-level declarations in `SourceLists` files, touched `class` / `struct` declarations that disagree with known headers or the PS2 visibility rule, touched project forward declarations that should be replaced by real includes, touched type members that look like invented padding or placeholder names, and touched style-guide issues that clang-format cannot fix for you (`using namespace`, `NULL`, bad cast spacing, or missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks). +Repeated findings are grouped by file so large branch audits stay readable. + +Useful focused passes: + +```sh +python tools/code_style.py audit --base origin/main --category safe-cpp +python tools/code_style.py audit --base origin/main --category match-sensitive-cpp +python tools/code_style.py format --check --base origin/main --category safe-cpp +``` + +If you have `clang-format` installed locally, you can also use: + +```sh +python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp +``` + +The formatter wrapper only targets safer C/C++ files by default. It intentionally skips match-sensitive code unless you explicitly pass `--include-match-sensitive` and verify the affected unit afterwards. +`SourceLists/z*.cpp` files remain audit-only and are never formatter targets. +`format --check` now distinguishes whitespace-only formatter deltas from more invasive output such as include reordering. +Files that use the repo's initializer-list guard comments (`//`) are skipped by default because clang-format fights that convention; override only if you are deliberately inspecting that output. +For declaration-kind checks, header declarations are treated as the repo source of truth; otherwise the helper falls back to the PS2 dump rule (`public:` / `private:` / `protected:` means `class`, no visibility labels means `struct`). + +`clang-format` is optional. Recommended installs: + +- macOS: `brew install clang-format` +- Linux: `sudo apt install clang-format` +- Windows: `winget install LLVM.LLVM` + +If your binary lives outside `PATH`, set `CLANG_FORMAT` to the executable path before running `tools/code_style.py format`. + # Contributors Special thanks to [Brawltendo](https://github.com/Brawltendo) for helping with tooling and letting me use his partial decomp. diff --git a/tools/_common.py b/tools/_common.py index db992bd8b..d1b4f42a0 100644 --- a/tools/_common.py +++ b/tools/_common.py @@ -14,9 +14,8 @@ ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) BUILD_NINJA = os.path.join(ROOT_DIR, "build.ninja") OBJDIFF_JSON = os.path.join(ROOT_DIR, "objdiff.json") +RELOC_DIFF_CHOICES = ("none", "name_address", "data_value", "all") OBJDIFF_DEFAULT_CONFIG_ARGS = [ - "-c", - "functionRelocDiffs=none", "-c", "ppc.calculatePoolRelocations=false", ] @@ -55,6 +54,15 @@ def ensure_project_prereqs(require_build_ninja: bool = False) -> None: ensure_exists(BUILD_NINJA, "Run: python configure.py") +def build_objdiff_config_args(reloc_diffs: str = "none") -> List[str]: + if reloc_diffs not in RELOC_DIFF_CHOICES: + raise ToolError( + f"Invalid relocation diff mode: {reloc_diffs} " + f"(expected one of {', '.join(RELOC_DIFF_CHOICES)})" + ) + return ["-c", f"functionRelocDiffs={reloc_diffs}", *OBJDIFF_DEFAULT_CONFIG_ARGS] + + def load_json_file(path: str, description: str) -> Any: try: with open(path) as f: @@ -262,12 +270,13 @@ def run_objdiff_json( *, base_obj: Optional[str] = None, extra_args: Optional[Sequence[str]] = None, + reloc_diffs: str = "none", root_dir: str = ROOT_DIR, ) -> Dict[str, Any]: ensure_project_prereqs() cmd = [objdiff_cli, "diff"] - cmd.extend(OBJDIFF_DEFAULT_CONFIG_ARGS) + cmd.extend(build_objdiff_config_args(reloc_diffs)) if extra_args: cmd.extend(extra_args) cmd.extend(["-u", unit_name, "-o", "-", "--format", "json"]) diff --git a/tools/code_style.py b/tools/code_style.py new file mode 100644 index 000000000..1f14f7c72 --- /dev/null +++ b/tools/code_style.py @@ -0,0 +1,965 @@ +#!/usr/bin/env python3 +""" +Decomp-aware code style helper. + +Examples: + python tools/code_style.py audit --base origin/main + python tools/code_style.py classify src/Speed/Indep/Src/Frontend/FEManager.cpp + python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp +""" + +import argparse +import os +import platform +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional, Sequence, Set + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.abspath(os.path.join(script_dir, "..")) +src_dir = os.path.join(root_dir, "src") +ps2_types_path = os.path.join(root_dir, "symbols", "PS2", "PS2_types.nothpp") + +CPP_EXTS = {".c", ".cc", ".cpp", ".h", ".hh", ".hpp"} +HEADER_EXTS = {".h", ".hh", ".hpp"} + +SAFE_CPP_PREFIXES = ( + "src/Speed/Indep/Src/Frontend/", + "src/Speed/Indep/Src/FEng/", +) +DOC_PREFIXES = ( + ".github/skills/", + "docs/", +) +TOOL_PREFIXES = ( + "tools/", +) +JUMBO_PREFIX = "src/Speed/Indep/SourceLists/" +MATCH_SENSITIVE_PREFIXES = ( + "src/Speed/Indep/Libs/Support/Utility/", + "src/Speed/Indep/bWare/Inc/", + "src/Speed/Indep/Src/", +) +ROOT_FILES = { + "AGENTS.md", + "README.md", +} +CATEGORIES = ( + "docs", + "jumbo-source-list", + "match-sensitive-cpp", + "match-sensitive-other", + "other", + "safe-cpp", + "safe-other", + "tooling", +) + + +@dataclass +class Finding: + path: str + line: int + severity: str + message: str + + +DECL_PATTERN = re.compile( + r"^\s*(struct|class)\s+([A-Za-z_][A-Za-z0-9_]*)\b(?:\s*[:;{]|$)" +) +TYPE_BODY_START_PATTERN = re.compile(r"^\s*(struct|class)\s+([A-Za-z_][A-Za-z0-9_]*)\b.*\{") +FORWARD_DECL_PATTERN = re.compile(r"^\s*(struct|class)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;\s*$") +VISIBILITY_PATTERN = re.compile(r"^\s*(public|private|protected)\s*:", re.MULTILINE) +ACCESS_SPECIFIER_PATTERN = re.compile(r"^\s*(public|private|protected)\s*:\s*$") +CAST_SPACING_PATTERN = re.compile( + r"\b(?:static_cast|reinterpret_cast|const_cast|dynamic_cast)\s*<\s+" + r"|" + r"\b(?:static_cast|reinterpret_cast|const_cast|dynamic_cast)\s*<[^>\n]*\s+>" +) +USING_NAMESPACE_PATTERN = re.compile(r"^\s*using\s+namespace\b") +NULL_PATTERN = re.compile(r"\bNULL\b") +HEADER_GUARD_IFNDEF_PATTERN = re.compile(r"^\s*#ifndef\s+[A-Za-z0-9_]+\s*$", re.MULTILINE) +HEADER_GUARD_DEFINE_PATTERN = re.compile(r"^\s*#define\s+[A-Za-z0-9_]+\s*$", re.MULTILINE) +EA_PRAGMA_BLOCK_PATTERN = re.compile( + r"^\s*#ifdef\s+EA_PRAGMA_ONCE_SUPPORTED\s*$" + r".*?^\s*#pragma\s+once\s*$" + r".*?^\s*#endif\s*$", + re.MULTILINE | re.DOTALL, +) +SUSPICIOUS_MEMBER_PATTERN = re.compile( + r"^(?:" + r"_?pad(?:ding)?[0-9A-Fa-f_]*" + r"|pad(?:byte|char)" + r"|unk(?:nown)?[0-9A-Fa-f_]*" + r"|unk_[A-Za-z0-9_]+" + r"|field_[0-9A-Fa-f]+" + r")$" +) + +_source_decl_cache: Optional[Dict[str, List[tuple]]] = None +_ps2_kind_cache: Dict[str, Optional[str]] = {} + + +def run_git(args: Sequence[str]) -> str: + result = subprocess.run( + ["git", *args], + cwd=root_dir, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "git command failed") + return result.stdout + + +def relpath(path: str) -> str: + abs_path = path if os.path.isabs(path) else os.path.join(root_dir, path) + return os.path.relpath(abs_path, root_dir).replace("\\", "/") + + +def path_category(path: str) -> str: + path = relpath(path) + ext = os.path.splitext(path)[1] + + if path in ROOT_FILES: + return "docs" + if any(path.startswith(prefix) for prefix in DOC_PREFIXES): + return "docs" + if any(path.startswith(prefix) for prefix in TOOL_PREFIXES): + return "tooling" + if path.startswith(JUMBO_PREFIX): + return "jumbo-source-list" + if any(path.startswith(prefix) for prefix in SAFE_CPP_PREFIXES): + return "safe-cpp" if ext in CPP_EXTS else "safe-other" + if any(path.startswith(prefix) for prefix in MATCH_SENSITIVE_PREFIXES): + return "match-sensitive-cpp" if ext in CPP_EXTS else "match-sensitive-other" + return "other" + + +def file_candidates_from_base(base: str, include_worktree: bool) -> List[str]: + files: Set[str] = set() + for line in run_git(["diff", "--name-only", f"{base}...HEAD"]).splitlines(): + if line.strip(): + files.add(line.strip()) + if include_worktree: + for line in run_git(["diff", "--name-only"]).splitlines(): + if line.strip(): + files.add(line.strip()) + return sorted(files) + + +def collect_touched_lines_from_diff(diff_text: str) -> Dict[str, Set[int]]: + touched: Dict[str, Set[int]] = {} + current_path: Optional[str] = None + + for line in diff_text.splitlines(): + if line.startswith("+++ b/"): + current_path = line[6:] + touched.setdefault(current_path, set()) + continue + + if not line.startswith("@@") or current_path is None: + continue + + match = re.search(r"\+(\d+)(?:,(\d+))?", line) + if match is None: + continue + + start = int(match.group(1)) + count = int(match.group(2) or "1") + if count == 0: + continue + + for line_no in range(start, start + count): + touched.setdefault(current_path, set()).add(line_no) + + return touched + + +def touched_lines_from_base(base: str, include_worktree: bool) -> Dict[str, Set[int]]: + touched = collect_touched_lines_from_diff( + run_git(["diff", "--unified=0", f"{base}...HEAD"]) + ) + if include_worktree: + worktree_touched = collect_touched_lines_from_diff( + run_git(["diff", "--unified=0"]) + ) + for path, lines in worktree_touched.items(): + touched.setdefault(path, set()).update(lines) + return touched + + +def read_text(path: str) -> str: + with open( + os.path.join(root_dir, relpath(path)), + encoding="utf-8", + errors="ignore", + ) as f: + return f.read() + + +def source_declaration_index() -> Dict[str, List[tuple]]: + global _source_decl_cache + if _source_decl_cache is not None: + return _source_decl_cache + + index: Dict[str, List[tuple]] = {} + for dirpath, _dirs, files in os.walk(src_dir): + for fname in files: + if os.path.splitext(fname)[1] not in CPP_EXTS: + continue + fpath = os.path.join(dirpath, fname) + rel = os.path.relpath(fpath, root_dir).replace("\\", "/") + try: + with open(fpath, encoding="utf-8", errors="ignore") as f: + for lineno, line in enumerate(f, 1): + match = DECL_PATTERN.match(line) + if match is None: + continue + kind = match.group(1) + name = match.group(2) + index.setdefault(name, []).append((kind, rel, lineno)) + except OSError: + continue + + _source_decl_cache = index + return index + + +def expected_kind_from_source(name: str, current_path: str, current_line: int) -> Optional[str]: + candidates = source_declaration_index().get(name, []) + filtered = [] + for kind, rel, lineno in candidates: + if rel == current_path and lineno == current_line: + continue + if os.path.splitext(rel)[1] not in {".h", ".hh", ".hpp"}: + continue + filtered.append(kind) + unique = sorted(set(filtered)) + if len(unique) == 1: + return unique[0] + return None + + +def header_declaration_paths(name: str, current_path: str, current_line: int) -> List[str]: + candidates = source_declaration_index().get(name, []) + headers = set() + for _kind, rel, lineno in candidates: + if rel == current_path and lineno == current_line: + continue + if os.path.splitext(rel)[1] not in {".h", ".hh", ".hpp"}: + continue + headers.add(rel) + return sorted(headers) + + +def expected_kind_from_ps2(name: str) -> Optional[str]: + cached = _ps2_kind_cache.get(name) + if cached is not None or name in _ps2_kind_cache: + return cached + + if not os.path.isfile(ps2_types_path): + _ps2_kind_cache[name] = None + return None + + result = subprocess.run( + ["python", "tools/lookup.py", "--file", ps2_types_path, "struct", name], + cwd=root_dir, + capture_output=True, + text=True, + ) + output = (result.stdout or result.stderr).strip() + if result.returncode != 0 or not output.startswith("struct "): + _ps2_kind_cache[name] = None + return None + + if VISIBILITY_PATTERN.search(output): + _ps2_kind_cache[name] = "class" + else: + _ps2_kind_cache[name] = "struct" + return _ps2_kind_cache[name] + + +def audit_type_kind_declarations( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings: List[Finding] = [] + for idx, line in enumerate(text.splitlines(), 1): + if touched_lines is not None and idx not in touched_lines: + continue + match = DECL_PATTERN.match(line) + if match is None: + continue + + actual_kind = match.group(1) + name = match.group(2) + expected_kind = expected_kind_from_source(name, path, idx) + reason = "repo declaration" + if expected_kind is None: + expected_kind = expected_kind_from_ps2(name) + reason = "PS2 visibility rule" + if expected_kind is None or expected_kind == actual_kind: + continue + + findings.append( + Finding( + path, + idx, + "WARN", + f"`{actual_kind} {name}` disagrees with known type kind; use `{expected_kind} {name}` ({reason})", + ) + ) + return findings + + +def extract_member_name(line: str) -> Optional[str]: + code = line.split("//", 1)[0].strip() + if not code or code.startswith("#") or code.endswith(":"): + return None + if "(" in code or ")" in code: + return None + if any(code.startswith(prefix) for prefix in ("typedef ", "using ", "enum ", "union ", "class ", "struct ", "friend ")): + return None + + code = code.rstrip(";").strip() + if "," in code: + return None + if "=" in code: + code = code.split("=", 1)[0].rstrip() + + match = re.search( + r"([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^\]]+\])?\s*(?::\s*\d+)?\s*$", + code, + ) + if match is None: + return None + return match.group(1) + + +def audit_placeholder_members( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings: List[Finding] = [] + current_type: Optional[str] = None + pending_type: Optional[str] = None + brace_depth = 0 + + for idx, line in enumerate(text.splitlines(), 1): + stripped = line.strip() + + if current_type is None: + start_match = TYPE_BODY_START_PATTERN.match(line) + if start_match is not None: + current_type = start_match.group(2) + brace_depth = line.count("{") - line.count("}") + if brace_depth <= 0: + current_type = None + brace_depth = 0 + continue + + decl_match = DECL_PATTERN.match(line) + if decl_match is not None and "{" not in line and not stripped.endswith(";"): + pending_type = decl_match.group(2) + + if pending_type is not None and "{" in line: + current_type = pending_type + pending_type = None + brace_depth = line.count("{") - line.count("}") + if brace_depth <= 0: + current_type = None + brace_depth = 0 + continue + + if stripped.endswith(";"): + pending_type = None + continue + + if touched_lines is None or idx in touched_lines: + if not ACCESS_SPECIFIER_PATTERN.match(line): + member_name = extract_member_name(line) + if member_name is not None and SUSPICIOUS_MEMBER_PATTERN.match(member_name): + findings.append( + Finding( + path, + idx, + "WARN", + f"`{current_type}` member `{member_name}` looks like placeholder padding/unknown naming; verify the real member from Dwarf/PS2 instead of inventing pads", + ) + ) + + brace_depth += line.count("{") - line.count("}") + if brace_depth <= 0: + current_type = None + brace_depth = 0 + + return findings + + +def audit_forward_declarations( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings: List[Finding] = [] + for idx, line in enumerate(text.splitlines(), 1): + if touched_lines is not None and idx not in touched_lines: + continue + match = FORWARD_DECL_PATTERN.match(line) + if match is None: + continue + + name = match.group(2) + headers = header_declaration_paths(name, path, idx) + if not headers: + continue + + sample = ", ".join(headers[:2]) + if len(headers) > 2: + sample += ", ..." + findings.append( + Finding( + path, + idx, + "WARN", + f"`{name}` is forward-declared even though repo headers exist; include {sample} instead of redeclaring", + ) + ) + return findings + + +def audit_style_guide_rules( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings: List[Finding] = [] + ext = os.path.splitext(path)[1] + + for idx, line in enumerate(text.splitlines(), 1): + if touched_lines is not None and idx not in touched_lines: + continue + stripped = line.strip() + if stripped.startswith("//"): + continue + + if CAST_SPACING_PATTERN.search(line): + findings.append( + Finding( + path, + idx, + "WARN", + "C++ cast uses spaces inside `<...>`; prefer `static_cast(expr)` style", + ) + ) + if USING_NAMESPACE_PATTERN.search(line): + findings.append( + Finding( + path, + idx, + "WARN", + "`using namespace` is not allowed here; keep names fully qualified", + ) + ) + if NULL_PATTERN.search(line): + findings.append( + Finding( + path, + idx, + "WARN", + "use `nullptr` instead of `NULL`", + ) + ) + + if ext in HEADER_EXTS: + should_check_guard = touched_lines is None or any(line_no <= 8 for line_no in touched_lines) + if should_check_guard: + has_ifndef = HEADER_GUARD_IFNDEF_PATTERN.search(text) is not None + has_define = HEADER_GUARD_DEFINE_PATTERN.search(text) is not None + has_pragma_block = EA_PRAGMA_BLOCK_PATTERN.search(text) is not None + if not (has_ifndef and has_define and has_pragma_block): + findings.append( + Finding( + path, + 1, + "WARN", + "header guard should use `#ifndef` / `#define` plus the `EA_PRAGMA_ONCE_SUPPORTED` `#pragma once` block", + ) + ) + + return findings + + +def audit_source_list( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings: List[Finding] = [] + lines = text.splitlines() + seen_include = False + prev_include_line = -1 + + for idx, line in enumerate(lines, 1): + stripped = line.strip() + if not seen_include: + if stripped.startswith("#include "): + seen_include = True + prev_include_line = idx + continue + + if stripped.startswith("#include "): + if idx == prev_include_line + 1 and ( + touched_lines is None + or idx in touched_lines + or prev_include_line in touched_lines + ): + findings.append( + Finding( + path, + idx, + "WARN", + "consecutive jumbo includes without a separating blank line", + ) + ) + prev_include_line = idx + continue + + if stripped == "": + continue + + if touched_lines is None or idx in touched_lines: + findings.append( + Finding( + path, + idx, + "INFO", + "top-level declaration/code in SourceLists file; keep only if placement is intentional", + ) + ) + break + + return findings + + +def audit_safe_cpp( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings = audit_type_kind_declarations(path, text, touched_lines) + findings.extend(audit_forward_declarations(path, text, touched_lines)) + findings.extend(audit_placeholder_members(path, text, touched_lines)) + findings.extend(audit_style_guide_rules(path, text, touched_lines)) + pointer_pattern = re.compile( + r"\b[A-Za-z_][A-Za-z0-9_:<>]*\*\s*[A-Za-z_][A-Za-z0-9_]*" + ) + + for idx, line in enumerate(text.splitlines(), 1): + stripped = line.strip() + if stripped.startswith("//") or stripped.startswith("#"): + continue + if touched_lines is not None and idx not in touched_lines: + continue + if pointer_pattern.search(line): + findings.append( + Finding( + path, + idx, + "INFO", + "pointer declaration/prototype uses `Type* name`; repo style prefers `Type *name`", + ) + ) + return findings + + +def audit_match_sensitive_cpp( + path: str, text: str, touched_lines: Optional[Set[int]] +) -> List[Finding]: + findings = audit_type_kind_declarations(path, text, touched_lines) + findings.extend(audit_forward_declarations(path, text, touched_lines)) + findings.extend(audit_placeholder_members(path, text, touched_lines)) + findings.extend(audit_style_guide_rules(path, text, touched_lines)) + nullptr_pattern = re.compile(r"\bif\s*\([^)]*(?:==|!=)\s*nullptr") + + for idx, line in enumerate(text.splitlines(), 1): + if touched_lines is not None and idx not in touched_lines: + continue + if nullptr_pattern.search(line): + findings.append( + Finding( + path, + idx, + "INFO", + "pointer-null comparison is a candidate for `if (ptr)` cleanup, but verify the affected TU first", + ) + ) + return findings + + +def audit_path(path: str, touched_lines: Optional[Set[int]]) -> List[Finding]: + path = relpath(path) + abs_path = os.path.join(root_dir, path) + if not os.path.isfile(abs_path): + return [] + + category = path_category(path) + text = read_text(path) + + if category == "jumbo-source-list": + return audit_source_list(path, text, touched_lines) + if category == "safe-cpp": + return audit_safe_cpp(path, text, touched_lines) + if category == "match-sensitive-cpp": + return audit_match_sensitive_cpp(path, text, touched_lines) + return [] + + +def gather_paths(args: argparse.Namespace) -> List[str]: + if args.paths: + return [relpath(path) for path in args.paths] + return file_candidates_from_base(args.base, include_worktree=not args.no_worktree) + + +def filter_paths_by_category( + paths: Iterable[str], categories: Optional[Sequence[str]] +) -> List[str]: + if not categories: + return list(paths) + allowed = set(categories) + return [path for path in paths if path_category(path) in allowed] + + +def format_line_list(lines: Sequence[int], sample_limit: int) -> str: + sample = list(lines[:sample_limit]) + rendered = ", ".join(str(line) for line in sample) + if len(lines) > sample_limit: + rendered += ", ..." + return rendered + + +def strip_whitespace(text: str) -> str: + return re.sub(r"\s+", "", text) + + +def include_lines(text: str) -> List[str]: + return [line.strip() for line in text.splitlines() if line.strip().startswith("#include ")] + + +def has_initializer_guard_comments(text: str) -> bool: + guard_pattern = re.compile(r"^\s*(?::|,)\s+.*//\s*$", re.MULTILINE) + return guard_pattern.search(text) is not None + + +def format_change_summary(before: str, after: str) -> str: + reasons: List[str] = [] + if strip_whitespace(before) == strip_whitespace(after): + reasons.append("whitespace-only") + else: + reasons.append("non-whitespace token/order changes") + + before_includes = include_lines(before) + after_includes = include_lines(after) + if before_includes and before_includes != after_includes and sorted(before_includes) == sorted(after_includes): + reasons.append("reorders includes") + + if has_initializer_guard_comments(before): + reasons.append("initializer-list guard comments present") + + return ", ".join(reasons) + + +def command_classify(args: argparse.Namespace) -> int: + for path in filter_paths_by_category(gather_paths(args), args.category): + print(f"{path_category(path):<22} {path}") + return 0 + + +def command_audit(args: argparse.Namespace) -> int: + paths = filter_paths_by_category(gather_paths(args), args.category) + if not paths: + print("No files selected.") + return 0 + + touched_lines = None if args.paths else touched_lines_from_base( + args.base, include_worktree=not args.no_worktree + ) + + by_category = {} + findings: List[Finding] = [] + for path in paths: + by_category.setdefault(path_category(path), []).append(path) + findings.extend( + audit_path(path, None if touched_lines is None else touched_lines.get(path)) + ) + + print("File categories:") + for category in sorted(by_category): + print(f" {category}: {len(by_category[category])}") + print() + + safe_format_candidates = [ + path + for path in paths + if path_category(path) == "safe-cpp" and os.path.splitext(path)[1] in CPP_EXTS + ] + if safe_format_candidates: + print("Safe clang-format candidates:") + for path in safe_format_candidates: + print(f" {path}") + print() + + if not findings: + print("No style findings.") + return 0 + + print("Findings:") + findings = sorted(findings, key=lambda item: (item.path, item.line, item.message)) + if args.ungrouped: + shown = findings[: args.max_findings] + for finding in shown: + print( + f" {finding.severity:<4} {finding.path}:{finding.line}: {finding.message}" + ) + if len(findings) > len(shown): + print() + print(f" ... {len(findings) - len(shown)} more finding(s) omitted") + return 0 + + grouped: Dict[tuple, List[int]] = {} + for finding in findings: + grouped.setdefault( + (finding.severity, finding.path, finding.message), [] + ).append(finding.line) + + grouped_items = sorted(grouped.items(), key=lambda item: (item[0][1], item[1][0], item[0][2])) + shown = grouped_items[: args.max_findings] + for (severity, path, message), lines in shown: + print( + f" {severity:<4} {path}: {message} ({len(lines)} occurrence(s); lines {format_line_list(lines, args.sample_lines)})" + ) + if len(grouped_items) > len(shown): + print() + print(f" ... {len(grouped_items) - len(shown)} more grouped finding(s) omitted") + return 0 + + +def find_clang_format() -> str: + env_override = os.environ.get("CLANG_FORMAT") + if env_override: + if os.path.isfile(env_override) and os.access(env_override, os.X_OK): + return env_override + resolved = shutil.which(env_override) + if resolved is not None: + return resolved + raise RuntimeError( + f"CLANG_FORMAT is set to '{env_override}', but that executable was not found." + ) + + candidates = ( + "clang-format", + "clang-format-19", + "clang-format-18", + "clang-format-17", + "clang-format-16", + "clang-format-15", + "clang-format-14", + ) + for candidate in candidates: + resolved = shutil.which(candidate) + if resolved is not None: + return resolved + + system = platform.system() + if system == "Darwin": + install_hint = "Install it with `brew install clang-format`." + elif system == "Linux": + install_hint = "Install it with your package manager, for example `sudo apt install clang-format`." + elif system == "Windows": + install_hint = "Install LLVM/clang-format, for example with `winget install LLVM.LLVM`." + else: + install_hint = "Install clang-format and ensure it is available on PATH." + + raise RuntimeError( + "clang-format not found. " + + install_hint + + " You can also point the helper at a specific binary with the CLANG_FORMAT environment variable." + ) + + +def format_paths(paths: Iterable[str], include_match_sensitive: bool) -> List[str]: + allowed = {"safe-cpp"} + if include_match_sensitive: + allowed.add("match-sensitive-cpp") + + return [ + relpath(path) + for path in paths + if path_category(path) in allowed and os.path.splitext(path)[1] in CPP_EXTS + ] + + +def command_format(args: argparse.Namespace) -> int: + selected = format_paths( + filter_paths_by_category(gather_paths(args), args.category), + args.include_match_sensitive, + ) + if not selected: + print("No format-eligible files selected.") + return 0 + + clang_format = find_clang_format() + changed: List[str] = [] + changed_summaries: Dict[str, str] = {} + skipped_initializer_guards: List[str] = [] + + for path in selected: + abs_path = os.path.join(root_dir, path) + with open(abs_path, encoding="utf-8", errors="ignore") as f: + before = f.read() + + if has_initializer_guard_comments(before) and not args.include_initializer_guards: + skipped_initializer_guards.append(path) + continue + + if args.check: + result = subprocess.run( + [clang_format, "--style=file", abs_path], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(result.stderr.strip(), file=sys.stderr) + return result.returncode + if result.stdout != before: + changed.append(path) + changed_summaries[path] = format_change_summary(before, result.stdout) + else: + result = subprocess.run([clang_format, "-i", "--style=file", abs_path]) + if result.returncode != 0: + return result.returncode + changed.append(path) + + if skipped_initializer_guards: + print("Skipped files with initializer-list guard comments:") + for path in skipped_initializer_guards: + print(f" {path}") + print(" clang-format fights this repo convention; inspect these manually or override explicitly.") + print() + + if args.check: + if changed: + print("Would reformat:") + for path in changed: + print(f" {path} [{changed_summaries[path]}]") + return 1 + print("All selected files already match clang-format output.") + return 0 + + print("Formatted files:") + for path in changed: + print(f" {path}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Decomp-aware code style helper") + subparsers = parser.add_subparsers(dest="command", required=True) + + shared = argparse.ArgumentParser(add_help=False) + shared.add_argument( + "paths", + nargs="*", + help="Files to inspect. If omitted, use changed files against --base.", + ) + shared.add_argument( + "--base", + default="origin/main", + help="Base ref used when paths are omitted (default: origin/main)", + ) + shared.add_argument( + "--no-worktree", + action="store_true", + help="Ignore uncommitted worktree changes when collecting default files", + ) + + classify = subparsers.add_parser( + "classify", + parents=[shared], + help="Classify files by style-risk bucket", + ) + classify.add_argument( + "--category", + action="append", + choices=CATEGORIES, + help="Restrict output to one or more categories", + ) + classify.set_defaults(func=command_classify) + + audit = subparsers.add_parser( + "audit", + parents=[shared], + help="Audit files for repo-specific style issues", + ) + audit.add_argument( + "--category", + action="append", + choices=CATEGORIES, + help="Restrict the audit to one or more categories", + ) + audit.add_argument( + "--max-findings", + type=int, + default=60, + help="Maximum number of findings or grouped findings to print (default: 60)", + ) + audit.add_argument( + "--sample-lines", + type=int, + default=5, + help="Maximum line samples to print per grouped finding (default: 5)", + ) + audit.add_argument( + "--ungrouped", + action="store_true", + help="Print individual findings instead of grouped summaries", + ) + audit.set_defaults(func=command_audit) + + fmt = subparsers.add_parser( + "format", + parents=[shared], + help="Run clang-format on safe files by default", + ) + fmt.add_argument( + "--category", + action="append", + choices=CATEGORIES, + help="Restrict the format pass to one or more categories", + ) + fmt.add_argument( + "--check", + action="store_true", + help="Report files that would change instead of formatting them", + ) + fmt.add_argument( + "--include-match-sensitive", + action="store_true", + help="Also format match-sensitive C/C++ files (dangerous; verify afterwards). SourceLists files stay excluded.", + ) + fmt.add_argument( + "--include-initializer-guards", + action="store_true", + help="Also format files that use initializer-list guard comments (`//`). Disabled by default because clang-format fights that repo convention.", + ) + fmt.set_defaults(func=command_format) + + return parser + + +def main() -> int: + parser = build_parser() + args = parser.parse_args() + try: + return args.func(args) + except RuntimeError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/decomp-context.py b/tools/decomp-context.py index 32a35fcae..87e486552 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -26,6 +26,7 @@ from typing import Any, Dict, List, Optional, Tuple from _common import ( ROOT_DIR, + RELOC_DIFF_CHOICES, ToolError, build_objdiff_symbol_rows, fail, @@ -69,11 +70,16 @@ def find_unit(config: Dict[str, Any], unit_name: str) -> Optional[Dict[str, Any] return None -def run_objdiff(unit_name: str, base_obj: Optional[str] = None) -> Optional[Dict[str, Any]]: +def run_objdiff( + unit_name: str, + base_obj: Optional[str] = None, + reloc_diffs: str = "none", +) -> Optional[Dict[str, Any]]: return run_objdiff_json( OBJDIFF_CLI, unit_name, base_obj=base_obj, + reloc_diffs=reloc_diffs, root_dir=root_dir, ) @@ -1105,6 +1111,15 @@ def main(): "Use this .o file as the decomp base instead of the one from objdiff.json." ), ) + parser.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help=( + "Control relocation-only mismatches in objdiff " + "(default: none; use all to surface relocation diffs)" + ), + ) args = parser.parse_args() if args.ghidra_check: @@ -1124,7 +1139,9 @@ def main(): source_path = meta.get("source_path", "") # === objdiff Status (run first so we have line numbers for source scoping) === - diff_data = run_objdiff(args.unit, base_obj=args.base_obj) + diff_data = run_objdiff( + args.unit, base_obj=args.base_obj, reloc_diffs=args.reloc_diffs + ) left_sym = right_sym = None if diff_data: @@ -1269,6 +1286,8 @@ def main(): args.unit, "-d", args.function, + "--reloc-diffs", + args.reloc_diffs, ] if args.base_obj: diff_cmd += ["--base-obj", args.base_obj] diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index 9f4653147..78017385e 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -11,6 +11,7 @@ python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -s nonmatching python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -d __9CAnimBank + python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -d __9CAnimBank --reloc-diffs all """ import argparse @@ -19,6 +20,7 @@ from typing import Any, Dict, List, Optional, Tuple from _common import ( ROOT_DIR, + RELOC_DIFF_CHOICES, ToolError, build_objdiff_symbol_rows, fail, @@ -29,11 +31,14 @@ OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") -def run_objdiff(unit: str, base_obj: Optional[str] = None) -> Dict[str, Any]: +def run_objdiff( + unit: str, base_obj: Optional[str] = None, reloc_diffs: str = "none" +) -> Dict[str, Any]: return run_objdiff_json( OBJDIFF_CLI, unit, base_obj=base_obj, + reloc_diffs=reloc_diffs, root_dir=root_dir, ) @@ -438,11 +443,22 @@ def main(): "Use this .o file as the decomp base instead of the one from objdiff.json." ), ) + parser.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help=( + "Control relocation-only mismatches in objdiff " + "(default: none; use all to surface relocation diffs)" + ), + ) args = parser.parse_args() try: - data = run_objdiff(args.unit, base_obj=args.base_obj) + data = run_objdiff( + args.unit, base_obj=args.base_obj, reloc_diffs=args.reloc_diffs + ) except ToolError as e: fail(str(e)) diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index f9e9a0fc8..df90eff4a 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -14,6 +14,7 @@ python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --lookup-mode full python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-lookup python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-source + python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zCamera -d UpdateAll --reloc-diffs all python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zCamera """ @@ -29,6 +30,7 @@ from _common import ( BUILD_NINJA, OBJDIFF_JSON, + RELOC_DIFF_CHOICES, ROOT_DIR, ToolError, ensure_exists, @@ -507,6 +509,8 @@ def command_function(args: argparse.Namespace) -> None: cmd.extend(["--ghidra-version", args.ghidra_version]) if args.brief: cmd.append("--brief") + if args.reloc_diffs != "none": + cmd.extend(["--reloc-diffs", args.reloc_diffs]) run_stream(cmd) @@ -526,6 +530,8 @@ def command_unit(args: argparse.Namespace) -> None: ) common_args: List[str] = ["-u", args.unit, "-t", "function"] + if args.reloc_diffs != "none": + common_args.extend(["--reloc-diffs", args.reloc_diffs]) if args.search: common_args.extend(["--search", args.search]) if args.limit is not None: @@ -624,6 +630,8 @@ def command_diff(args: argparse.Namespace) -> None: ensure_shared_unit_output(args.unit) cmd: List[str] = python_tool("decomp-diff.py", "-u", args.unit) + if args.reloc_diffs != "none": + cmd.extend(["--reloc-diffs", args.reloc_diffs]) if args.diff: cmd.extend(["-d", args.diff]) if args.type: @@ -717,6 +725,12 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Trim helper sections like related-source hints and suggested commands", ) + function.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help="Pass through objdiff relocation diff mode to decomp-context.py", + ) function.set_defaults(func=command_function) unit = subparsers.add_parser( @@ -730,6 +744,12 @@ def build_parser() -> argparse.ArgumentParser: type=int, help="Limit each symbol list to the first N matching rows", ) + unit.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help="Pass through objdiff relocation diff mode to decomp-diff.py", + ) unit.set_defaults(func=command_unit) next_cmd = subparsers.add_parser( @@ -804,6 +824,12 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Don't collapse matching instruction runs", ) + diff.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help="Pass through objdiff relocation diff mode to decomp-diff.py", + ) diff.set_defaults(func=command_diff) return parser From 41b71d55b36299e0b2f27847f53b389deaec07db Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 14:34:54 +0100 Subject: [PATCH 20/71] docs: note stub-header ownership cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 5aafbce35..d5ec45a39 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -99,6 +99,7 @@ Foo::Foo() - Prefer including the owning repo header over adding a local forward declaration for a project type. - If the repo already has a header declaration/definition for a type, include that header instead of redeclaring the type locally. +- If the repo only has an empty or stub owner header, and line info / surrounding source clearly points at that header's subsystem, prefer populating that owner header over leaving a recovered project type declaration inside a `.cpp`. - Only keep a local forward declaration when no canonical repo header exists yet and you have verified that the ownership is still unresolved. - Prefer moving helper template declarations next to their real use site instead of leaving them in an unrelated file. diff --git a/AGENTS.md b/AGENTS.md index 0c400cca5..39b682fe7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -281,6 +281,7 @@ This is a **C++98** codebase compiled with ProDG GC 3.9.3 (GCC 2.95 under the ho - Inline assembly is acceptable when needed to reproduce dead code or compiler scheduling that source alone cannot express cleanly - Preserve the original `class` vs `struct` kind. Check existing headers first, then Dwarf / PS2 info when needed. Even forward declarations and local partial declarations should use the accurate keyword when known. - Prefer including the real repo header over introducing a local forward declaration for a project type. If a type already has a header in `src/`, include it instead of redeclaring it locally. +- If a subsystem already has a stub owner header and the debug line info points back at that subsystem, fill the owner header instead of keeping a recovered project type declaration in a `.cpp`. - Preserve original member names, types, order, and proven layout comments. Do not invent `pad`, `unk`, or `field_XXXX` members just to satisfy a guessed size or offset; verify the real members with `find-symbol.py`, GC Dwarf, and PS2 data, and leave a short TODO if a layout detail is still uncertain. - Follow DWARF member naming exactly (`mMember` vs `m_member`) instead of normalizing names - Omit the `this` pointer. From b37777749ad8f153e8763bb4b7ce8a078fad8a21 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 15:20:30 +0100 Subject: [PATCH 21/71] tools: document match-safe null sweeps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 1 + tools/code_style.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index d5ec45a39..950d7c6ee 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -93,6 +93,7 @@ Foo::Foo() - Spell casts without spaces inside the angle brackets: `static_cast(expr)`, not `static_cast< Type * >(expr)`. - Use `nullptr` exclusively for null pointers. - Prefer `if (ptr)` / `if (!ptr)` over explicit null comparisons when the change is local and verified safe. +- When a match-sensitive TU has many explicit `nullptr` checks and you decide to normalize them, prefer one mechanical full-TU pass over piecemeal cleanup. Rebuild the unit and re-check its status before keeping the rewrite. - Inline assembly is acceptable when it is needed to preserve dead-code compares, ordering, or other compiler behavior that source alone cannot reproduce. ### Forward declarations and local prototypes diff --git a/AGENTS.md b/AGENTS.md index 39b682fe7..5a55e5e13 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -286,6 +286,7 @@ This is a **C++98** codebase compiled with ProDG GC 3.9.3 (GCC 2.95 under the ho - Follow DWARF member naming exactly (`mMember` vs `m_member`) instead of normalizing names - Omit the `this` pointer. - Use `nullptr` and `override`. If they are missing, you need to include `types.h`. +- Prefer `if (ptr)` / `if (!ptr)` over explicit `nullptr` comparisons. In match-sensitive translation units, if you choose to normalize many of them, do it as one mechanical TU-wide pass and then rebuild / re-check that unit instead of assuming a piecemeal cleanup is free. - Omit `struct` when declaring variables or parameters, we are not in C land. - Avoid using `using` directives at all cost. Since the game uses jumbo builds, they leak through files. diff --git a/tools/code_style.py b/tools/code_style.py index 1f14f7c72..a1a8f83fa 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -585,7 +585,7 @@ def audit_match_sensitive_cpp( path, idx, "INFO", - "pointer-null comparison is a candidate for `if (ptr)` cleanup, but verify the affected TU first", + "pointer-null comparison is a candidate for `if (ptr)` cleanup; in match-sensitive code, prefer a mechanical full-TU pass and then rebuild/status-check that unit", ) ) return findings From ebaa4a4ca6fe4886ad64d51af0fc7d43ce2c0ede Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 17:37:15 +0100 Subject: [PATCH 22/71] Improve DWARF verification workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 14 + .github/skills/implement/SKILL.md | 32 +- .github/skills/refiner/SKILL.md | 20 +- AGENTS.md | 30 ++ tools/decomp-workflow.py | 223 +++++++++++++ tools/dwarf-compare.py | 517 ++++++++++++++++++++++++++++++ tools/lookup.py | 34 +- 7 files changed, 863 insertions(+), 7 deletions(-) create mode 100644 tools/dwarf-compare.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index c4366f2a6..b2cf62247 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -9,6 +9,8 @@ Your goal is to decompile a full translation unit: understand the current state, scaffold any missing classes if needed, then match the unit function by function until the produced C++ compiles to byte-identical object code against the original retail binary. +For each function, "done" means both objdiff and normalized DWARF are exact. + ## Overview This workflow combines several smaller workflows: @@ -113,6 +115,7 @@ For each missing or nonmatching function, follow the implementation workflow in - Branch structure mismatches indicate wrong control flow (if/switch/loop) - **Match percentage is misleading.** The last few percent are often the hardest. Treat 95% as unfinished; the goal is 100%. +- **DWARF is equally mandatory.** A 100% objdiff function with a DWARF mismatch is still unfinished. ### 3d. Collect and propagate matching tips @@ -135,6 +138,15 @@ Use `python tools/decomp-workflow.py function ...` or `python tools/decomp-workflow.py diff ...` when you want a shorter, wrapper-first view for one function. +After each function-level edit pass, run: + +```sh +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +If it fails, follow up with `decomp-workflow.py diff` and `decomp-workflow.py dwarf` +until both checks pass. + ### 3g. Periodic reassessment After every few functions, re-run the full status check: @@ -171,6 +183,8 @@ fallback. For any remaining nonmatching functions, make one final pass using the implementation or refiner workflow with all context accumulated during the session. +Do not report a function as complete unless its per-function `verify` check also passes. + ## Phase 5: Report Summarize the session: diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 95ad95016..b51773f02 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -7,6 +7,8 @@ description: Workflow for decompiling and iterating on a function. Your goal is to decompile a specific function: writing C++ source that compiles to byte-identical object code against the original retail binary, verified via `decomp-diff.py`. +A function is not done until it is exact in both objdiff and normalized DWARF. + ## Phase 1: Gather Context Collect data from **all** of these sources in parallel where possible. @@ -145,6 +147,7 @@ For a rebuild plus a standardized diff run, use: ```sh python tools/decomp-workflow.py build -u main/Path/To/TU python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName ``` If the build fails, fix compilation errors first. @@ -172,7 +175,28 @@ Refer to the **Matching Tips** section in AGENTS.md for detailed patterns on resolving instruction mismatches, register allocation issues, stack frame differences, and symbol naming. -After writing your code, occasionally run the dwarf dump on the compiled output and then query your output dump with lookup.py to compare your decompiled functions against the originals. Since the address of the function you're working on can keep changing +After each meaningful edit/build iteration, run the combined verification gate first: + +Preferred shortcut: + +```sh +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +This fails unless both the instruction diff and normalized DWARF are exact. + +If the verify gate fails because of DWARF, inspect the DWARF block diff directly: + +```sh +python tools/decomp-workflow.py dwarf -u main/Path/To/TU -f FunctionName +``` + +This gives you a normalized DWARF match percentage plus a diff-like report of what still +differs between the original and rebuilt DWARF blocks for that function. + +Manual fallback: + +After writing your code, you can also run the dwarf dump on the compiled output and then query your output dump with lookup.py to compare your decompiled functions against the originals. Since the address of the function you're working on can keep changing due to work on other functions, query the unmangled name instead. ```bash @@ -193,17 +217,19 @@ Repeat the build-diff cycle until the diff shows 100% match with no `~` lines: ```sh python tools/decomp-workflow.py build -u main/Path/To/TU -python tools/decomp-workflow.py diff -u main/Path/To/TU -d FunctionName +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName ``` Every mismatched instruction is a signal — don't settle for "close enough". -Reaching 100% matching status is not enough, also make sure that the dwarf of the function matches the original. +Reaching 100% instruction matching status is not enough. Stay in the loop until `verify` +passes, which means the DWARF of the function also matches after normalization. ## Phase 5: Report Summarize: - Final match status (percentage, instruction count) +- Final DWARF status (exact or remaining mismatch summary) - What the function does (brief description) - Key decisions or tricky patterns used to achieve the match - If not fully matching, document remaining mismatches and theories diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md index 82b2a2608..4d1fe1bb7 100644 --- a/.github/skills/refiner/SKILL.md +++ b/.github/skills/refiner/SKILL.md @@ -115,7 +115,23 @@ sequences on PPC (see `xoris` pattern in AGENTS.md). Check all casts. ## Phase 3: DWARF verification -After any instruction match, verify the DWARF also matches. +After any instruction match, verify the DWARF also matches. The function is not done +until both objdiff and normalized DWARF are exact. + +Preferred shortcut: + +```bash +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +If the combined gate fails because of DWARF, inspect the DWARF diff directly with: + +```bash +python tools/decomp-workflow.py dwarf -u main/Path/To/TU -f FunctionName +``` + +Manual fallback: + Use the rebuilt shared object from Phase 1 (or rebuild again if you've changed the source): ```bash @@ -143,5 +159,5 @@ Summarize: - What was blocking the match (the root cause category from Phase 1) - The specific source change that resolved it - Any new generalizable assembly pattern discovered (add to AGENTS.md if so) -- DWARF match status +- DWARF match status and whether `verify` passes - If still not matching: the exact diff lines that remain and your best theory diff --git a/AGENTS.md b/AGENTS.md index 5a55e5e13..ff958229f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,6 +156,7 @@ python tools/decomp-workflow.py build -u main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zAnim -d FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --brief +python tools/decomp-workflow.py verify -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --ghidra-version gc python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zAnim -f FindIOWin --lookup-mode full python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zAnim --search FindIOWin --limit 20 @@ -197,6 +198,31 @@ real content. Add `--brief` when you want to keep the helper sections compact; it trims suggested commands and related-source hints without hiding the core status/diff/source data. +For every function you touch, treat DWARF as a first-class completion gate, not a +secondary polish pass. After each meaningful code/build iteration, run the wrapper's +combined verification flow: + +```sh +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +`verify` fails unless **both** checks are exact for that function: + +- objdiff instruction match is 100% +- normalized DWARF block match is exact + +If the combined check fails, then inspect the DWARF diff directly with: + +```sh +python tools/decomp-workflow.py dwarf -u main/Path/To/TU -f FunctionName +``` + +It compares the original and rebuilt DWARF blocks for one function, prints a normalized +DWARF match percentage, and shows a diff-like view of what still differs. Use it +whenever `verify` says the function is still failing the DWARF gate. This is the +fastest way to see whether you are still missing locals, have the wrong inline body, or +changed signature/type details even when the instruction diff already looks good. + When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work faster, shorter, or more reliable, prefer implementing that improvement as part of the task @@ -334,6 +360,10 @@ You may use sub-agents to gather read-only context during this process, but they edit files. Treat their output as analysis input for the main worker, not as a path to delegate source changes. +A function is only done when both objdiff and normalized DWARF are exact. Treat a +100% instruction match with a DWARF mismatch as unfinished work, not a near-complete +result. + The dwarf of your structs doesn't have to neccessarily match the original due to various reasons, just make sure that you copied everything correctly. Never dismiss a diff as "close enough" or "just register allocation." Every mismatched diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index df90eff4a..9193425cd 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -15,6 +15,9 @@ python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-lookup python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-source python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zCamera -d UpdateAll --reloc-diffs all + python tools/decomp-workflow.py dwarf -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll + python tools/decomp-workflow.py dwarf -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' --full-diff + python tools/decomp-workflow.py verify -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zCamera """ @@ -33,10 +36,12 @@ RELOC_DIFF_CHOICES, ROOT_DIR, ToolError, + build_objdiff_symbol_rows, ensure_exists, find_objdiff_unit, load_objdiff_config, make_abs, + run_objdiff_json, ) @@ -228,6 +233,77 @@ def describe_path(path: str) -> str: return "present" +def fuzzy_match(pattern: str, name: str) -> bool: + return pattern.lower() in name.lower() + + +def find_objdiff_rows_for_function( + unit_name: str, function_name: str, reloc_diffs: str = "none" +) -> List[Dict[str, Any]]: + data = run_objdiff_json( + OBJDIFF_CLI, + unit_name, + reloc_diffs=reloc_diffs, + root_dir=ROOT_DIR, + ) + rows = [ + row + for row in build_objdiff_symbol_rows(data) + if row["type"] == "function" + ] + + exact_matches = [ + row + for row in rows + if function_name in row["name"] or function_name in row["symbol_name"] + ] + if exact_matches: + return exact_matches + + return [ + row + for row in rows + if fuzzy_match(function_name, row["name"]) + or fuzzy_match(function_name, row["symbol_name"]) + ] + + +def choose_objdiff_row(unit_name: str, function_name: str, reloc_diffs: str = "none") -> Dict[str, Any]: + matches = find_objdiff_rows_for_function(unit_name, function_name, reloc_diffs=reloc_diffs) + if not matches: + raise WorkflowError( + f"objdiff: function '{function_name}' not found in {unit_name}.\n" + "Hint: run `python tools/decomp-workflow.py unit -u " + f"{unit_name} --search {shlex.quote(function_name)}` to inspect nearby symbols." + ) + + if len(matches) > 1: + preview = "\n".join(f" - {row['name']}" for row in matches[:8]) + extra = "" + if len(matches) > 8: + extra = f"\n ... {len(matches) - 8} more" + raise WorkflowError( + f"objdiff: function query '{function_name}' matched multiple symbols in {unit_name}.\n" + f"Use a more specific function name.\n{preview}{extra}" + ) + return matches[0] + + +def load_dwarf_report( + unit_name: str, + function_name: str, + rebuilt_dwarf_file: Optional[str] = None, +) -> Dict[str, Any]: + cmd: List[str] = python_tool("dwarf-compare.py", "-u", unit_name, "-f", function_name, "--json") + if rebuilt_dwarf_file: + cmd.extend(["--rebuilt-dwarf-file", rebuilt_dwarf_file]) + result = run_capture(cmd) + try: + return json.loads(result.stdout) + except json.JSONDecodeError as e: + raise WorkflowError(f"dwarf-compare.py returned invalid JSON: {e}") + + def lookup_symbol_address(symbols_file: str, mangled_name: str) -> Optional[str]: if not os.path.exists(symbols_file): return None @@ -512,6 +588,12 @@ def command_function(args: argparse.Namespace) -> None: if args.reloc_diffs != "none": cmd.extend(["--reloc-diffs", args.reloc_diffs]) run_stream(cmd) + print(flush=True) + print( + "Required completion check: python tools/decomp-workflow.py verify " + f"-u {shlex.quote(args.unit)} -f {shlex.quote(args.function)}", + flush=True, + ) def command_unit(args: argparse.Namespace) -> None: @@ -653,6 +735,88 @@ def command_diff(args: argparse.Namespace) -> None: run_stream(cmd) +def command_dwarf(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + print_section(f"DWARF Workflow: {args.unit} / {args.function}") + if not args.rebuilt_dwarf_file: + ensure_shared_unit_output(args.unit) + + cmd: List[str] = python_tool("dwarf-compare.py", "-u", args.unit, "-f", args.function) + if args.summary: + cmd.append("--summary") + if args.json: + cmd.append("--json") + if args.context is not None: + cmd.extend(["-C", str(args.context)]) + if args.no_collapse: + cmd.append("--no-collapse") + if args.require_exact: + cmd.append("--require-exact") + if args.rebuilt_dwarf_file: + cmd.extend(["--rebuilt-dwarf-file", args.rebuilt_dwarf_file]) + run_stream(cmd) + + +def command_verify(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + print_section(f"Verify Workflow: {args.unit} / {args.function}") + ensure_shared_unit_output(args.unit) + + objdiff_row = choose_objdiff_row(args.unit, args.function, reloc_diffs=args.reloc_diffs) + dwarf_report = load_dwarf_report( + args.unit, + args.function, + rebuilt_dwarf_file=args.rebuilt_dwarf_file, + ) + + objdiff_exact = ( + objdiff_row["status"] == "match" + and objdiff_row["match_percent"] is not None + and float(objdiff_row["match_percent"]) >= 100.0 + ) + dwarf_exact = bool(dwarf_report["normalized_exact_match"]) + overall_ok = objdiff_exact and dwarf_exact + + objdiff_percent = ( + f"{float(objdiff_row['match_percent']):.1f}%" + if objdiff_row["match_percent"] is not None + else "-" + ) + dwarf_percent = f"{float(dwarf_report['match_percent']):.1f}%" + + print( + f"objdiff: {'PASS' if objdiff_exact else 'FAIL'} | " + f"{objdiff_percent} | status={objdiff_row['status']} | " + f"unmatched~{objdiff_row['unmatched_bytes_est']}B" + ) + print( + f"DWARF: {'PASS' if dwarf_exact else 'FAIL'} | " + f"{dwarf_percent} | normalized exact={'yes' if dwarf_exact else 'no'} | " + f"change groups={dwarf_report['changed_groups']}" + ) + print(f"Overall: {'PASS' if overall_ok else 'FAIL'}") + print("Done means both objdiff and normalized DWARF are exact for the function.") + + if overall_ok: + return + + print(flush=True) + print("Follow-up commands:", flush=True) + print( + f" python tools/decomp-workflow.py diff -u {shlex.quote(args.unit)} " + f"-d {shlex.quote(args.function)}", + flush=True, + ) + print( + f" python tools/decomp-workflow.py dwarf -u {shlex.quote(args.unit)} " + f"-f {shlex.quote(args.function)}", + flush=True, + ) + raise WorkflowError( + "Verification failed: the function is not complete until both objdiff and DWARF match." + ) + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( @@ -832,6 +996,65 @@ def build_parser() -> argparse.ArgumentParser: ) diff.set_defaults(func=command_diff) + dwarf = subparsers.add_parser( + "dwarf", + help="Compare original vs rebuilt DWARF for one function", + ) + dwarf.add_argument("-u", "--unit", required=True, help="Translation unit name") + dwarf.add_argument("-f", "--function", required=True, help="Function name to compare") + dwarf.add_argument( + "--summary", + action="store_true", + help="Print only the DWARF summary without the diff view", + ) + dwarf.add_argument( + "--json", + action="store_true", + help="Print the DWARF comparison report as JSON", + ) + dwarf.add_argument( + "-C", + "--context", + type=int, + default=3, + help="Context lines around collapsed matching DWARF runs (default: 3)", + ) + dwarf.add_argument( + "--no-collapse", + "--full-diff", + dest="no_collapse", + action="store_true", + help="Show the whole normalized DWARF block with diff markers instead of collapsing matching runs", + ) + dwarf.add_argument( + "--rebuilt-dwarf-file", + help="Use an existing rebuilt DWARF dump instead of dumping the unit object", + ) + dwarf.add_argument( + "--require-exact", + action="store_true", + help="Exit non-zero unless the normalized DWARF block matches exactly", + ) + dwarf.set_defaults(func=command_dwarf) + + verify = subparsers.add_parser( + "verify", + help="Fail unless one function matches in both objdiff and DWARF", + ) + verify.add_argument("-u", "--unit", required=True, help="Translation unit name") + verify.add_argument("-f", "--function", required=True, help="Function name to verify") + verify.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help="Pass through objdiff relocation diff mode when checking instruction match", + ) + verify.add_argument( + "--rebuilt-dwarf-file", + help="Use an existing rebuilt DWARF dump instead of dumping the unit object", + ) + verify.set_defaults(func=command_verify) + return parser diff --git a/tools/dwarf-compare.py b/tools/dwarf-compare.py new file mode 100644 index 000000000..51f35ea9b --- /dev/null +++ b/tools/dwarf-compare.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 + +""" +Compare the original DWARF for one function against the rebuilt DWARF from a unit object. + +Examples: + python tools/dwarf-compare.py -u main/Speed/Indep/SourceLists/zCamera -f "Camera::UpdateAll(float)" + python tools/dwarf-compare.py -u main/Speed/Indep/SourceLists/zAI -f "AIPursuit::AIPursuit(Sim::Param)" --summary + python tools/dwarf-compare.py -u main/Speed/Indep/SourceLists/zAttribSys -f "Attrib::Class::RemoveCollection(Attrib::Collection *)" --full-diff + python tools/dwarf-compare.py -u main/Speed/Indep/SourceLists/zCamera -f "Camera::UpdateAll(float)" --require-exact +""" + +import argparse +import difflib +import json +import os +import re +import sys +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple + +from _common import ROOT_DIR, ToolError, find_objdiff_unit, load_objdiff_config, make_abs +from lookup import ( + _candidate_func_names, + _normalise_func_name, + _sig_contains_name, + read_text, + split_functions, +) + + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +TOOLS_DIR = os.path.join(ROOT_DIR, "tools") +GC_DWARF = os.path.join(ROOT_DIR, "symbols", "Dwarf") +DTK = os.path.join(ROOT_DIR, "build", "tools", "dtk") +HEX_RE = re.compile(r"0x[0-9A-Fa-f]+") + + +class DwarfCompareError(RuntimeError): + pass + + +FunctionBlock = Tuple[str, str, str, str] + + +def tool_path(name: str) -> str: + return os.path.join(TOOLS_DIR, name) + + +def print_section(title: str) -> None: + print(flush=True) + print("=" * 60, flush=True) + print(f" {title}", flush=True) + print("=" * 60, flush=True) + + +def format_failure( + cmd: Sequence[str], returncode: int, stdout: str = "", stderr: str = "" +) -> str: + message = [f"Command failed (exit {returncode}): {' '.join(cmd)}"] + stdout = stdout.strip() + stderr = stderr.strip() + if stdout: + message.append(f"stdout:\n{stdout}") + if stderr: + message.append(f"stderr:\n{stderr}") + return "\n".join(message) + + +def maybe_remove(path: Optional[str]) -> None: + if not path: + return + try: + if os.path.exists(path): + os.remove(path) + except OSError as e: + print(f"Warning: failed to remove temporary file {path}: {e}", file=sys.stderr) + + +def get_unit_build_output(unit_name: str) -> str: + config = load_objdiff_config() + unit = find_objdiff_unit(config, unit_name) + if unit is None: + raise DwarfCompareError(f"Unit not found in objdiff.json: {unit_name}") + + target = unit.get("base_path") or unit.get("target_path") + if not target: + raise DwarfCompareError(f"Unit has no build target in objdiff.json: {unit_name}") + return make_abs(str(target)) or str(target) + + +def dtk_dwarf_dump(obj_path: str) -> str: + import tempfile + import subprocess + + fd, output_path = tempfile.mkstemp(prefix="nfsmw_dwarf_compare_", suffix=".nothpp") + os.close(fd) + maybe_remove(output_path) + + result = subprocess.run( + [DTK, "dwarf", "dump", obj_path, "-o", output_path], + cwd=ROOT_DIR, + text=True, + capture_output=True, + ) + if result.returncode != 0: + maybe_remove(output_path) + raise DwarfCompareError( + format_failure( + [DTK, "dwarf", "dump", obj_path, "-o", output_path], + result.returncode, + result.stdout, + result.stderr, + ) + ) + + tool_output = "\n".join( + part.strip() for part in [result.stdout, result.stderr] if part.strip() + ) + if "ERROR " in tool_output or tool_output.startswith("ERROR"): + maybe_remove(output_path) + raise DwarfCompareError( + f"dtk reported an error while dumping DWARF:\n{tool_output}" + ) + + if not os.path.exists(output_path): + raise DwarfCompareError("dtk dwarf dump succeeded but did not write an output file") + + return output_path + + +def load_function_blocks(path: str, folder_mode: bool) -> List[FunctionBlock]: + if folder_mode: + text = read_text(os.path.join(path, "functions.nothpp")) + else: + text = read_text(path) + return split_functions(text) + + +def find_function_blocks(funcs: Iterable[FunctionBlock], query: str) -> List[FunctionBlock]: + candidates = _candidate_func_names(query) + matches: List[FunctionBlock] = [] + exact_substring_matches: List[FunctionBlock] = [] + + for func in funcs: + sig_line = func[2] + if query in sig_line: + exact_substring_matches.append(func) + if any(_sig_contains_name(sig_line, candidate) for candidate in candidates): + matches.append(func) + + if exact_substring_matches: + return exact_substring_matches + return matches + + +def last_name_token(query: str) -> str: + bare = _normalise_func_name(query) + if "::" in bare: + return bare.split("::")[-1] + return bare + + +def find_similar_signatures( + funcs: Sequence[FunctionBlock], query: str, limit: int = 8 +) -> List[str]: + token = last_name_token(query) + token_matches: List[str] = [] + seen = set() + + for _, _, sig_line, _ in funcs: + if token and token in sig_line and sig_line not in seen: + token_matches.append(sig_line) + seen.add(sig_line) + if len(token_matches) >= limit: + return token_matches + + choices = [sig_line for _, _, sig_line, _ in funcs if sig_line] + for sig_line in difflib.get_close_matches(query, choices, n=limit, cutoff=0.35): + if sig_line not in seen: + token_matches.append(sig_line) + seen.add(sig_line) + if len(token_matches) >= limit: + break + return token_matches + + +def choose_function_block( + funcs: List[FunctionBlock], query: str, label: str +) -> FunctionBlock: + matches = find_function_blocks(funcs, query) + if not matches: + if not funcs: + raise DwarfCompareError( + f"{label}: function '{query}' not found.\n" + "The scanned DWARF source contains no top-level function blocks." + ) + + similar = find_similar_signatures(funcs, query) + details = [ + f"{label}: function '{query}' not found.", + f"Scanned {len(funcs)} top-level function block(s).", + ] + if similar: + details.append("Closest signatures:") + details.extend(f" - {sig}" for sig in similar) + raise DwarfCompareError("\n".join(details)) + if len(matches) > 1: + signatures = "\n".join(f" - {match[2]}" for match in matches[:8]) + extra = "" + if len(matches) > 8: + extra = f"\n ... {len(matches) - 8} more" + raise DwarfCompareError( + f"{label}: function query '{query}' matched multiple DWARF blocks.\n" + f"Use a more specific function name.\n{signatures}{extra}" + ) + return matches[0] + + +def normalize_line(line: str) -> str: + stripped = line.rstrip("\n").rstrip() + if stripped.startswith("// Range:"): + return "// Range: " + return HEX_RE.sub("0xADDR", stripped) + + +def normalize_block(block: str) -> List[str]: + return [normalize_line(line) for line in block.splitlines()] + + +def count_lines_for_opcodes(opcodes: Sequence[Tuple[str, int, int, int, int]]) -> Dict[str, int]: + matching = 0 + original_only = 0 + rebuilt_only = 0 + changed_groups = 0 + for tag, i1, i2, j1, j2 in opcodes: + if tag == "equal": + matching += i2 - i1 + continue + changed_groups += 1 + if tag in ("replace", "delete"): + original_only += i2 - i1 + if tag in ("replace", "insert"): + rebuilt_only += j2 - j1 + return { + "matching_lines": matching, + "original_only_lines": original_only, + "rebuilt_only_lines": rebuilt_only, + "changed_groups": changed_groups, + } + + +def build_diff_lines( + original_lines: Sequence[str], + rebuilt_lines: Sequence[str], + function_name: str, + context: int, + collapse: bool, +) -> List[str]: + if list(original_lines) == list(rebuilt_lines): + return [] + + rendered: List[str] = [ + f"--- original:{function_name}", + f"+++ rebuilt:{function_name}", + ] + + matcher = difflib.SequenceMatcher(a=original_lines, b=rebuilt_lines) + for group in matcher.get_grouped_opcodes(context if collapse else max(len(original_lines), len(rebuilt_lines))): + first = group[0] + last = group[-1] + a_start = first[1] + 1 + a_len = last[2] - first[1] + b_start = first[3] + 1 + b_len = last[4] - first[3] + rendered.append(f"@@ -{a_start},{a_len} +{b_start},{b_len} @@") + + for tag, i1, i2, j1, j2 in group: + if tag == "equal": + for idx in range(i1, i2): + rendered.append(f" L{idx + 1:04d} {original_lines[idx]}") + continue + + if tag in ("replace", "delete"): + for idx in range(i1, i2): + rendered.append(f"- L{idx + 1:04d} {original_lines[idx]}") + if tag in ("replace", "insert"): + for idx in range(j1, j2): + rendered.append(f"+ L{idx + 1:04d} {rebuilt_lines[idx]}") + + return rendered + + +def build_report( + unit_name: str, + function_name: str, + original_block: FunctionBlock, + rebuilt_block: FunctionBlock, + collapse: bool, + context: int, +) -> Dict[str, Any]: + original_raw = original_block[3].splitlines() + rebuilt_raw = rebuilt_block[3].splitlines() + original_lines = normalize_block(original_block[3]) + rebuilt_lines = normalize_block(rebuilt_block[3]) + + matcher = difflib.SequenceMatcher(a=original_lines, b=rebuilt_lines) + opcodes = matcher.get_opcodes() + counts = count_lines_for_opcodes(opcodes) + total_lines = max(len(original_lines), len(rebuilt_lines), 1) + match_percent = 100.0 * counts["matching_lines"] / total_lines + signature_match = normalize_line(original_block[2]) == normalize_line(rebuilt_block[2]) + raw_exact_match = original_raw == rebuilt_raw + normalized_exact_match = original_lines == rebuilt_lines + + diff_lines = build_diff_lines( + original_lines, + rebuilt_lines, + function_name, + context=context, + collapse=collapse, + ) + mismatch_summaries: List[str] = [] + for tag, i1, i2, j1, j2 in opcodes: + if tag == "equal": + continue + original_span = ( + f"L{i1 + 1:04d}" if i2 - i1 <= 1 else f"L{i1 + 1:04d}-L{i2:04d}" + ) if tag in ("replace", "delete") else "-" + rebuilt_span = ( + f"L{j1 + 1:04d}" if j2 - j1 <= 1 else f"L{j1 + 1:04d}-L{j2:04d}" + ) if tag in ("replace", "insert") else "-" + + if tag == "replace" and i2 - i1 == 1 and j2 - j1 == 1: + detail = f"{original_lines[i1]} -> {rebuilt_lines[j1]}" + elif tag == "delete": + detail = f"removed {i2 - i1} original line(s)" + elif tag == "insert": + detail = f"added {j2 - j1} rebuilt line(s)" + else: + detail = ( + f"replaced {i2 - i1} original line(s) with " + f"{j2 - j1} rebuilt line(s)" + ) + mismatch_summaries.append( + f"- {original_span} -> {rebuilt_span}: {detail}" + ) + + return { + "unit": unit_name, + "function": function_name, + "match_percent": match_percent, + "matching_lines": counts["matching_lines"], + "total_lines": total_lines, + "original_line_count": len(original_lines), + "rebuilt_line_count": len(rebuilt_lines), + "original_only_lines": counts["original_only_lines"], + "rebuilt_only_lines": counts["rebuilt_only_lines"], + "changed_groups": counts["changed_groups"], + "signature_match": signature_match, + "normalized_exact_match": normalized_exact_match, + "raw_exact_match": raw_exact_match, + "original_signature": original_block[2], + "rebuilt_signature": rebuilt_block[2], + "original_range": [original_block[0], original_block[1]], + "rebuilt_range": [rebuilt_block[0], rebuilt_block[1]], + "mismatch_summaries": mismatch_summaries, + "diff_lines": diff_lines, + } + + +def print_summary(report: Dict[str, Any]) -> None: + print_section(f"DWARF Match: {report['function']}") + print(f"Unit: {report['unit']}") + print( + f"Normalized DWARF match: {report['match_percent']:.1f}% " + f"({report['matching_lines']}/{report['total_lines']} lines)" + ) + print( + f"Signature: {'match' if report['signature_match'] else 'mismatch'} | " + f"Change groups: {report['changed_groups']} | " + f"Original-only lines: {report['original_only_lines']} | " + f"Rebuilt-only lines: {report['rebuilt_only_lines']}" + ) + print( + f"Normalized exact match: {'yes' if report['normalized_exact_match'] else 'no'}" + ) + if report["normalized_exact_match"] and not report["raw_exact_match"]: + print("Raw textual exact match: no (only raw addresses/ranges differ)") + else: + print(f"Raw textual exact match: {'yes' if report['raw_exact_match'] else 'no'}") + print( + "Address-only range differences are normalized out so the percentage tracks " + "structural/function-body DWARF changes." + ) + if not report["signature_match"]: + print() + print("Original signature:") + print(f" {report['original_signature']}") + print("Rebuilt signature:") + print(f" {report['rebuilt_signature']}") + + +def print_diff(report: Dict[str, Any]) -> None: + if report["mismatch_summaries"]: + print_section("Mismatch Summary") + for line in report["mismatch_summaries"]: + print(line) + print_section("DWARF Diff") + if not report["diff_lines"]: + print("No DWARF differences after normalization.") + return + for line in report["diff_lines"]: + print(line) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Compare original and rebuilt DWARF for one function and show a " + "normalized line-match report plus a diff-like view." + ) + ) + parser.add_argument("-u", "--unit", required=True, help="Translation unit name") + parser.add_argument("-f", "--function", required=True, help="Function name to compare") + parser.add_argument( + "--summary", + action="store_true", + help="Print only the summary header without the diff view", + ) + parser.add_argument( + "--json", + action="store_true", + help="Print the report as JSON", + ) + parser.add_argument( + "-C", + "--context", + type=int, + default=3, + help="Context lines to keep around collapsed matching runs (default: 3)", + ) + parser.add_argument( + "--no-collapse", + "--full-diff", + dest="no_collapse", + action="store_true", + help="Show the whole normalized DWARF block with diff markers instead of collapsing matching runs", + ) + parser.add_argument( + "--rebuilt-dwarf-file", + help="Use an existing rebuilt DWARF dump instead of dumping the unit object", + ) + parser.add_argument( + "--require-exact", + action="store_true", + help="Exit non-zero unless the normalized DWARF block matches exactly", + ) + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + rebuilt_dwarf_path: Optional[str] = None + cleanup_rebuilt_dwarf = False + try: + if args.rebuilt_dwarf_file: + rebuilt_dwarf_path = os.path.abspath(args.rebuilt_dwarf_file) + else: + obj_path = get_unit_build_output(args.unit) + if not os.path.exists(obj_path): + raise DwarfCompareError( + f"Missing built object for {args.unit}: {obj_path}\n" + f"Hint: run `python tools/decomp-workflow.py build -u {args.unit}` " + "or use the wrapper `python tools/decomp-workflow.py dwarf ...`." + ) + rebuilt_dwarf_path = dtk_dwarf_dump(obj_path) + cleanup_rebuilt_dwarf = True + + original_funcs = load_function_blocks(GC_DWARF, folder_mode=True) + rebuilt_funcs = load_function_blocks(rebuilt_dwarf_path, folder_mode=False) + + original_block = choose_function_block(original_funcs, args.function, "original DWARF") + rebuilt_block = choose_function_block(rebuilt_funcs, args.function, "rebuilt DWARF") + + report = build_report( + args.unit, + args.function, + original_block, + rebuilt_block, + collapse=not args.no_collapse, + context=args.context, + ) + + if args.json: + print(json.dumps(report, indent=2)) + if args.require_exact and not report["normalized_exact_match"]: + sys.exit(1) + return + + print_summary(report) + if not args.summary: + print_diff(report) + if args.require_exact and not report["normalized_exact_match"]: + sys.exit(1) + + except (DwarfCompareError, ToolError) as e: + print(e, file=sys.stderr) + sys.exit(1) + finally: + if cleanup_rebuilt_dwarf: + maybe_remove(rebuilt_dwarf_path) + + +if __name__ == "__main__": + main() diff --git a/tools/lookup.py b/tools/lookup.py index e8188bfca..cecbae991 100644 --- a/tools/lookup.py +++ b/tools/lookup.py @@ -248,6 +248,32 @@ def _normalise_func_name(name: str) -> str: return name.strip() +def _candidate_func_names(name: str) -> list[str]: + """ + Generate progressively shorter qualified-name suffixes. + + Example: + Attrib::Class::RemoveCollection -> [ + 'Attrib::Class::RemoveCollection', + 'Class::RemoveCollection', + 'RemoveCollection', + ] + + This helps match DWARF signatures that omit leading namespaces. + """ + bare = _normalise_func_name(name) + if not bare: + return [] + + parts = bare.split("::") + candidates: list[str] = [] + for index in range(len(parts)): + candidate = "::".join(parts[index:]).strip() + if candidate and candidate not in candidates: + candidates.append(candidate) + return candidates + + def _sig_contains_name(sig_line: str, bare_name: str) -> bool: """ Return True if *bare_name* (e.g. 'EPerfectLaunch::~EPerfectLaunch') appears @@ -291,8 +317,12 @@ def find_functions_by_name( funcs: list[tuple[str, str, str, str]], query: str ) -> list[str]: """Return all function blocks whose signature matches *query* (ignoring params).""" - bare = _normalise_func_name(query) - return [block for start, end, sig, block in funcs if _sig_contains_name(sig, bare)] + candidates = _candidate_func_names(query) + return [ + block + for start, end, sig, block in funcs + if any(_sig_contains_name(sig, candidate) for candidate in candidates) + ] # --------------------------------------------------------------------------- From cc4c6904c9e179b74ef1039e08f579d698a5db2a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 17:38:03 +0100 Subject: [PATCH 23/71] - again --- tools/build-unit.py | 324 -------------------------------------------- 1 file changed, 324 deletions(-) delete mode 100644 tools/build-unit.py diff --git a/tools/build-unit.py b/tools/build-unit.py deleted file mode 100644 index dbfacf7b7..000000000 --- a/tools/build-unit.py +++ /dev/null @@ -1,324 +0,0 @@ -#!/usr/bin/env python3 - -""" -Compile a single translation unit to a temporary (or specified) object file. - -Uses `ninja -t compdb` to extract the exact compile command for the unit, then -redirects the output to a private path so parallel agents never overwrite each -other's work. - -Usage: - # Auto-generate a temp path (printed to stdout): - python tools/build-unit.py -u main/Speed/Indep/SourceLists/zAnim - - # Compile to an explicit path: - python tools/build-unit.py -u main/Speed/Indep/SourceLists/zAnim -o /tmp/my.o - -The path of the compiled .o is always printed to stdout on success so it can be -captured with command substitution: - - TEMPOBJ=$(python tools/build-unit.py -u main/Speed/Indep/SourceLists/zAnim) - python tools/decomp-diff.py -u main/Speed/Indep/SourceLists/zAnim -d MyFunc --base-obj "$TEMPOBJ" - dtk dwarf dump "$TEMPOBJ" -o /tmp/my_dwarf.nothpp -""" - -import argparse -import json -import os -import re -import subprocess -import sys -import tempfile -from typing import Any, Dict, List, Optional, Tuple, Union - -script_dir = os.path.dirname(os.path.realpath(__file__)) -root_dir = os.path.abspath(os.path.join(script_dir, "..")) -OBJDIFF_JSON = os.path.join(root_dir, "objdiff.json") -BUILD_NINJA = os.path.join(root_dir, "build.ninja") -COMPILE_COMMANDS = os.path.join(root_dir, "compile_commands.json") - -Command = Union[str, List[str]] - - -def load_objdiff() -> Dict[str, Any]: - with open(OBJDIFF_JSON) as f: - return json.load(f) - - -def find_unit_source(config: Dict[str, Any], unit_name: str) -> Optional[str]: - """Return the source_path for a unit from objdiff.json, or None.""" - for unit in config.get("units", []): - if unit["name"] == unit_name: - src = unit.get("metadata", {}).get("source_path") - return str(src) if src else None - return None - - -def find_unit_target(config: Dict[str, Any], unit_name: str) -> Optional[str]: - """Return the build target path for a unit from objdiff.json, or None.""" - for unit in config.get("units", []): - if unit["name"] == unit_name: - target = unit.get("base_path") or unit.get("target_path") - return str(target) if target else None - return None - - -def get_compdb() -> Optional[List[Dict[str, Any]]]: - """Load compile_commands.json, falling back to `ninja -t compdb` if needed.""" - if os.path.exists(COMPILE_COMMANDS): - try: - with open(COMPILE_COMMANDS) as f: - return json.load(f) - except json.JSONDecodeError as e: - print(f"Failed to parse compile_commands.json: {e}", file=sys.stderr) - - result = subprocess.run( - ["ninja", "-t", "compdb"], - capture_output=True, - cwd=root_dir, - ) - if result.returncode != 0: - print( - f"ninja -t compdb failed:\n{result.stderr.decode(errors='replace')}", - file=sys.stderr, - ) - return None - try: - return json.loads(result.stdout) - except json.JSONDecodeError as e: - print(f"Failed to parse ninja compdb output: {e}", file=sys.stderr) - return None - - -def get_build_command(target_path: str) -> Optional[str]: - """Return the final ninja command used to build target_path.""" - result = subprocess.run( - ["ninja", "-t", "commands", target_path], - capture_output=True, - cwd=root_dir, - ) - if result.returncode != 0: - print( - f"ninja -t commands failed:\n{result.stderr.decode(errors='replace')}", - file=sys.stderr, - ) - return None - - commands = [line.strip() for line in result.stdout.decode().splitlines() if line.strip()] - return commands[-1] if commands else None - - -def find_entry( - compdb: List[Dict[str, Any]], source_path: str -) -> Optional[Dict[str, Any]]: - """Find the compdb entry whose 'file' matches source_path. - - Prefers entries whose output is a .o file (actual compiler invocations) - over auxiliary entries (e.g. hash generation). - """ - abs_source = os.path.normcase(os.path.abspath(os.path.join(root_dir, source_path))) - candidates = [] - for entry in compdb: - file_val = entry.get("file", "") - if not os.path.isabs(file_val): - entry_dir = entry.get("directory", root_dir) - file_val = os.path.abspath(os.path.join(entry_dir, file_val)) - if os.path.normcase(file_val) == abs_source: - candidates.append(entry) - for entry in candidates: - out = entry.get("output", "") - if out.endswith(".o") or out.endswith(".obj"): - return entry - return candidates[0] if candidates else None - - -def get_command(entry: Dict[str, Any]) -> Command: - command = entry.get("command") - if isinstance(command, str): - return command - - arguments = entry.get("arguments") - if isinstance(arguments, list): - return arguments[:] - - print( - "Compilation entry is missing both 'command' and 'arguments'", - file=sys.stderr, - ) - sys.exit(1) - - -def strip_transform_dep(command: Command) -> Command: - """Remove the `&& python transform_dep.py ...` step from a compile command. - - The dependency file transformation is only needed for incremental ninja - builds; it is safe to skip for one-off temp compilations. - """ - if isinstance(command, list): - return command - return re.sub( - r"\s*&&\s*\S*python3?\S*\s+\S*transform_dep\.py\s+\S+\s+\S+", - "", - command, - ) - - -def find_output_argument(command: Command) -> Optional[Tuple[int, str]]: - if isinstance(command, list): - for i in range(len(command) - 1): - if command[i] == "-o": - return i + 1, command[i + 1] - return None - - m = re.search(r"(? Command: - """Replace the compiler output path in command with new_output. - - Handles two styles: - - Direct file output (-o path/to/file.o): ngccc/ProDG, MSVC, EE-GCC - - Directory output (-o path/to/dir): mwcc (MWCC outputs to a dir) - """ - output_arg = find_output_argument(command) - if output_arg is None: - print("Could not find -o argument in compile command", file=sys.stderr) - sys.exit(1) - - index, o_arg = output_arg - - if o_arg.endswith(".o") or o_arg.endswith(".obj"): - replacement = new_output - else: - replacement = os.path.dirname(new_output) - - if isinstance(command, list): - new_command = command[:] - new_command[index] = replacement - return new_command - - return command[:index] + replacement + command[index + len(o_arg) :] - - -def actual_output_path(command: Command, source_path: str, new_output: str) -> str: - """Return the path where the compiled .o actually lands. - - For direct-file compilers this is new_output. For directory-output - compilers it is /.o. - """ - output_arg = find_output_argument(command) - if output_arg is None: - return new_output - _, o_arg = output_arg - if o_arg.endswith(".o") or o_arg.endswith(".obj"): - return new_output - stem = os.path.splitext(os.path.basename(source_path))[0] - return os.path.join(os.path.dirname(new_output), stem + ".o") - - -def compile_unit(unit_name: str, output_path: str) -> str: - """Compile unit to output_path and return the actual .o path.""" - if not os.path.exists(OBJDIFF_JSON): - print( - "objdiff.json not found. Run: python configure.py && ninja all_source", - file=sys.stderr, - ) - sys.exit(1) - - config = load_objdiff() - source_path = find_unit_source(config, unit_name) - target_path = find_unit_target(config, unit_name) - if not source_path: - print( - f"No source_path found for unit '{unit_name}' in objdiff.json.\n" - "The unit may not have a source file yet (missing implementation).", - file=sys.stderr, - ) - sys.exit(1) - if not target_path: - print( - f"No target_path found for unit '{unit_name}' in objdiff.json.", - file=sys.stderr, - ) - sys.exit(1) - - if not os.path.exists(BUILD_NINJA): - print( - "build.ninja not found. Run: python configure.py && ninja all_source", - file=sys.stderr, - ) - sys.exit(1) - - command = get_build_command(target_path) - if command is None: - print( - f"No build command found for target '{target_path}'.\n" - "Make sure the unit exists and `python configure.py` has been run.", - file=sys.stderr, - ) - sys.exit(1) - - # 1. Strip the dependency-file transform step — not needed for temp builds. - command = strip_transform_dep(command) - - # 2. Determine the actual output path before modifying the command. - actual = actual_output_path(command, source_path, output_path) - - # 3. Ensure the output directory exists. - out_dir = os.path.dirname(actual) - if out_dir: - os.makedirs(out_dir, exist_ok=True) - - # 4. Redirect the compiler output. - command = redirect_output(command, source_path, output_path) - - # 5. Run the compile. - result = subprocess.run(command, shell=isinstance(command, str), cwd=root_dir) - if result.returncode != 0: - print( - f"Compilation failed (exit code {result.returncode})", file=sys.stderr - ) - sys.exit(1) - - return actual - - -def main() -> None: - parser = argparse.ArgumentParser( - description=( - "Compile a translation unit to a temporary or specified .o file. " - "Safe to run in parallel — each call produces an independent output." - ) - ) - parser.add_argument( - "-u", - "--unit", - required=True, - help="Unit name (e.g. main/Speed/Indep/SourceLists/zAnim)", - ) - parser.add_argument( - "-o", - "--output", - help="Explicit output .o path (default: auto-generated temp file)", - ) - args = parser.parse_args() - - if args.output: - output_path = os.path.abspath(args.output) - else: - unit_stem = os.path.basename(args.unit) - fd, output_path = tempfile.mkstemp( - prefix=f"nfsmw_{unit_stem}_", - suffix=".o", - ) - os.close(fd) - - actual = compile_unit(args.unit, output_path) - print(actual) - - -if __name__ == "__main__": - main() From 718ab67aa4093f67f585863ac978f88b12350329 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 17:44:32 +0100 Subject: [PATCH 24/71] Add merge rollout helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/merge-main-rollout.py | 327 ++++++++++++++++++++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 tools/merge-main-rollout.py diff --git a/tools/merge-main-rollout.py b/tools/merge-main-rollout.py new file mode 100644 index 000000000..4848fbdfd --- /dev/null +++ b/tools/merge-main-rollout.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 + +""" +Safely merge a base branch into recent local branches and open-PR branches. + +Examples: + python tools/merge-main-rollout.py --base main --recent-hours 5 --pr-repo dbalatoni13/nfsmw --dry-run + python tools/merge-main-rollout.py --base main --recent-hours 5 --pr-repo dbalatoni13/nfsmw +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Sequence, Set, Tuple + +from _common import ROOT_DIR, format_subprocess_error + + +TRAILER = "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" + + +class RolloutError(RuntimeError): + pass + + +@dataclass +class TargetBranch: + name: str + reasons: Set[str] = field(default_factory=set) + + +@dataclass +class MergeResult: + branch: str + status: str + detail: str + + +def run_capture(cmd: Sequence[str], cwd: str = ROOT_DIR) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + cmd, + cwd=cwd, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise RolloutError(format_subprocess_error(cmd, result.returncode, result.stdout, result.stderr)) + return result + + +def run_stream(cmd: Sequence[str], cwd: str = ROOT_DIR) -> None: + result = subprocess.run(cmd, cwd=cwd, text=True) + if result.returncode != 0: + raise RolloutError(format_subprocess_error(cmd, result.returncode)) + + +def git_common_dir() -> str: + return run_capture(["git", "rev-parse", "--git-common-dir"]).stdout.strip() + + +def get_worktrees() -> Dict[str, str]: + result = run_capture(["git", "worktree", "list", "--porcelain"]) + worktrees: Dict[str, str] = {} + current: Dict[str, str] = {} + for line in result.stdout.splitlines(): + if not line: + branch = current.get("branch", "") + worktree = current.get("worktree") + if branch.startswith("refs/heads/") and worktree: + worktrees[branch.replace("refs/heads/", "", 1)] = worktree + current = {} + continue + key, _, value = line.partition(" ") + current[key] = value + if current: + branch = current.get("branch", "") + worktree = current.get("worktree") + if branch.startswith("refs/heads/") and worktree: + worktrees[branch.replace("refs/heads/", "", 1)] = worktree + return worktrees + + +def worktree_dirty(path: str) -> bool: + result = run_capture(["git", "status", "--short"], cwd=path) + return bool(result.stdout.strip()) + + +def get_recent_local_branches(hours: float) -> List[str]: + threshold = int(time.time() - hours * 3600.0) + result = run_capture( + [ + "git", + "for-each-ref", + "--format=%(refname:short)\t%(committerdate:unix)", + "refs/heads", + ] + ) + recent: List[str] = [] + for line in result.stdout.splitlines(): + if not line.strip(): + continue + name, _, ts = line.partition("\t") + if not name or not ts: + continue + if int(ts) >= threshold: + recent.append(name) + return recent + + +def get_open_pr_branches(repo: str) -> List[Tuple[str, str]]: + result = run_capture( + [ + "gh", + "pr", + "list", + "--repo", + repo, + "--state", + "open", + "--limit", + "100", + "--json", + "headRefName,headRepositoryOwner", + ] + ) + data = json.loads(result.stdout) + refs: List[Tuple[str, str]] = [] + for entry in data: + branch = entry.get("headRefName") + owner = ((entry.get("headRepositoryOwner") or {}).get("login") or "").strip() + if branch: + refs.append((branch, owner)) + return refs + + +def local_branch_exists(branch: str) -> bool: + result = subprocess.run( + ["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch}"], + cwd=ROOT_DIR, + text=True, + ) + return result.returncode == 0 + + +def discover_targets(base: str, recent_hours: float, pr_repos: Sequence[str]) -> Dict[str, TargetBranch]: + targets: Dict[str, TargetBranch] = {} + for branch in get_recent_local_branches(recent_hours): + if branch == base: + continue + target = targets.setdefault(branch, TargetBranch(branch)) + target.reasons.add(f"recent<={recent_hours:g}h") + + for repo in pr_repos: + for branch, owner in get_open_pr_branches(repo): + if branch == base: + continue + target = targets.setdefault(branch, TargetBranch(branch)) + if owner: + target.reasons.add(f"open-pr:{repo}:{owner}") + else: + target.reasons.add(f"open-pr:{repo}") + return targets + + +def ensure_temp_worktree(branch: str, temp_root: str) -> str: + os.makedirs(temp_root, exist_ok=True) + path = tempfile.mkdtemp(prefix=f"{branch.replace('/', '_')}_", dir=temp_root) + try: + run_capture(["git", "worktree", "add", path, branch]) + except Exception: + shutil.rmtree(path, ignore_errors=True) + raise + return path + + +def remove_temp_worktree(path: str) -> None: + result = subprocess.run( + ["git", "worktree", "remove", "--force", path], + cwd=ROOT_DIR, + text=True, + capture_output=True, + ) + if result.returncode != 0: + shutil.rmtree(path, ignore_errors=True) + + +def merge_into_branch(branch: str, base: str, path: str) -> MergeResult: + before = run_capture(["git", "rev-parse", "HEAD"], cwd=path).stdout.strip() + merge_message = f"Merge {base} into {branch}\n\n{TRAILER}" + result = subprocess.run( + ["git", "merge", "--no-ff", base, "-m", merge_message], + cwd=path, + text=True, + capture_output=True, + ) + if result.returncode != 0: + subprocess.run(["git", "merge", "--abort"], cwd=path, text=True, capture_output=True) + return MergeResult( + branch=branch, + status="conflict", + detail=(result.stderr.strip() or result.stdout.strip() or "merge failed"), + ) + + after = run_capture(["git", "rev-parse", "HEAD"], cwd=path).stdout.strip() + if before == after: + return MergeResult(branch=branch, status="up-to-date", detail="already contained base") + return MergeResult(branch=branch, status="merged", detail=after) + + +def rollout( + base: str, + recent_hours: float, + pr_repos: Sequence[str], + dry_run: bool, +) -> List[MergeResult]: + targets = discover_targets(base, recent_hours, pr_repos) + worktrees = get_worktrees() + temp_root = os.path.join(git_common_dir(), "merge-rollout-worktrees") + results: List[MergeResult] = [] + + for branch in sorted(targets): + if branch == base: + continue + + target = targets[branch] + reason_text = ", ".join(sorted(target.reasons)) + + if not local_branch_exists(branch): + results.append( + MergeResult(branch=branch, status="skipped", detail=f"no local branch ({reason_text})") + ) + continue + + existing_path = worktrees.get(branch) + temp_path: Optional[str] = None + path = existing_path + try: + if existing_path: + if worktree_dirty(existing_path): + results.append( + MergeResult( + branch=branch, + status="skipped", + detail=f"dirty worktree: {existing_path} ({reason_text})", + ) + ) + continue + else: + temp_path = ensure_temp_worktree(branch, temp_root) + path = temp_path + + assert path is not None + if dry_run: + location = existing_path if existing_path else temp_path + results.append( + MergeResult( + branch=branch, + status="would-merge", + detail=f"{location} ({reason_text})", + ) + ) + continue + + results.append(merge_into_branch(branch, base, path)) + finally: + if temp_path is not None: + remove_temp_worktree(temp_path) + + return results + + +def print_results(results: Sequence[MergeResult]) -> None: + print(f"{'BRANCH':<40} {'STATUS':<12} DETAIL") + print("-" * 120) + for result in results: + print(f"{result.branch:<40} {result.status:<12} {result.detail}") + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Safely merge a base branch into recent local branches and local open-PR branches.", + ) + parser.add_argument("--base", default="main", help="Base branch to merge from (default: main)") + parser.add_argument( + "--recent-hours", + type=float, + default=5.0, + help="Include local branches with commits in the last N hours (default: 5)", + ) + parser.add_argument( + "--pr-repo", + action="append", + default=[], + help="GitHub repo in OWNER/REPO form; include open PR head branches if they exist locally", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Discover and print targets without merging", + ) + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + try: + results = rollout( + base=args.base, + recent_hours=args.recent_hours, + pr_repos=args.pr_repo, + dry_run=args.dry_run, + ) + print_results(results) + except RolloutError as e: + print(e, file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() From 9ff950b49f1c28851c4fe21bed3e4242ba108bb6 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 17:49:22 +0100 Subject: [PATCH 25/71] Support dirty merge rollout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/merge-main-rollout.py | 79 +++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/tools/merge-main-rollout.py b/tools/merge-main-rollout.py index 4848fbdfd..2f2afeeda 100644 --- a/tools/merge-main-rollout.py +++ b/tools/merge-main-rollout.py @@ -190,27 +190,56 @@ def remove_temp_worktree(path: str) -> None: shutil.rmtree(path, ignore_errors=True) -def merge_into_branch(branch: str, base: str, path: str) -> MergeResult: +def merge_in_progress(path: str) -> bool: + result = subprocess.run( + ["git", "rev-parse", "-q", "--verify", "MERGE_HEAD"], + cwd=path, + text=True, + capture_output=True, + ) + return result.returncode == 0 + + +def merge_into_branch(branch: str, base: str, path: str, allow_dirty: bool) -> MergeResult: before = run_capture(["git", "rev-parse", "HEAD"], cwd=path).stdout.strip() + dirty_before = worktree_dirty(path) merge_message = f"Merge {base} into {branch}\n\n{TRAILER}" + cmd = ["git", "merge", "--no-ff"] + if allow_dirty and dirty_before: + cmd.append("--autostash") + cmd.extend([base, "-m", merge_message]) result = subprocess.run( - ["git", "merge", "--no-ff", base, "-m", merge_message], + cmd, cwd=path, text=True, capture_output=True, ) if result.returncode != 0: - subprocess.run(["git", "merge", "--abort"], cwd=path, text=True, capture_output=True) + if merge_in_progress(path): + subprocess.run(["git", "merge", "--abort"], cwd=path, text=True, capture_output=True) + return MergeResult( + branch=branch, + status="conflict", + detail=(result.stderr.strip() or result.stdout.strip() or "merge failed"), + ) return MergeResult( branch=branch, - status="conflict", + status="restore-conflict" if dirty_before else "failed", detail=(result.stderr.strip() or result.stdout.strip() or "merge failed"), ) after = run_capture(["git", "rev-parse", "HEAD"], cwd=path).stdout.strip() if before == after: - return MergeResult(branch=branch, status="up-to-date", detail="already contained base") - return MergeResult(branch=branch, status="merged", detail=after) + status = "up-to-date-dirty" if dirty_before else "up-to-date" + detail = "already contained base" + if dirty_before and allow_dirty: + detail += " (dirty changes preserved with autostash)" + return MergeResult(branch=branch, status=status, detail=detail) + status = "merged-dirty" if dirty_before else "merged" + detail = after + if dirty_before and allow_dirty: + detail += " (dirty changes preserved with autostash)" + return MergeResult(branch=branch, status=status, detail=detail) def rollout( @@ -218,6 +247,7 @@ def rollout( recent_hours: float, pr_repos: Sequence[str], dry_run: bool, + dirty_mode: str, ) -> List[MergeResult]: targets = discover_targets(base, recent_hours, pr_repos) worktrees = get_worktrees() @@ -242,7 +272,8 @@ def rollout( path = existing_path try: if existing_path: - if worktree_dirty(existing_path): + dirty = worktree_dirty(existing_path) + if dirty and dirty_mode == "skip": results.append( MergeResult( branch=branch, @@ -252,22 +283,41 @@ def rollout( ) continue else: + if dry_run: + location = f" ({reason_text})" + results.append( + MergeResult(branch=branch, status="would-merge", detail=location) + ) + continue temp_path = ensure_temp_worktree(branch, temp_root) path = temp_path assert path is not None if dry_run: - location = existing_path if existing_path else temp_path + dirty = worktree_dirty(path) + if dirty and dirty_mode == "autostash": + status = "would-merge-dirty" + location = f"{path} ({reason_text}; autostash)" + else: + status = "would-merge" + location = f"{path} ({reason_text})" results.append( MergeResult( branch=branch, - status="would-merge", - detail=f"{location} ({reason_text})", + status=status, + detail=location, ) ) continue - results.append(merge_into_branch(branch, base, path)) + results.append( + merge_into_branch( + branch, + base, + path, + allow_dirty=(dirty_mode == "autostash"), + ) + ) finally: if temp_path is not None: remove_temp_worktree(temp_path) @@ -304,6 +354,12 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Discover and print targets without merging", ) + parser.add_argument( + "--dirty-mode", + choices=["autostash", "skip"], + default="autostash", + help="How to handle dirty checked-out worktrees (default: autostash)", + ) return parser @@ -316,6 +372,7 @@ def main() -> None: recent_hours=args.recent_hours, pr_repos=args.pr_repo, dry_run=args.dry_run, + dirty_mode=args.dirty_mode, ) print_results(results) except RolloutError as e: From e7f538205d204154c3c4a470af5578a015967031 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 18:59:07 +0100 Subject: [PATCH 26/71] Tune health checks and line ownership tooling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 4 + .github/skills/implement/SKILL.md | 14 ++ AGENTS.md | 18 ++ tools/_common.py | 1 + tools/decomp-context.py | 1 + tools/decomp-workflow.py | 99 ++++++++- tools/dwarf-compare.py | 354 +++++++++++++++++++++++++++++- tools/line_lookup.py | 2 +- 8 files changed, 484 insertions(+), 9 deletions(-) diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index b2cf62247..58e81937e 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -32,10 +32,14 @@ the main worker after reviewing the read-only findings. Before any work begins, establish a regression baseline: ```sh +python tools/decomp-workflow.py health --full main/Path/To/TU ninja # ensure clean build ninja baseline # snapshot current match state ``` +Add `--timings` to the `health --full` command when you are investigating slow worktrees +or unexpectedly expensive build/tool startup. + After modifying shared headers, check `ninja changes` to verify no regressions were introduced. An empty changeset means no regressions. If regressions appear, the shared header change must be reverted. diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 1a68ccec5..25f56b926 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -26,6 +26,15 @@ functions unless the user explicitly wants a cleanup/refiner pass. Use the wrapper flow first throughout this skill. Drop to raw `decomp-context.py` or `decomp-diff.py` only when the wrapper is missing a specific flag or you are debugging. +On a new, suspicious, or recently updated worktree, start with: + +```sh +python tools/decomp-workflow.py health --full main/Path/To/TU +``` + +Add `--timings` when you need to understand why wrapper/tool startup or the shared build +smoke is slow. + ### 1a. decomp-context.py Preferred shortcut: @@ -189,6 +198,11 @@ python tools/decomp-workflow.py dwarf -u main/Path/To/TU -f FunctionName This gives you a normalized DWARF match percentage plus a diff-like report of what still differs between the original and rebuilt DWARF blocks for that function. +Pay attention to the `Range source ownership` summary there as well. It compares the +debug-line owner files for each DWARF `// Range:` block, which makes it much easier to +spot inlines that are coming from the wrong header or owner file. Exact line-number +agreement is a useful secondary hint, but file ownership is the first thing to check. + Manual fallback: After writing your code, you can also run the dwarf dump on the compiled output and then query your output dump with lookup.py to compare your decompiled functions against the originals. Since the address of the function you're working on can keep changing diff --git a/AGENTS.md b/AGENTS.md index ff958229f..ca49784a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,11 @@ originates from, use this script against the compiler-generated debug line mappi See `.github/skills/line_lookup/SKILL.md` for the full workflow. +`line_lookup.py` now accepts both the original `0xADDR:` debug-line format and rebuilt +object exports written as bare `ADDR:` lines, so you can point it at +`symbols/debug_lines.txt` or at a rebuilt `debug_lines.txt` from +`tools/dwarf1_gcc_line_info.py`. + ### code-style — Repo-local style guidance When you are writing code, polishing code you already touched, or doing a style-review pass, @@ -148,6 +153,8 @@ Prefer this wrapper for routine agent-driven flows instead of manually chaining ```sh python tools/decomp-workflow.py health +python tools/decomp-workflow.py health --full main/Speed/Indep/SourceLists/zAnim +python tools/decomp-workflow.py health --full main/Speed/Indep/SourceLists/zAnim --timings python tools/decomp-workflow.py health --smoke-build main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py health --smoke-dtk main/Speed/Indep/SourceLists/zAnim python tools/decomp-workflow.py next --category game --limit 10 @@ -168,6 +175,12 @@ repeated command chaining and to standardize routine worktree preflight checks f once when that output is missing, so wrapper-first inspection works more often on half-prepared worktrees. +Use `health --full ` when you want one end-to-end tooling smoke test for a worktree. +It reuses a single shared build for the build smoke, DTK dump, rebuilt debug-line export, +and rebuilt `line_lookup.py` check, so it is faster and more representative than chaining +`--smoke-build` and `--smoke-dtk` separately. Add `--timings` when you are diagnosing a +slow worktree or compiler/tool startup. + In normal agent work, use the wrapper commands first. Drop to the raw backend tools only when you specifically need a backend-only flag, are debugging a wrapper/backend discrepancy, or are doing a final exhaustive check that the wrapper does not expose directly. @@ -223,6 +236,11 @@ whenever `verify` says the function is still failing the DWARF gate. This is the fastest way to see whether you are still missing locals, have the wrong inline body, or changed signature/type details even when the instruction diff already looks good. +It also compares the debug-line ownership of each `// Range:` block. Treat the +`Range source ownership` summary as the fast inline-placement check: file mismatches are +strong evidence that an inline body came from the wrong header or owner file. The exact +file+line count is stricter and mainly useful as a secondary hint, not as the main gate. + When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work faster, shorter, or more reliable, prefer implementing that improvement as part of the task diff --git a/tools/_common.py b/tools/_common.py index 2acfbac2a..985a122bc 100644 --- a/tools/_common.py +++ b/tools/_common.py @@ -20,6 +20,7 @@ "-c", "ppc.calculatePoolRelocations=false", ] +RELOC_DIFF_CHOICES = ("none", "function", "data", "all") class ToolError(RuntimeError): diff --git a/tools/decomp-context.py b/tools/decomp-context.py index c2382602b..b13790ece 100644 --- a/tools/decomp-context.py +++ b/tools/decomp-context.py @@ -25,6 +25,7 @@ import sys from typing import Any, Dict, List, Optional, Tuple from _common import ( + RELOC_DIFF_CHOICES, ROOT_DIR, ToolError, build_objdiff_symbol_rows, diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index 9193425cd..84f2f83d5 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -26,10 +26,12 @@ import re import os import shlex +import shutil import subprocess import sys import tempfile -from typing import Any, Dict, List, Optional, Sequence +import time +from typing import Any, Dict, List, Optional, Sequence, Tuple from _common import ( BUILD_NINJA, OBJDIFF_JSON, @@ -59,6 +61,7 @@ DEBUG_SYMBOL_PROBE_MANGLED = "UpdateAll__6Cameraf" DEBUG_SYMBOL_PROBE_DEMANGLED = "Camera::UpdateAll(float)" DEBUG_SYMBOL_PROBE_GC_ADDR = "0x80065A84" +REBUILT_DEBUG_LINE_RE = re.compile(r"^\s*([0-9A-Fa-f]+)\s*:") LOW_MATCH_PRIORITY_THRESHOLD = 60.0 VERY_LOW_MATCH_PRIORITY_THRESHOLD = 40.0 HIGH_MATCH_CLEANUP_THRESHOLD = 85.0 @@ -321,6 +324,11 @@ def lookup_symbol_address(symbols_file: str, mangled_name: str) -> Optional[str] def command_health(args: argparse.Namespace) -> None: failures = 0 + timings: List[Tuple[str, float]] = [] + build_cache: Dict[str, str] = {} + + smoke_build_unit = args.smoke_build or args.full + smoke_dtk_unit = args.smoke_dtk or args.full print_section("Worktree Health") print(f"Root: {ROOT_DIR}") @@ -332,6 +340,20 @@ def report(ok: bool, label: str, detail: str) -> None: if not ok: failures += 1 + def timed(label: str, func): + start = time.monotonic() + try: + return func() + finally: + timings.append((label, time.monotonic() - start)) + + def build_shared_unit_cached(unit: str) -> str: + if unit in build_cache: + return build_cache[unit] + output_path = timed(f"build {unit}", lambda: build_shared_unit(unit)) + build_cache[unit] = output_path + return output_path + report( os.path.exists(BUILD_NINJA), "build.ninja", @@ -368,7 +390,7 @@ def report(ok: bool, label: str, detail: str) -> None: DTK if os.path.exists(DTK) else "missing (seed build/tools in this worktree)", ) try: - run_capture(python_tool("decomp-context.py", "--ghidra-check")) + timed("ghidra-check", lambda: run_capture(python_tool("decomp-context.py", "--ghidra-check"))) report(True, "ghidra", "GC + PS2 programs available") except WorkflowError as e: report(False, "ghidra", str(e)) @@ -418,10 +440,10 @@ def report(ok: bool, label: str, detail: str) -> None: except WorkflowError as e: report(False, "debug-lines", str(e)) - if args.smoke_build: + if smoke_build_unit: print_section("Build Smoke Test") try: - output_path = build_shared_unit(args.smoke_build) + output_path = build_shared_unit_cached(smoke_build_unit) report(True, "build", output_path) except WorkflowError as e: detail = str(e) @@ -429,17 +451,65 @@ def report(ok: bool, label: str, detail: str) -> None: detail += "\nHint: Run: python tools/share_worktree_assets.py bootstrap" report(False, "build", detail) - if args.smoke_dtk: + if smoke_dtk_unit: print_section("DTK Smoke Test") dump_path = None + debug_lines_dir = None try: - obj_path = build_shared_unit(args.smoke_dtk) - dump_path = dtk_dwarf_dump(obj_path) + obj_path = build_shared_unit_cached(smoke_dtk_unit) + dump_path = timed(f"dtk dump {smoke_dtk_unit}", lambda: dtk_dwarf_dump(obj_path)) report(True, "dtk", dump_path) except WorkflowError as e: report(False, "dtk", str(e)) + else: + try: + debug_lines_dir = tempfile.mkdtemp(prefix="nfsmw_health_debug_lines_") + timed( + f"debug-line export {smoke_dtk_unit}", + lambda: run_capture( + python_tool("dwarf1_gcc_line_info.py", obj_path, debug_lines_dir) + ), + ) + rebuilt_debug_lines = os.path.join(debug_lines_dir, "debug_lines.txt") + if not os.path.exists(rebuilt_debug_lines): + raise WorkflowError( + "rebuilt debug-line export did not produce debug_lines.txt" + ) + first_address = None + with open(rebuilt_debug_lines) as f: + for raw_line in f: + match = REBUILT_DEBUG_LINE_RE.match(raw_line) + if match is not None: + first_address = match.group(1) + break + if first_address is None: + raise WorkflowError( + "rebuilt debug-line export produced no address entries" + ) + result = timed( + f"rebuilt line lookup {smoke_dtk_unit}", + lambda: run_capture( + python_tool("line_lookup.py", rebuilt_debug_lines, first_address) + ), + ) + ok = "Exact match found" in result.stdout + detail = ( + f"rebuilt line export ok ({first_address})" + if ok + else "rebuilt line lookup output did not contain an exact match" + ) + report(ok, "rebuilt-debug-lines", detail) + except WorkflowError as e: + report(False, "rebuilt-debug-lines", str(e)) finally: maybe_remove(dump_path) + if debug_lines_dir is not None: + shutil.rmtree(debug_lines_dir, ignore_errors=True) + + if args.timings and timings: + print_section("Timings") + for label, elapsed in timings: + print(f"{elapsed:7.2f}s {label}") if failures: raise WorkflowError(f"Health check failed with {failures} issue(s)") @@ -829,6 +899,16 @@ def build_parser() -> argparse.ArgumentParser: "health", help="Check whether the current worktree is ready for GC and PS2 decomp work", ) + health.add_argument( + "--full", + metavar="UNIT", + nargs="?", + const=DEFAULT_SMOKE_UNIT, + help=( + "Run the full smoke path for one unit: shared build, dtk dump, rebuilt " + f"debug-line export, and rebuilt line lookup. If UNIT is omitted, uses {DEFAULT_SMOKE_UNIT}" + ), + ) health.add_argument( "--smoke-build", metavar="UNIT", @@ -839,6 +919,11 @@ def build_parser() -> argparse.ArgumentParser: f"{DEFAULT_SMOKE_UNIT}" ), ) + health.add_argument( + "--timings", + action="store_true", + help="Show wall-clock timings for the heavier health-check steps", + ) health.add_argument( "--smoke-dtk", metavar="UNIT", diff --git a/tools/dwarf-compare.py b/tools/dwarf-compare.py index 51f35ea9b..087389d78 100644 --- a/tools/dwarf-compare.py +++ b/tools/dwarf-compare.py @@ -11,14 +11,19 @@ """ import argparse +import contextlib import difflib +import io import json import os import re +import shutil import sys +import tempfile from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple from _common import ROOT_DIR, ToolError, find_objdiff_unit, load_objdiff_config, make_abs +from dwarf1_gcc_line_info import process_file as export_debug_lines from lookup import ( _candidate_func_names, _normalise_func_name, @@ -33,6 +38,10 @@ GC_DWARF = os.path.join(ROOT_DIR, "symbols", "Dwarf") DTK = os.path.join(ROOT_DIR, "build", "tools", "dtk") HEX_RE = re.compile(r"0x[0-9A-Fa-f]+") +RANGE_RE = re.compile(r"^(\s*)// Range:\s*(0x[0-9A-Fa-f]+)\s*->\s*(0x[0-9A-Fa-f]+)") +DEBUG_LINE_RE = re.compile( + r"^\s*(?:0x)?([0-9A-Fa-f]+):\s*(.+?)\s+\(line\s+(\d+)(?:,\s+column\s+(\d+))?\)\s*$" +) class DwarfCompareError(RuntimeError): @@ -227,6 +236,269 @@ def normalize_block(block: str) -> List[str]: return [normalize_line(line) for line in block.splitlines()] +def canonical_debug_path(debug_path: str) -> str: + normalized = debug_path.replace("\\", "/").strip() + lowered = normalized.lower().replace("\\", "/") + if "/src/" in lowered: + src_index = lowered.index("/src/") + suffix = normalized[src_index + len("/src/") :].lstrip("/") + return os.path.normpath("src/" + suffix).replace("\\", "/") + if "/speed/indep/" in lowered: + indep_index = lowered.index("/speed/indep/") + suffix = normalized[indep_index + len("/speed/indep/") :].lstrip("/") + return os.path.normpath("src/Speed/Indep/" + suffix.lstrip("/")).replace("\\", "/") + return os.path.normpath(normalized).replace("\\", "/") + + +def normalize_source_location(path: str, line_number: int) -> str: + normalized = os.path.normpath(path.replace("\\", "/")).replace("\\", "/").lower() + return f"{normalized}:{line_number}" + + +def parse_debug_lines_file(path: str) -> Dict[int, List[Dict[str, Any]]]: + entries: Dict[int, List[Dict[str, Any]]] = {} + with open(path) as f: + for raw_line in f: + line = raw_line.rstrip("\n") + match = DEBUG_LINE_RE.match(line) + if match is None: + continue + address = int(match.group(1), 16) + debug_path = match.group(2) + line_number = int(match.group(3)) + display_path = canonical_debug_path(debug_path) + entries.setdefault(address, []).append( + { + "address": address, + "debug_path": debug_path, + "display_path": display_path, + "line_number": line_number, + "normalized_file": os.path.normpath(display_path.replace("\\", "/")) + .replace("\\", "/") + .lower(), + "normalized": normalize_source_location(display_path, line_number), + } + ) + return entries + + +def dedupe_source_locations(locations: Sequence[Dict[str, Any]]) -> List[str]: + deduped: List[str] = [] + seen = set() + for entry in locations: + rendered = f"{entry['display_path']}:{entry['line_number']}" + if rendered in seen: + continue + seen.add(rendered) + deduped.append(rendered) + return deduped + + +def dedupe_source_files(locations: Sequence[Dict[str, Any]]) -> List[str]: + deduped: List[str] = [] + seen = set() + for entry in locations: + normalized_file = entry["normalized_file"] + if normalized_file in seen: + continue + seen.add(normalized_file) + deduped.append(entry["display_path"]) + return deduped + + +def render_list(items: Sequence[str], limit: int = 6) -> str: + if not items: + return "" + if len(items) <= limit: + return ", ".join(items) + hidden = len(items) - limit + return f"{', '.join(items[:limit])}, ... (+{hidden} more)" + + +def build_debug_lines_file_for_object(obj_path: str) -> str: + temp_dir = tempfile.mkdtemp(prefix="nfsmw_debug_lines_") + with contextlib.redirect_stdout(io.StringIO()): + export_debug_lines(obj_path, temp_dir) + debug_lines_path = os.path.join(temp_dir, "debug_lines.txt") + if not os.path.exists(debug_lines_path): + raise DwarfCompareError("failed to export rebuilt debug lines") + return debug_lines_path + + +def collect_range_entries(block: str) -> List[Dict[str, Any]]: + lines = block.splitlines() + entries: List[Dict[str, Any]] = [] + for idx, line in enumerate(lines): + match = RANGE_RE.match(line) + if match is None: + continue + signature = "" + for follow in lines[idx + 1 :]: + stripped = follow.strip() + if not stripped or stripped.startswith("//"): + continue + signature = stripped.split("{")[0].strip() + break + entries.append( + { + "line_index": idx + 1, + "indent": len(match.group(1)) // 4, + "start_address": int(match.group(2), 16), + "end_address": int(match.group(3), 16), + "signature": signature, + } + ) + return entries + + +def normalized_signature_key(signature: str) -> str: + signature = signature.strip() + if not signature: + return "" + return normalize_line(signature) + + +def align_range_entries( + original_entries: Sequence[Dict[str, Any]], + rebuilt_entries: Sequence[Dict[str, Any]], +) -> List[Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]]: + original_keys = [ + f"{entry['indent']}|{normalized_signature_key(entry['signature'])}" for entry in original_entries + ] + rebuilt_keys = [ + f"{entry['indent']}|{normalized_signature_key(entry['signature'])}" for entry in rebuilt_entries + ] + matcher = difflib.SequenceMatcher(a=original_keys, b=rebuilt_keys) + aligned: List[Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]] = [] + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + for orig, reb in zip(original_entries[i1:i2], rebuilt_entries[j1:j2]): + aligned.append((orig, reb)) + elif tag == "replace": + max_len = max(i2 - i1, j2 - j1) + for offset in range(max_len): + orig = original_entries[i1 + offset] if i1 + offset < i2 else None + reb = rebuilt_entries[j1 + offset] if j1 + offset < j2 else None + aligned.append((orig, reb)) + elif tag == "delete": + for orig in original_entries[i1:i2]: + aligned.append((orig, None)) + elif tag == "insert": + for reb in rebuilt_entries[j1:j2]: + aligned.append((None, reb)) + return aligned + + +def build_range_source_summary( + original_block: FunctionBlock, + rebuilt_block: FunctionBlock, + rebuilt_debug_lines_path: Optional[str], +) -> Dict[str, Any]: + if not rebuilt_debug_lines_path or not os.path.exists(rebuilt_debug_lines_path): + return {"available": False} + original_debug_lines_path = os.path.join(ROOT_DIR, "symbols", "debug_lines.txt") + if not os.path.exists(original_debug_lines_path): + return {"available": False} + + original_entries = collect_range_entries(original_block[3]) + rebuilt_entries = collect_range_entries(rebuilt_block[3]) + aligned_entries = align_range_entries(original_entries, rebuilt_entries) + original_debug_lines = parse_debug_lines_file(original_debug_lines_path) + rebuilt_debug_lines = parse_debug_lines_file(rebuilt_debug_lines_path) + + rows: List[Dict[str, Any]] = [] + file_match_count = 0 + exact_match_count = 0 + for original_entry, rebuilt_entry in aligned_entries: + original_items = ( + original_debug_lines.get(int(original_entry["start_address"]), []) + if original_entry is not None + else [] + ) + rebuilt_items = ( + rebuilt_debug_lines.get(int(rebuilt_entry["start_address"]), []) + if rebuilt_entry is not None + else [] + ) + original_locations = ( + dedupe_source_locations(original_items) if original_entry is not None else [] + ) + rebuilt_locations = ( + dedupe_source_locations(rebuilt_items) if rebuilt_entry is not None else [] + ) + original_files_display = ( + dedupe_source_files(original_items) if original_entry is not None else [] + ) + rebuilt_files_display = ( + dedupe_source_files(rebuilt_items) if rebuilt_entry is not None else [] + ) + original_norm = { + item["normalized"] + for item in original_items + } if original_entry is not None else set() + original_files = { + item["normalized_file"] + for item in original_items + } if original_entry is not None else set() + rebuilt_norm = { + item["normalized"] + for item in rebuilt_items + } if rebuilt_entry is not None else set() + rebuilt_files = { + item["normalized_file"] + for item in rebuilt_items + } if rebuilt_entry is not None else set() + common_files = [ + path + for path in original_files_display + if os.path.normpath(path.replace("\\", "/")).replace("\\", "/").lower() + in rebuilt_files + ] + original_only_files = [ + path + for path in original_files_display + if os.path.normpath(path.replace("\\", "/")).replace("\\", "/").lower() + not in rebuilt_files + ] + rebuilt_only_files = [ + path + for path in rebuilt_files_display + if os.path.normpath(path.replace("\\", "/")).replace("\\", "/").lower() + not in original_files + ] + if original_entry is not None and rebuilt_entry is not None and original_files == rebuilt_files: + file_status = "match" + file_match_count += 1 + else: + file_status = "mismatch" + if original_entry is not None and rebuilt_entry is not None and original_norm == rebuilt_norm: + exact_status = "match" + exact_match_count += 1 + else: + exact_status = "mismatch" + rows.append( + { + "file_status": file_status, + "exact_status": exact_status, + "indent": (original_entry or rebuilt_entry or {}).get("indent", 0), + "line_number": (original_entry or rebuilt_entry or {}).get("line_index"), + "signature": (original_entry or rebuilt_entry or {}).get("signature", ""), + "original_locations": original_locations, + "rebuilt_locations": rebuilt_locations, + "common_files": common_files, + "original_only_files": original_only_files, + "rebuilt_only_files": rebuilt_only_files, + } + ) + return { + "available": True, + "rows": rows, + "total": len(rows), + "file_matches": file_match_count, + "exact_matches": exact_match_count, + } + + def count_lines_for_opcodes(opcodes: Sequence[Tuple[str, int, int, int, int]]) -> Dict[str, int]: matching = 0 original_only = 0 @@ -297,6 +569,7 @@ def build_report( rebuilt_block: FunctionBlock, collapse: bool, context: int, + rebuilt_debug_lines_path: Optional[str], ) -> Dict[str, Any]: original_raw = original_block[3].splitlines() rebuilt_raw = rebuilt_block[3].splitlines() @@ -345,6 +618,12 @@ def build_report( f"- {original_span} -> {rebuilt_span}: {detail}" ) + range_sources = build_range_source_summary( + original_block, + rebuilt_block, + rebuilt_debug_lines_path=rebuilt_debug_lines_path, + ) + return { "unit": unit_name, "function": function_name, @@ -364,6 +643,7 @@ def build_report( "original_range": [original_block[0], original_block[1]], "rebuilt_range": [rebuilt_block[0], rebuilt_block[1]], "mismatch_summaries": mismatch_summaries, + "range_sources": range_sources, "diff_lines": diff_lines, } @@ -392,6 +672,17 @@ def print_summary(report: Dict[str, Any]) -> None: "Address-only range differences are normalized out so the percentage tracks " "structural/function-body DWARF changes." ) + if report["range_sources"].get("available"): + line_drifts = report["range_sources"]["file_matches"] - report["range_sources"]["exact_matches"] + print( + f"Range source ownership: files agree {report['range_sources']['file_matches']}/" + f"{report['range_sources']['total']}" + f" | file mismatches {report['range_sources']['total'] - report['range_sources']['file_matches']}/" + f"{report['range_sources']['total']}" + f" | line drifts {line_drifts}/{report['range_sources']['total']}" + ) + else: + print("Range source ownership: unavailable (missing debug-line export data)") if not report["signature_match"]: print() print("Original signature:") @@ -401,6 +692,55 @@ def print_summary(report: Dict[str, Any]) -> None: def print_diff(report: Dict[str, Any]) -> None: + if not report["range_sources"].get("available"): + file_mismatches = [] + line_drifts = [] + elif report["range_sources"]["rows"]: + file_mismatches = [ + row for row in report["range_sources"]["rows"] if row["file_status"] != "match" + ] + line_drifts = [ + row + for row in report["range_sources"]["rows"] + if row["file_status"] == "match" and row["exact_status"] != "match" + ] + else: + file_mismatches = [] + line_drifts = [] + + if file_mismatches: + print_section("Range Source Ownership") + for row in file_mismatches: + location = f"L{row['line_number']:04d}" if row["line_number"] else "L????" + print(f"- {location} {row['signature']}") + if row["common_files"]: + print(f" common files: {render_list(row['common_files'])}") + if row["original_only_files"]: + print( + f" original-only files: {render_list(row['original_only_files'])}" + ) + if row["rebuilt_only_files"]: + print( + f" rebuilt-only files: {render_list(row['rebuilt_only_files'])}" + ) + print( + f" original lines: {render_list(row['original_locations'])}" + ) + print( + f" rebuilt lines: {render_list(row['rebuilt_locations'])}" + ) + if line_drifts: + print() + print( + f"Additionally, {len(line_drifts)}/{report['range_sources']['total']} ranges keep " + "the same source files but drift on line numbers." + ) + elif line_drifts: + print_section("Range Source Ownership") + print( + "All range starts resolve to the same source files; line numbers drift " + f"for {len(line_drifts)}/{report['range_sources']['total']} ranges." + ) if report["mismatch_summaries"]: print_section("Mismatch Summary") for line in report["mismatch_summaries"]: @@ -463,12 +803,14 @@ def main() -> None: args = parser.parse_args() rebuilt_dwarf_path: Optional[str] = None + rebuilt_debug_lines_path: Optional[str] = None cleanup_rebuilt_dwarf = False + cleanup_rebuilt_debug_lines_dir = False try: + obj_path = get_unit_build_output(args.unit) if args.rebuilt_dwarf_file: rebuilt_dwarf_path = os.path.abspath(args.rebuilt_dwarf_file) else: - obj_path = get_unit_build_output(args.unit) if not os.path.exists(obj_path): raise DwarfCompareError( f"Missing built object for {args.unit}: {obj_path}\n" @@ -478,6 +820,13 @@ def main() -> None: rebuilt_dwarf_path = dtk_dwarf_dump(obj_path) cleanup_rebuilt_dwarf = True + if os.path.exists(obj_path): + try: + rebuilt_debug_lines_path = build_debug_lines_file_for_object(obj_path) + cleanup_rebuilt_debug_lines_dir = True + except Exception: + rebuilt_debug_lines_path = None + original_funcs = load_function_blocks(GC_DWARF, folder_mode=True) rebuilt_funcs = load_function_blocks(rebuilt_dwarf_path, folder_mode=False) @@ -491,6 +840,7 @@ def main() -> None: rebuilt_block, collapse=not args.no_collapse, context=args.context, + rebuilt_debug_lines_path=rebuilt_debug_lines_path, ) if args.json: @@ -511,6 +861,8 @@ def main() -> None: finally: if cleanup_rebuilt_dwarf: maybe_remove(rebuilt_dwarf_path) + if cleanup_rebuilt_debug_lines_dir and rebuilt_debug_lines_path: + shutil.rmtree(os.path.dirname(rebuilt_debug_lines_path), ignore_errors=True) if __name__ == "__main__": diff --git a/tools/line_lookup.py b/tools/line_lookup.py index 65aed6649..41890b877 100644 --- a/tools/line_lookup.py +++ b/tools/line_lookup.py @@ -21,7 +21,7 @@ def parse_map_file(filepath): with open(filepath, "r") as f: for line in f: line = line.rstrip("\n") - match = re.match(r"^\s*(0x[0-9A-Fa-f]+)\s*:", line) + match = re.match(r"^\s*((?:0x)?[0-9A-Fa-f]+)\s*:", line) if match: addr = int(match.group(1), 16) entries.append((addr, line)) From 6ac3c90906aba8b8db2298c69f1869362d4bc4b7 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 19:10:44 +0100 Subject: [PATCH 27/71] Add ELF address lookup tool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 1 + .github/skills/refiner/SKILL.md | 2 +- AGENTS.md | 16 +++ tools/elf_lookup.py | 174 ++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 tools/elf_lookup.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 58e81937e..7abf9cb3f 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -115,6 +115,7 @@ For each missing or nonmatching function, follow the implementation workflow in implementing the next function reveals patterns that make the previous one click. - **Mismatch triage:** - `@stringBase0` offset mismatches often resolve as more string literals are added + - If you need to inspect the original string or rodata at a virtual address, use `python tools/elf_lookup.py 0xADDR` - Register swaps and stack layout issues require direct intervention - Branch structure mismatches indicate wrong control flow (if/switch/loop) - **Match percentage is misleading.** The last few percent are often the hardest. diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md index 4d1fe1bb7..a6aeb2125 100644 --- a/.github/skills/refiner/SKILL.md +++ b/.github/skills/refiner/SKILL.md @@ -46,7 +46,7 @@ Read every instruction pair. Categorize each mismatch: | **Stack frame size** | Wrong frame size in prologue | Count locals in DWARF; remove temporaries not in DWARF | | **Float vs int sequence** | `xoris` present → field is `int`; absent → `uint` | Check field type in DWARF; change cast | | **`fmuls` operand order** | `fmuls fX, fX, fY` or `fmuls fX, fY, fX` | Try `v *= fY` vs `fY * v` explicitly | -| **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order | +| **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order. Use `python tools/elf_lookup.py 0xADDR` when you need to confirm the original string/rodata at a virtual address | | **Virtual vs direct call** | `bl` vs indirect through vtable | Check const-qualifier; use `GetFoo()` vs `Foo()` | | **Inline vs outlined** | Extra call to helper vs inlined sequence | Force inline by rewriting the expression without calling the helper | | **Loop structure** | Guarded `do/while` from Ghidra or mismatched loop branches | Rewrite to the natural source form suggested by the control flow; in particular, a guarded `do/while` often needs to become a plain `for` loop | diff --git a/AGENTS.md b/AGENTS.md index ca49784a2..bd8a1ec3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,20 @@ object exports written as bare `ADDR:` lines, so you can point it at `symbols/debug_lines.txt` or at a rebuilt `debug_lines.txt` from `tools/dwarf1_gcc_line_info.py`. +### elf_lookup.py — Resolve strings / rodata by virtual address + +When you have a virtual address inside the original ELF and need to know which string or +rodata bytes live there, use: + +```sh +python tools/elf_lookup.py 0x803E58F4 +python tools/elf_lookup.py 0x803E58F4 --mode bytes --length 32 +python tools/elf_lookup.py 0x002F1234 --game ps2 +``` + +This is the preferred replacement for ad-hoc Python snippets that manually parse the ELF +to chase `@stringBase0` or other rodata/data references. + ### code-style — Repo-local style guidance When you are writing code, polishing code you already touched, or doing a style-review pass, @@ -434,6 +448,8 @@ It's very important that you use math inlines from bMath and UMath as shown in t - When you have to use a constant that looks like an address, it's possible that the splitter thought it was an allocation and it shows up as a diff because the left side has a symbol and the right side has a constant. In this case you need to figure out the virtual address of the instruction and block the relocation in config.yml. +- When you need to confirm what lives at a rodata/data address from the original ELF, use + `python tools/elf_lookup.py 0xADDR` instead of writing a one-off Python script. ### PPC EABI calling convention diff --git a/tools/elf_lookup.py b/tools/elf_lookup.py new file mode 100644 index 000000000..de0840a15 --- /dev/null +++ b/tools/elf_lookup.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Look up string or raw bytes in an ELF by virtual address. + +Examples: + python tools/elf_lookup.py 0x803E58F4 + python tools/elf_lookup.py 803E58F4 --mode bytes --length 32 + python tools/elf_lookup.py 0x002F1234 --game ps2 + python tools/elf_lookup.py 0x803E58F4 --elf orig/GOWE69/NFSMWRELEASE.ELF +""" + +import argparse +import os +import string +import sys +from typing import Any, Dict, Optional + +from elftools.elf.elffile import ELFFile + +from _common import ROOT_DIR, ToolError + + +DEFAULT_ELF_BY_GAME = { + "gc": os.path.join(ROOT_DIR, "orig", "GOWE69", "NFSMWRELEASE.ELF"), + "ps2": os.path.join(ROOT_DIR, "orig", "SLES-53558-A124", "NFS.ELF"), +} + + +def parse_address(raw: str) -> int: + try: + return int(raw, 16) + except ValueError as exc: + raise ToolError(f"invalid hex address: {raw}") from exc + + +def choose_elf_path(args: argparse.Namespace) -> str: + if args.elf: + path = args.elf + if not os.path.isabs(path): + path = os.path.join(ROOT_DIR, path) + return os.path.abspath(path) + return DEFAULT_ELF_BY_GAME[args.game] + + +def find_section_for_address(elffile: ELFFile, address: int) -> Optional[Dict[str, Any]]: + for section in elffile.iter_sections(): + header = section.header + start = int(header["sh_addr"]) + size = int(header["sh_size"]) + if size <= 0: + continue + end = start + size + if start <= address < end: + return { + "name": section.name, + "address": start, + "size": size, + "offset": int(header["sh_offset"]), + "data": section.data(), + } + return None + + +def read_c_string(data: bytes, start_offset: int, max_bytes: int) -> bytes: + blob = data[start_offset : start_offset + max_bytes] + terminator = blob.find(b"\x00") + if terminator != -1: + blob = blob[:terminator] + return blob + + +def printable_ratio(blob: bytes) -> float: + if not blob: + return 1.0 + printable = set(string.printable.encode("ascii")) + hits = sum(1 for byte in blob if byte in printable and byte not in b"\x0b\x0c") + return hits / len(blob) + + +def decode_display_string(blob: bytes) -> str: + try: + return blob.decode("utf-8") + except UnicodeDecodeError: + return blob.decode("latin-1", errors="replace") + + +def format_hex_bytes(blob: bytes, width: int = 16) -> str: + lines = [] + for offset in range(0, len(blob), width): + chunk = blob[offset : offset + width] + hex_part = " ".join(f"{byte:02X}" for byte in chunk) + ascii_part = "".join(chr(byte) if 32 <= byte <= 126 else "." for byte in chunk) + lines.append(f" +0x{offset:04X}: {hex_part:<{width * 3}} {ascii_part}") + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Look up a string or raw bytes in an ELF by virtual address." + ) + parser.add_argument("address", help="Virtual address, with or without 0x prefix") + parser.add_argument( + "--game", + choices=sorted(DEFAULT_ELF_BY_GAME), + default="gc", + help="Shortcut for selecting the default GameCube or PS2 ELF (default: gc)", + ) + parser.add_argument( + "--elf", + help="Explicit ELF path. Overrides --game.", + ) + parser.add_argument( + "--mode", + choices=["string", "bytes"], + default="string", + help="Read a C string or raw bytes from the address (default: string)", + ) + parser.add_argument( + "--length", + type=int, + default=64, + help="Maximum bytes to read (default: 64)", + ) + args = parser.parse_args() + + address = parse_address(args.address) + elf_path = choose_elf_path(args) + if not os.path.exists(elf_path): + raise ToolError(f"ELF not found: {elf_path}") + + with open(elf_path, "rb") as f: + elffile = ELFFile(f) + section = find_section_for_address(elffile, address) + + if section is None: + raise ToolError( + f"address 0x{address:08X} is not inside any ELF section in {elf_path}" + ) + + section_offset = address - int(section["address"]) + file_offset = int(section["offset"]) + section_offset + section_data = section["data"] + + print(f"ELF: {os.path.relpath(elf_path, ROOT_DIR)}") + print(f"Address: 0x{address:08X}") + print( + f"Section: {section['name']} +0x{section_offset:X} " + f"(section VA 0x{int(section['address']):08X}, file offset 0x{file_offset:X})" + ) + + if args.mode == "string": + blob = read_c_string(section_data, section_offset, args.length) + display = decode_display_string(blob) + kind = "string" if printable_ratio(blob) >= 0.85 else "non-printable bytes" + print(f"Kind: {kind}") + print(f"Length: {len(blob)} byte(s)") + print(f"Value: {display!r}") + if blob: + print("Hex:") + print(format_hex_bytes(blob)) + return + + blob = section_data[section_offset : section_offset + args.length] + print(f"Length: {len(blob)} byte(s)") + print("Hex:") + print(format_hex_bytes(blob)) + + +if __name__ == "__main__": + try: + main() + except ToolError as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) From dfd359ec701c47d115d6266ed9fedecf45d47e34 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 19:10:44 +0100 Subject: [PATCH 28/71] Add ELF address lookup tool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/execute/SKILL.md | 1 + .github/skills/refiner/SKILL.md | 2 +- AGENTS.md | 16 +++ tools/elf_lookup.py | 174 ++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 tools/elf_lookup.py diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 58e81937e..7abf9cb3f 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -115,6 +115,7 @@ For each missing or nonmatching function, follow the implementation workflow in implementing the next function reveals patterns that make the previous one click. - **Mismatch triage:** - `@stringBase0` offset mismatches often resolve as more string literals are added + - If you need to inspect the original string or rodata at a virtual address, use `python tools/elf_lookup.py 0xADDR` - Register swaps and stack layout issues require direct intervention - Branch structure mismatches indicate wrong control flow (if/switch/loop) - **Match percentage is misleading.** The last few percent are often the hardest. diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md index 4d1fe1bb7..a6aeb2125 100644 --- a/.github/skills/refiner/SKILL.md +++ b/.github/skills/refiner/SKILL.md @@ -46,7 +46,7 @@ Read every instruction pair. Categorize each mismatch: | **Stack frame size** | Wrong frame size in prologue | Count locals in DWARF; remove temporaries not in DWARF | | **Float vs int sequence** | `xoris` present → field is `int`; absent → `uint` | Check field type in DWARF; change cast | | **`fmuls` operand order** | `fmuls fX, fX, fY` or `fmuls fX, fY, fX` | Try `v *= fY` vs `fY * v` explicitly | -| **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order | +| **Relocation offset** | `@stringBase0` or data offset differs | More string literals will shift this; add them in order. Use `python tools/elf_lookup.py 0xADDR` when you need to confirm the original string/rodata at a virtual address | | **Virtual vs direct call** | `bl` vs indirect through vtable | Check const-qualifier; use `GetFoo()` vs `Foo()` | | **Inline vs outlined** | Extra call to helper vs inlined sequence | Force inline by rewriting the expression without calling the helper | | **Loop structure** | Guarded `do/while` from Ghidra or mismatched loop branches | Rewrite to the natural source form suggested by the control flow; in particular, a guarded `do/while` often needs to become a plain `for` loop | diff --git a/AGENTS.md b/AGENTS.md index ca49784a2..bd8a1ec3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,20 @@ object exports written as bare `ADDR:` lines, so you can point it at `symbols/debug_lines.txt` or at a rebuilt `debug_lines.txt` from `tools/dwarf1_gcc_line_info.py`. +### elf_lookup.py — Resolve strings / rodata by virtual address + +When you have a virtual address inside the original ELF and need to know which string or +rodata bytes live there, use: + +```sh +python tools/elf_lookup.py 0x803E58F4 +python tools/elf_lookup.py 0x803E58F4 --mode bytes --length 32 +python tools/elf_lookup.py 0x002F1234 --game ps2 +``` + +This is the preferred replacement for ad-hoc Python snippets that manually parse the ELF +to chase `@stringBase0` or other rodata/data references. + ### code-style — Repo-local style guidance When you are writing code, polishing code you already touched, or doing a style-review pass, @@ -434,6 +448,8 @@ It's very important that you use math inlines from bMath and UMath as shown in t - When you have to use a constant that looks like an address, it's possible that the splitter thought it was an allocation and it shows up as a diff because the left side has a symbol and the right side has a constant. In this case you need to figure out the virtual address of the instruction and block the relocation in config.yml. +- When you need to confirm what lives at a rodata/data address from the original ELF, use + `python tools/elf_lookup.py 0xADDR` instead of writing a one-off Python script. ### PPC EABI calling convention diff --git a/tools/elf_lookup.py b/tools/elf_lookup.py new file mode 100644 index 000000000..de0840a15 --- /dev/null +++ b/tools/elf_lookup.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Look up string or raw bytes in an ELF by virtual address. + +Examples: + python tools/elf_lookup.py 0x803E58F4 + python tools/elf_lookup.py 803E58F4 --mode bytes --length 32 + python tools/elf_lookup.py 0x002F1234 --game ps2 + python tools/elf_lookup.py 0x803E58F4 --elf orig/GOWE69/NFSMWRELEASE.ELF +""" + +import argparse +import os +import string +import sys +from typing import Any, Dict, Optional + +from elftools.elf.elffile import ELFFile + +from _common import ROOT_DIR, ToolError + + +DEFAULT_ELF_BY_GAME = { + "gc": os.path.join(ROOT_DIR, "orig", "GOWE69", "NFSMWRELEASE.ELF"), + "ps2": os.path.join(ROOT_DIR, "orig", "SLES-53558-A124", "NFS.ELF"), +} + + +def parse_address(raw: str) -> int: + try: + return int(raw, 16) + except ValueError as exc: + raise ToolError(f"invalid hex address: {raw}") from exc + + +def choose_elf_path(args: argparse.Namespace) -> str: + if args.elf: + path = args.elf + if not os.path.isabs(path): + path = os.path.join(ROOT_DIR, path) + return os.path.abspath(path) + return DEFAULT_ELF_BY_GAME[args.game] + + +def find_section_for_address(elffile: ELFFile, address: int) -> Optional[Dict[str, Any]]: + for section in elffile.iter_sections(): + header = section.header + start = int(header["sh_addr"]) + size = int(header["sh_size"]) + if size <= 0: + continue + end = start + size + if start <= address < end: + return { + "name": section.name, + "address": start, + "size": size, + "offset": int(header["sh_offset"]), + "data": section.data(), + } + return None + + +def read_c_string(data: bytes, start_offset: int, max_bytes: int) -> bytes: + blob = data[start_offset : start_offset + max_bytes] + terminator = blob.find(b"\x00") + if terminator != -1: + blob = blob[:terminator] + return blob + + +def printable_ratio(blob: bytes) -> float: + if not blob: + return 1.0 + printable = set(string.printable.encode("ascii")) + hits = sum(1 for byte in blob if byte in printable and byte not in b"\x0b\x0c") + return hits / len(blob) + + +def decode_display_string(blob: bytes) -> str: + try: + return blob.decode("utf-8") + except UnicodeDecodeError: + return blob.decode("latin-1", errors="replace") + + +def format_hex_bytes(blob: bytes, width: int = 16) -> str: + lines = [] + for offset in range(0, len(blob), width): + chunk = blob[offset : offset + width] + hex_part = " ".join(f"{byte:02X}" for byte in chunk) + ascii_part = "".join(chr(byte) if 32 <= byte <= 126 else "." for byte in chunk) + lines.append(f" +0x{offset:04X}: {hex_part:<{width * 3}} {ascii_part}") + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Look up a string or raw bytes in an ELF by virtual address." + ) + parser.add_argument("address", help="Virtual address, with or without 0x prefix") + parser.add_argument( + "--game", + choices=sorted(DEFAULT_ELF_BY_GAME), + default="gc", + help="Shortcut for selecting the default GameCube or PS2 ELF (default: gc)", + ) + parser.add_argument( + "--elf", + help="Explicit ELF path. Overrides --game.", + ) + parser.add_argument( + "--mode", + choices=["string", "bytes"], + default="string", + help="Read a C string or raw bytes from the address (default: string)", + ) + parser.add_argument( + "--length", + type=int, + default=64, + help="Maximum bytes to read (default: 64)", + ) + args = parser.parse_args() + + address = parse_address(args.address) + elf_path = choose_elf_path(args) + if not os.path.exists(elf_path): + raise ToolError(f"ELF not found: {elf_path}") + + with open(elf_path, "rb") as f: + elffile = ELFFile(f) + section = find_section_for_address(elffile, address) + + if section is None: + raise ToolError( + f"address 0x{address:08X} is not inside any ELF section in {elf_path}" + ) + + section_offset = address - int(section["address"]) + file_offset = int(section["offset"]) + section_offset + section_data = section["data"] + + print(f"ELF: {os.path.relpath(elf_path, ROOT_DIR)}") + print(f"Address: 0x{address:08X}") + print( + f"Section: {section['name']} +0x{section_offset:X} " + f"(section VA 0x{int(section['address']):08X}, file offset 0x{file_offset:X})" + ) + + if args.mode == "string": + blob = read_c_string(section_data, section_offset, args.length) + display = decode_display_string(blob) + kind = "string" if printable_ratio(blob) >= 0.85 else "non-printable bytes" + print(f"Kind: {kind}") + print(f"Length: {len(blob)} byte(s)") + print(f"Value: {display!r}") + if blob: + print("Hex:") + print(format_hex_bytes(blob)) + return + + blob = section_data[section_offset : section_offset + args.length] + print(f"Length: {len(blob)} byte(s)") + print("Hex:") + print(format_hex_bytes(blob)) + + +if __name__ == "__main__": + try: + main() + except ToolError as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) From 2dcd3a08c115dd4cf0bfba767a8849c55eb98007 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 19:15:37 +0100 Subject: [PATCH 29/71] Fix ProDG dep path rewriting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/transform_dep.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/tools/transform_dep.py b/tools/transform_dep.py index 124de04b9..36dbea004 100755 --- a/tools/transform_dep.py +++ b/tools/transform_dep.py @@ -13,6 +13,7 @@ import argparse import os +import re from platform import uname wineprefix = os.path.join(os.environ["HOME"], ".wine") @@ -27,6 +28,7 @@ def in_wsl() -> bool: def import_d_file(in_file: str) -> str: out_text = "" + build_root = os.getcwd() with open(in_file) as file: for idx, line in enumerate(file): @@ -42,19 +44,26 @@ def import_d_file(in_file: str) -> str: path = line.lstrip()[:-3] else: path = line.strip() - # lowercase drive letter - path = path[0].lower() + path[1:] - if path[0] == "z": - # shortcut for z: - path = path[2:].replace("\\", "/") - elif in_wsl(): - path = path[0:1] + path[2:] - path = os.path.join("/mnt", path.replace("\\", "/")) + path = path.replace("\\", "/") + if re.match(r"^[A-Za-z]:", path): + # lowercase drive letter for Windows-style absolute paths + path = path[0].lower() + path[1:] + if path[0] == "z": + # shortcut for z: + path = path[2:] + elif in_wsl(): + path = path[0:1] + path[2:] + path = os.path.join("/mnt", path) + else: + # use $WINEPREFIX/dosdevices to resolve path + path = os.path.realpath(os.path.join(winedevices, path)) + elif os.path.isabs(path): + path = os.path.realpath(path) else: - # use $WINEPREFIX/dosdevices to resolve path - path = os.path.realpath( - os.path.join(winedevices, path.replace("\\", "/")) - ) + # ProDG often emits repo-relative includes like src\Foo.h. + # Keep them anchored to the current build root instead of + # incorrectly treating them as Wine drive roots. + path = os.path.realpath(os.path.join(build_root, path)) out_text += "\t" + path + suffix + "\n" return out_text From 4aafb334420e5334a323abb42cff5070c9ae3e95 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 19:43:04 +0100 Subject: [PATCH 30/71] Clarify formatter allowlist semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 8 ++++---- AGENTS.md | 8 ++++++-- README.md | 2 +- tools/code_style.py | 16 +++++++++------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 950d7c6ee..2c9ba5f19 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -27,14 +27,14 @@ Use the repo-local helper before doing a style pass: python tools/code_style.py audit --base origin/main ``` -- `audit` classifies changed files into safe vs match-sensitive buckets and reports repo-specific findings. +- `audit` classifies changed files into default-format vs match-sensitive buckets and reports repo-specific findings. - `audit` also checks touched `class` / `struct` declarations against known header declarations and, when no header exists, against the PS2 visibility rule. - `audit` warns on touched local forward declarations when the repo already has a header for that type. - `audit` warns on touched type members that look like invented padding or placeholder names such as `pad`, `unk`, or `field_1234`. - `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, and missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's guard region is touched. - `audit` groups repeated findings by file so branch-wide output stays readable. -- Use `audit --category safe-cpp` for frontend/support cleanup passes and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. -- `format --check` is an opt-in wrapper around the repo's `.clang-format`, but it only targets safe C/C++ files by default. +- Use `audit --category safe-cpp` for the tool's default-format frontend/support bucket and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. +- `format --check` is an opt-in wrapper around the repo's `.clang-format`, but it only targets the tool's default allowlisted C/C++ files by default. - Use `format --check --base origin/main --category safe-cpp` when you want a branch-level formatter probe instead of spelling every file path out. - `format --check` labels whitespace-only formatter output separately from more invasive changes such as include reordering. - `format` never targets `SourceLists/z*.cpp`; those files stay audit-only even when you opt into risky formatting. @@ -56,7 +56,7 @@ Examples: For these files, style cleanup must be conservative and verified. -### 1b. Safer support / frontend / tooling code +### 1b. Default-format support / frontend / tooling code Examples: diff --git a/AGENTS.md b/AGENTS.md index bd8a1ec3a..95cd388f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,8 +110,8 @@ cleanup rules, including jumbo include spacing, initializer-list comment markers placement, pointer style, and how to keep style work safe in match-sensitive code. Use `python tools/code_style.py audit --base origin/main` before a branch-wide style pass. -It classifies changed files, reports repo-specific findings, and only treats safer C/C++ files -as clang-format candidates by default. +It classifies changed files, reports repo-specific findings, and only treats a narrow +allowlisted subset of C/C++ files as clang-format candidates by default. ### decomp-diff.py — Diff & symbol overview @@ -238,6 +238,10 @@ python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName - objdiff instruction match is 100% - normalized DWARF block match is exact +Pass the normal demangled function name to `verify`. The objdiff side matches against both +the demangled and symbol-name fields, and the DWARF side reuses the demangled-name lookup +matching that also tolerates omitted leading namespaces when that information is inconsistent. + If the combined check fails, then inspect the DWARF diff directly with: ```sh diff --git a/README.md b/README.md index e36bd8a4e..d0103a8d1 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ If you have `clang-format` installed locally, you can also use: python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp ``` -The formatter wrapper only targets safer C/C++ files by default. It intentionally skips match-sensitive code unless you explicitly pass `--include-match-sensitive` and verify the affected unit afterwards. +The formatter wrapper only targets a narrow allowlisted subset of C/C++ files by default. That allowlist is about limiting churn, not about whitespace changing codegen by itself. The risky cases are formatter-driven changes such as include reordering and files that rely on the repo's initializer-list guard comments, so match-sensitive code is still skipped unless you explicitly pass `--include-match-sensitive` and verify the affected unit afterwards. `SourceLists/z*.cpp` files remain audit-only and are never formatter targets. `format --check` now distinguishes whitespace-only formatter deltas from more invasive output such as include reordering. Files that use the repo's initializer-list guard comments (`//`) are skipped by default because clang-format fights that convention; override only if you are deliberately inspecting that output. diff --git a/tools/code_style.py b/tools/code_style.py index a1a8f83fa..09be61f20 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -26,7 +26,9 @@ CPP_EXTS = {".c", ".cc", ".cpp", ".h", ".hh", ".hpp"} HEADER_EXTS = {".h", ".hh", ".hpp"} -SAFE_CPP_PREFIXES = ( +# Default clang-format allowlist. This is about limiting churn, not guaranteeing +# byte-match safety. +DEFAULT_FORMAT_CPP_PREFIXES = ( "src/Speed/Indep/Src/Frontend/", "src/Speed/Indep/Src/FEng/", ) @@ -132,7 +134,7 @@ def path_category(path: str) -> str: return "tooling" if path.startswith(JUMBO_PREFIX): return "jumbo-source-list" - if any(path.startswith(prefix) for prefix in SAFE_CPP_PREFIXES): + if any(path.startswith(prefix) for prefix in DEFAULT_FORMAT_CPP_PREFIXES): return "safe-cpp" if ext in CPP_EXTS else "safe-other" if any(path.startswith(prefix) for prefix in MATCH_SENSITIVE_PREFIXES): return "match-sensitive-cpp" if ext in CPP_EXTS else "match-sensitive-other" @@ -692,14 +694,14 @@ def command_audit(args: argparse.Namespace) -> int: print(f" {category}: {len(by_category[category])}") print() - safe_format_candidates = [ + default_format_candidates = [ path for path in paths if path_category(path) == "safe-cpp" and os.path.splitext(path)[1] in CPP_EXTS ] - if safe_format_candidates: - print("Safe clang-format candidates:") - for path in safe_format_candidates: + if default_format_candidates: + print("Default clang-format candidates:") + for path in default_format_candidates: print(f" {path}") print() @@ -923,7 +925,7 @@ def build_parser() -> argparse.ArgumentParser: fmt = subparsers.add_parser( "format", parents=[shared], - help="Run clang-format on safe files by default", + help="Run clang-format on the default allowlisted files by default", ) fmt.add_argument( "--category", From 71946bdd2428673716a6f233a16b302c7a5f533c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 19:58:15 +0100 Subject: [PATCH 31/71] Clarify formatter default scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 6 +++--- AGENTS.md | 4 ++-- README.md | 6 +++--- tools/code_style.py | 7 ++++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 2c9ba5f19..d6a13c738 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -33,12 +33,12 @@ python tools/code_style.py audit --base origin/main - `audit` warns on touched type members that look like invented padding or placeholder names such as `pad`, `unk`, or `field_1234`. - `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, and missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's guard region is touched. - `audit` groups repeated findings by file so branch-wide output stays readable. -- Use `audit --category safe-cpp` for the tool's default-format frontend/support bucket and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. +- Use `audit --category safe-cpp` for the tool's intentionally tiny Frontend/FEng default-format bucket and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. - `format --check` is an opt-in wrapper around the repo's `.clang-format`, but it only targets the tool's default allowlisted C/C++ files by default. - Use `format --check --base origin/main --category safe-cpp` when you want a branch-level formatter probe instead of spelling every file path out. -- `format --check` labels whitespace-only formatter output separately from more invasive changes such as include reordering. +- `format --check` labels whitespace-only formatter output separately from other non-whitespace changes. - `format` never targets `SourceLists/z*.cpp`; those files stay audit-only even when you opt into risky formatting. -- `format` skips files that use initializer-list guard comments (`//`) unless you explicitly override that, because clang-format fights this repo-specific convention. +- `format` skips files that use initializer-list guard comments (`//`) unless you explicitly override that, because clang-format fights this repo-specific layout convention. - `clang-format` itself is optional. If it is not on `PATH`, install it locally or point the helper at it with `CLANG_FORMAT=/path/to/clang-format`. - Do not pass `--include-match-sensitive` unless you are deliberately taking on verification work afterwards. diff --git a/AGENTS.md b/AGENTS.md index 95cd388f2..9285959f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,8 +110,8 @@ cleanup rules, including jumbo include spacing, initializer-list comment markers placement, pointer style, and how to keep style work safe in match-sensitive code. Use `python tools/code_style.py audit --base origin/main` before a branch-wide style pass. -It classifies changed files, reports repo-specific findings, and only treats a narrow -allowlisted subset of C/C++ files as clang-format candidates by default. +It classifies changed files, reports repo-specific findings, and only treats the tiny +Frontend/FEng default bucket as clang-format candidates by default. ### decomp-diff.py — Diff & symbol overview diff --git a/README.md b/README.md index d0103a8d1..037a995f2 100644 --- a/README.md +++ b/README.md @@ -287,10 +287,10 @@ If you have `clang-format` installed locally, you can also use: python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp ``` -The formatter wrapper only targets a narrow allowlisted subset of C/C++ files by default. That allowlist is about limiting churn, not about whitespace changing codegen by itself. The risky cases are formatter-driven changes such as include reordering and files that rely on the repo's initializer-list guard comments, so match-sensitive code is still skipped unless you explicitly pass `--include-match-sensitive` and verify the affected unit afterwards. +The formatter wrapper only targets a tiny default C/C++ bucket by default: currently `src/Speed/Indep/Src/Frontend/` and `src/Speed/Indep/Src/FEng/`. Nothing magical happens in those directories; they are just the initial low-churn UI-heavy areas chosen for branch-wide formatter probes, while the rest of `src/` stays opt-in because most of this repo is match-sensitive decomp code. `SourceLists/z*.cpp` files remain audit-only and are never formatter targets. -`format --check` now distinguishes whitespace-only formatter deltas from more invasive output such as include reordering. -Files that use the repo's initializer-list guard comments (`//`) are skipped by default because clang-format fights that convention; override only if you are deliberately inspecting that output. +`format --check` now distinguishes whitespace-only formatter deltas from other non-whitespace output changes. +Files that use the repo's initializer-list guard comments (`//`) are skipped by default because clang-format fights that convention by reflowing the guarded initializer layout. That is a source-layout concern, not a claim that whitespace by itself changes codegen; if you override it, verify the affected unit afterwards. For declaration-kind checks, header declarations are treated as the repo source of truth; otherwise the helper falls back to the PS2 dump rule (`public:` / `private:` / `protected:` means `class`, no visibility labels means `struct`). `clang-format` is optional. Recommended installs: diff --git a/tools/code_style.py b/tools/code_style.py index 09be61f20..e3f4fb6ea 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -26,8 +26,9 @@ CPP_EXTS = {".c", ".cc", ".cpp", ".h", ".hh", ".hpp"} HEADER_EXTS = {".h", ".hh", ".hpp"} -# Default clang-format allowlist. This is about limiting churn, not guaranteeing -# byte-match safety. +# Seed default for branch-wide clang-format probes: keep the automatic bucket tiny +# and limited to the UI-heavy Frontend/FEng directories. Broader C/C++ stays +# audit-first and opt-in. DEFAULT_FORMAT_CPP_PREFIXES = ( "src/Speed/Indep/Src/Frontend/", "src/Speed/Indep/Src/FEng/", @@ -925,7 +926,7 @@ def build_parser() -> argparse.ArgumentParser: fmt = subparsers.add_parser( "format", parents=[shared], - help="Run clang-format on the default allowlisted files by default", + help="Run clang-format on the tiny Frontend/FEng default allowlist by default", ) fmt.add_argument( "--category", From effd68f12dea2e950d15850321b1078658308905 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 21:59:39 +0100 Subject: [PATCH 32/71] fix mac build --- tools/project.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/project.py b/tools/project.py index a7847c129..793200414 100644 --- a/tools/project.py +++ b/tools/project.py @@ -755,10 +755,13 @@ def write_cargo_rule(): gnu_as_implicit = None ld_cmd = None ld_implicit = None + # macOS has a very low default soft fd limit (256) which is not enough + # for linking hundreds of objects through wibo/wine. + ld_prefix = "ulimit -n 65536 && " if sys.platform == "darwin" else "" if config.platform == Platform.GC_WII: # NGCLD ngcld = compiler_path / "ngcld.exe" - ld_cmd = f"{wrapper_cmd}{ngcld} $ldflags -o $out @$out.rsp" + ld_cmd = f"{ld_prefix}{wrapper_cmd}{ngcld} $ldflags -o $out @$out.rsp" ld_implicit: List[Optional[Path]] = [ compilers_implicit or ngcld, wrapper_implicit, @@ -776,7 +779,7 @@ def write_cargo_rule(): elif config.platform == Platform.X360: # MSVC linker msvc_link = compiler_path / "link.exe" - ld_cmd = f"{wrapper_cmd}{msvc_link} $ldflags /OUT:$out @$out.rsp" + ld_cmd = f"{ld_prefix}{wrapper_cmd}{msvc_link} $ldflags /OUT:$out @$out.rsp" ld_implicit: List[Optional[Path]] = [ compilers_implicit or msvc_link, wrapper_implicit, @@ -795,7 +798,7 @@ def write_cargo_rule(): else: # GNU linker gnu_ld = binutils / f"mips-linux-gnu-ld{EXE}" - ld_cmd = f"{wrapper_cmd}{gnu_ld} $ldflags -o $out @$out.rsp" + ld_cmd = f"{ld_prefix}{wrapper_cmd}{gnu_ld} $ldflags -o $out @$out.rsp" ld_implicit: List[Optional[Path]] = [ compilers_implicit or gnu_ld, wrapper_implicit, From d46f238f937e474de513dbb606ca0221ac1e165a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 22:36:10 +0100 Subject: [PATCH 33/71] tooling: broaden code_style format defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 8 ++++---- AGENTS.md | 4 ++-- README.md | 4 ++-- tools/code_style.py | 33 ++++++++---------------------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index d6a13c738..97179bab8 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -33,14 +33,14 @@ python tools/code_style.py audit --base origin/main - `audit` warns on touched type members that look like invented padding or placeholder names such as `pad`, `unk`, or `field_1234`. - `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, and missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's guard region is touched. - `audit` groups repeated findings by file so branch-wide output stays readable. -- Use `audit --category safe-cpp` for the tool's intentionally tiny Frontend/FEng default-format bucket and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. -- `format --check` is an opt-in wrapper around the repo's `.clang-format`, but it only targets the tool's default allowlisted C/C++ files by default. +- Use `audit --category safe-cpp` when you want a smaller Frontend/FEng-focused subset and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. +- `format --check` is an opt-in wrapper around the repo's `.clang-format`, and by default it targets eligible changed C/C++ files, including match-sensitive code. - Use `format --check --base origin/main --category safe-cpp` when you want a branch-level formatter probe instead of spelling every file path out. - `format --check` labels whitespace-only formatter output separately from other non-whitespace changes. - `format` never targets `SourceLists/z*.cpp`; those files stay audit-only even when you opt into risky formatting. -- `format` skips files that use initializer-list guard comments (`//`) unless you explicitly override that, because clang-format fights this repo-specific layout convention. +- Files that use initializer-list guard comments (`//`) are still formatter targets; if a formatting pass touches match-sensitive code, verify the affected unit afterwards. - `clang-format` itself is optional. If it is not on `PATH`, install it locally or point the helper at it with `CLANG_FORMAT=/path/to/clang-format`. -- Do not pass `--include-match-sensitive` unless you are deliberately taking on verification work afterwards. +- Do not assume a formatting-only change is automatically byte-stable; verify affected units when the formatter touches match-sensitive code. ## Phase 1: Classify the File Before Cleaning diff --git a/AGENTS.md b/AGENTS.md index 9285959f8..e5f644c7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,8 +110,8 @@ cleanup rules, including jumbo include spacing, initializer-list comment markers placement, pointer style, and how to keep style work safe in match-sensitive code. Use `python tools/code_style.py audit --base origin/main` before a branch-wide style pass. -It classifies changed files, reports repo-specific findings, and only treats the tiny -Frontend/FEng default bucket as clang-format candidates by default. +It classifies changed files, reports repo-specific findings, and can run clang-format +across eligible changed C/C++ files by default (excluding `SourceLists/z*.cpp`). ### decomp-diff.py — Diff & symbol overview diff --git a/README.md b/README.md index eba142b0e..88ecd7210 100644 --- a/README.md +++ b/README.md @@ -277,10 +277,10 @@ If you have `clang-format` installed locally, you can also use: python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp ``` -The formatter wrapper only targets a tiny default C/C++ bucket by default: currently `src/Speed/Indep/Src/Frontend/` and `src/Speed/Indep/Src/FEng/`. Nothing magical happens in those directories; they are just the initial low-churn UI-heavy areas chosen for branch-wide formatter probes, while the rest of `src/` stays opt-in because most of this repo is match-sensitive decomp code. +The formatter wrapper targets eligible changed C/C++ files by default, including match-sensitive code. If you want a smaller focused pass, restrict it with `--category safe-cpp`, which currently maps to `src/Speed/Indep/Src/Frontend/` and `src/Speed/Indep/Src/FEng/`. `SourceLists/z*.cpp` files remain audit-only and are never formatter targets. `format --check` now distinguishes whitespace-only formatter deltas from other non-whitespace output changes. -Files that use the repo's initializer-list guard comments (`//`) are skipped by default because clang-format fights that convention by reflowing the guarded initializer layout. That is a source-layout concern, not a claim that whitespace by itself changes codegen; if you override it, verify the affected unit afterwards. +Files that use the repo's initializer-list guard comments (`//`) are formatter targets too. If a formatting pass touches match-sensitive code, rebuild and verify the affected unit afterwards instead of assuming the change is automatically byte-stable. For declaration-kind checks, header declarations are treated as the repo source of truth; otherwise the helper falls back to the PS2 dump rule (`public:` / `private:` / `protected:` means `class`, no visibility labels means `struct`). `clang-format` is optional. Recommended installs: diff --git a/tools/code_style.py b/tools/code_style.py index e3f4fb6ea..ddbe3436d 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -26,10 +26,10 @@ CPP_EXTS = {".c", ".cc", ".cpp", ".h", ".hh", ".hpp"} HEADER_EXTS = {".h", ".hh", ".hpp"} -# Seed default for branch-wide clang-format probes: keep the automatic bucket tiny -# and limited to the UI-heavy Frontend/FEng directories. Broader C/C++ stays -# audit-first and opt-in. -DEFAULT_FORMAT_CPP_PREFIXES = ( +# Small focused C/C++ subset for targeted probes. The format command itself +# now covers all eligible changed C/C++ files by default; this bucket remains +# useful when a caller explicitly wants a narrower Frontend/FEng-only pass. +SAFE_CPP_PREFIXES = ( "src/Speed/Indep/Src/Frontend/", "src/Speed/Indep/Src/FEng/", ) @@ -135,7 +135,7 @@ def path_category(path: str) -> str: return "tooling" if path.startswith(JUMBO_PREFIX): return "jumbo-source-list" - if any(path.startswith(prefix) for prefix in DEFAULT_FORMAT_CPP_PREFIXES): + if any(path.startswith(prefix) for prefix in SAFE_CPP_PREFIXES): return "safe-cpp" if ext in CPP_EXTS else "safe-other" if any(path.startswith(prefix) for prefix in MATCH_SENSITIVE_PREFIXES): return "match-sensitive-cpp" if ext in CPP_EXTS else "match-sensitive-other" @@ -785,9 +785,7 @@ def find_clang_format() -> str: def format_paths(paths: Iterable[str], include_match_sensitive: bool) -> List[str]: - allowed = {"safe-cpp"} - if include_match_sensitive: - allowed.add("match-sensitive-cpp") + allowed = {"safe-cpp", "match-sensitive-cpp"} return [ relpath(path) @@ -808,17 +806,11 @@ def command_format(args: argparse.Namespace) -> int: clang_format = find_clang_format() changed: List[str] = [] changed_summaries: Dict[str, str] = {} - skipped_initializer_guards: List[str] = [] - for path in selected: abs_path = os.path.join(root_dir, path) with open(abs_path, encoding="utf-8", errors="ignore") as f: before = f.read() - if has_initializer_guard_comments(before) and not args.include_initializer_guards: - skipped_initializer_guards.append(path) - continue - if args.check: result = subprocess.run( [clang_format, "--style=file", abs_path], @@ -837,13 +829,6 @@ def command_format(args: argparse.Namespace) -> int: return result.returncode changed.append(path) - if skipped_initializer_guards: - print("Skipped files with initializer-list guard comments:") - for path in skipped_initializer_guards: - print(f" {path}") - print(" clang-format fights this repo convention; inspect these manually or override explicitly.") - print() - if args.check: if changed: print("Would reformat:") @@ -926,7 +911,7 @@ def build_parser() -> argparse.ArgumentParser: fmt = subparsers.add_parser( "format", parents=[shared], - help="Run clang-format on the tiny Frontend/FEng default allowlist by default", + help="Run clang-format on changed C/C++ files by default (SourceLists stay excluded)", ) fmt.add_argument( "--category", @@ -942,12 +927,12 @@ def build_parser() -> argparse.ArgumentParser: fmt.add_argument( "--include-match-sensitive", action="store_true", - help="Also format match-sensitive C/C++ files (dangerous; verify afterwards). SourceLists files stay excluded.", + help="Deprecated no-op kept for compatibility; eligible match-sensitive C/C++ files are already included by default.", ) fmt.add_argument( "--include-initializer-guards", action="store_true", - help="Also format files that use initializer-list guard comments (`//`). Disabled by default because clang-format fights that repo convention.", + help="Deprecated no-op kept for compatibility; files with initializer-list guard comments are formatted by default.", ) fmt.set_defaults(func=command_format) From 1bb0e62e8e2ec3b4e0d19de981f65a43728bc0a6 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 23:22:58 +0100 Subject: [PATCH 34/71] tooling: globalize clang-format scope Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 3 ++- AGENTS.md | 2 +- README.md | 2 +- tools/code_style.py | 14 +++++++------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 97179bab8..5218fb40b 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -35,9 +35,10 @@ python tools/code_style.py audit --base origin/main - `audit` groups repeated findings by file so branch-wide output stays readable. - Use `audit --category safe-cpp` when you want a smaller Frontend/FEng-focused subset and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. - `format --check` is an opt-in wrapper around the repo's `.clang-format`, and by default it targets eligible changed C/C++ files, including match-sensitive code. +- Use `format --check --base origin/main` for a branch-wide formatter pass over all changed C/C++ files. - Use `format --check --base origin/main --category safe-cpp` when you want a branch-level formatter probe instead of spelling every file path out. - `format --check` labels whitespace-only formatter output separately from other non-whitespace changes. -- `format` never targets `SourceLists/z*.cpp`; those files stay audit-only even when you opt into risky formatting. +- `format` also accepts `SourceLists/z*.cpp` and other repo C/C++ files; if a formatting pass touches match-sensitive code, verify the affected unit afterwards. - Files that use initializer-list guard comments (`//`) are still formatter targets; if a formatting pass touches match-sensitive code, verify the affected unit afterwards. - `clang-format` itself is optional. If it is not on `PATH`, install it locally or point the helper at it with `CLANG_FORMAT=/path/to/clang-format`. - Do not assume a formatting-only change is automatically byte-stable; verify affected units when the formatter touches match-sensitive code. diff --git a/AGENTS.md b/AGENTS.md index e5f644c7b..d367fc237 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ placement, pointer style, and how to keep style work safe in match-sensitive cod Use `python tools/code_style.py audit --base origin/main` before a branch-wide style pass. It classifies changed files, reports repo-specific findings, and can run clang-format -across eligible changed C/C++ files by default (excluding `SourceLists/z*.cpp`). +across eligible changed C/C++ files by default. ### decomp-diff.py — Diff & symbol overview diff --git a/README.md b/README.md index 88ecd7210..b67d7d54c 100644 --- a/README.md +++ b/README.md @@ -274,11 +274,11 @@ python tools/code_style.py format --check --base origin/main --category safe-cpp If you have `clang-format` installed locally, you can also use: ```sh +python tools/code_style.py format --check --base origin/main python tools/code_style.py format --check src/Speed/Indep/Src/Frontend/FEManager.cpp ``` The formatter wrapper targets eligible changed C/C++ files by default, including match-sensitive code. If you want a smaller focused pass, restrict it with `--category safe-cpp`, which currently maps to `src/Speed/Indep/Src/Frontend/` and `src/Speed/Indep/Src/FEng/`. -`SourceLists/z*.cpp` files remain audit-only and are never formatter targets. `format --check` now distinguishes whitespace-only formatter deltas from other non-whitespace output changes. Files that use the repo's initializer-list guard comments (`//`) are formatter targets too. If a formatting pass touches match-sensitive code, rebuild and verify the affected unit afterwards instead of assuming the change is automatically byte-stable. For declaration-kind checks, header declarations are treated as the repo source of truth; otherwise the helper falls back to the PS2 dump rule (`public:` / `private:` / `protected:` means `class`, no visibility labels means `struct`). diff --git a/tools/code_style.py b/tools/code_style.py index ddbe3436d..ecb85f713 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -695,14 +695,14 @@ def command_audit(args: argparse.Namespace) -> int: print(f" {category}: {len(by_category[category])}") print() - default_format_candidates = [ + safe_cpp_candidates = [ path for path in paths if path_category(path) == "safe-cpp" and os.path.splitext(path)[1] in CPP_EXTS ] - if default_format_candidates: - print("Default clang-format candidates:") - for path in default_format_candidates: + if safe_cpp_candidates: + print("Focused safe-cpp subset:") + for path in safe_cpp_candidates: print(f" {path}") print() @@ -785,12 +785,12 @@ def find_clang_format() -> str: def format_paths(paths: Iterable[str], include_match_sensitive: bool) -> List[str]: - allowed = {"safe-cpp", "match-sensitive-cpp"} + del include_match_sensitive return [ relpath(path) for path in paths - if path_category(path) in allowed and os.path.splitext(path)[1] in CPP_EXTS + if os.path.splitext(path)[1] in CPP_EXTS ] @@ -911,7 +911,7 @@ def build_parser() -> argparse.ArgumentParser: fmt = subparsers.add_parser( "format", parents=[shared], - help="Run clang-format on changed C/C++ files by default (SourceLists stay excluded)", + help="Run clang-format on changed C/C++ files by default", ) fmt.add_argument( "--category", From 2564a6ac7a92aebf7438551f03141ea47cd5df40 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 11:48:31 +0100 Subject: [PATCH 35/71] agent % --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d367fc237..fc4f7af24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -363,13 +363,13 @@ python tools/decomp-status.py --unit main/Path/To/TU Commit whenever the match percentage increases (e.g. you matched a new function). Use this format for the commit message: ``` -n.n%: short description of what was matched or changed +n.n[n]%: short description of what was matched or changed ``` Examples: - `42.1%: match UpdateCamera` -- `78.5%: match PlayerController constructor and destructor` +- `78.56%: match PlayerController constructor and destructor` - `100.0%: full match for zAnim` Do not batch up multiple percentage milestones into one commit — commit as each improvement lands. From 80df004796b6ca9b25d64be548945ab9b4377cfb Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:56:02 +0100 Subject: [PATCH 36/71] fix: link shared assets and track agent skills symlinks --- .agents/skills/code_style | 1 + .agents/skills/execute | 1 + .agents/skills/ghidra | 1 + .agents/skills/implement | 1 + .agents/skills/line_lookup | 1 + .agents/skills/lookup | 1 + .agents/skills/refiner | 1 + .agents/skills/scaffold | 1 + 8 files changed, 8 insertions(+) create mode 120000 .agents/skills/code_style create mode 120000 .agents/skills/execute create mode 120000 .agents/skills/ghidra create mode 120000 .agents/skills/implement create mode 120000 .agents/skills/line_lookup create mode 120000 .agents/skills/lookup create mode 120000 .agents/skills/refiner create mode 120000 .agents/skills/scaffold diff --git a/.agents/skills/code_style b/.agents/skills/code_style new file mode 120000 index 000000000..edb444fb7 --- /dev/null +++ b/.agents/skills/code_style @@ -0,0 +1 @@ +../../.github/skills/code_style \ No newline at end of file diff --git a/.agents/skills/execute b/.agents/skills/execute new file mode 120000 index 000000000..ff61f8ce2 --- /dev/null +++ b/.agents/skills/execute @@ -0,0 +1 @@ +../../.github/skills/execute \ No newline at end of file diff --git a/.agents/skills/ghidra b/.agents/skills/ghidra new file mode 120000 index 000000000..e348149ed --- /dev/null +++ b/.agents/skills/ghidra @@ -0,0 +1 @@ +../../.github/skills/ghidra \ No newline at end of file diff --git a/.agents/skills/implement b/.agents/skills/implement new file mode 120000 index 000000000..609ac0075 --- /dev/null +++ b/.agents/skills/implement @@ -0,0 +1 @@ +../../.github/skills/implement \ No newline at end of file diff --git a/.agents/skills/line_lookup b/.agents/skills/line_lookup new file mode 120000 index 000000000..98bcd185e --- /dev/null +++ b/.agents/skills/line_lookup @@ -0,0 +1 @@ +../../.github/skills/line_lookup \ No newline at end of file diff --git a/.agents/skills/lookup b/.agents/skills/lookup new file mode 120000 index 000000000..e0466a56a --- /dev/null +++ b/.agents/skills/lookup @@ -0,0 +1 @@ +../../.github/skills/lookup \ No newline at end of file diff --git a/.agents/skills/refiner b/.agents/skills/refiner new file mode 120000 index 000000000..d61e56ea1 --- /dev/null +++ b/.agents/skills/refiner @@ -0,0 +1 @@ +../../.github/skills/refiner \ No newline at end of file diff --git a/.agents/skills/scaffold b/.agents/skills/scaffold new file mode 120000 index 000000000..5684eddc2 --- /dev/null +++ b/.agents/skills/scaffold @@ -0,0 +1 @@ +../../.github/skills/scaffold \ No newline at end of file From d76c2f640a7b741902a5dbe13adf6f5423e89e46 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 17:35:17 +0100 Subject: [PATCH 37/71] fix diff --- tools/decomp-diff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index 13e54e26f..7b50a6b02 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -20,6 +20,7 @@ from typing import Any, Dict, List, Optional, Tuple from _common import ( ROOT_DIR, + RELOC_DIFF_CHOICES, ToolError, build_objdiff_symbol_rows, fail, From 1a54bf300cac21f922f60417ef1928a37f49e777 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:03:54 +0100 Subject: [PATCH 38/71] improvements derived from zBWare review changes --- .github/skills/code_style/SKILL.md | 33 +++++++++- .github/skills/execute/SKILL.md | 9 +++ .github/skills/implement/SKILL.md | 29 +++++++++ .github/skills/refiner/SKILL.md | 19 ++++++ .github/skills/scaffold/SKILL.md | 20 +++++- tools/code_style.py | 63 +++++++++++++++++- tools/decomp-workflow.py | 100 +++++++++++++++++++++++------ 7 files changed, 251 insertions(+), 22 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 5218fb40b..d5b2cbbb3 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -31,7 +31,7 @@ python tools/code_style.py audit --base origin/main - `audit` also checks touched `class` / `struct` declarations against known header declarations and, when no header exists, against the PS2 visibility rule. - `audit` warns on touched local forward declarations when the repo already has a header for that type. - `audit` warns on touched type members that look like invented padding or placeholder names such as `pad`, `unk`, or `field_1234`. -- `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, and missing `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's guard region is touched. +- `audit` also checks touched style-guide rules that clang-format cannot enforce for you, such as cast spacing, `using namespace`, `NULL`, bare `#if MACRO` presence checks, recovered layout members that still use raw `unsigned char` / `unsigned short`, and missing or misordered `EA_PRAGMA_ONCE_SUPPORTED` guard blocks when a header's prologue is touched. - `audit` groups repeated findings by file so branch-wide output stays readable. - Use `audit --category safe-cpp` when you want a smaller Frontend/FEng-focused subset and `audit --category match-sensitive-cpp` when you want a conservative review queue for decomp code. - `format --check` is an opt-in wrapper around the repo's `.clang-format`, and by default it targets eligible changed C/C++ files, including match-sensitive code. @@ -95,7 +95,14 @@ Foo::Foo() - Use `nullptr` exclusively for null pointers. - Prefer `if (ptr)` / `if (!ptr)` over explicit null comparisons when the change is local and verified safe. - When a match-sensitive TU has many explicit `nullptr` checks and you decide to normalize them, prefer one mechanical full-TU pass over piecemeal cleanup. Rebuild the unit and re-check its status before keeping the rewrite. +- When a helper is doing address arithmetic, prefer `intptr_t` / `uintptr_t` or byte-pointer (`reinterpret_cast`) math over plain `int` parameters or integerized pointer subtraction. - Inline assembly is acceptable when it is needed to preserve dead-code compares, ordering, or other compiler behavior that source alone cannot reproduce. +- In low-level list / node / allocator code, prefer existing helper methods such as `AddBefore`, `AddAfter`, `Remove`, `GetPrev`, `GetNext`, or typed accessors over open-coding link rewiring once the helper exists. + +### Header prologues and preprocessor checks + +- In headers, keep the guard / `EA_PRAGMA_ONCE_SUPPORTED` block before any project `#include`; do not place includes ahead of `#pragma once`. +- Use `#ifdef MACRO` / `#ifndef MACRO` for presence checks. Reserve bare `#if MACRO` for cases where you really need the macro's numeric value. ### Forward declarations and local prototypes @@ -103,6 +110,7 @@ Foo::Foo() - If the repo already has a header declaration/definition for a type, include that header instead of redeclaring the type locally. - If the repo only has an empty or stub owner header, and line info / surrounding source clearly points at that header's subsystem, prefer populating that owner header over leaving a recovered project type declaration inside a `.cpp`. - Only keep a local forward declaration when no canonical repo header exists yet and you have verified that the ownership is still unresolved. +- Likewise for project free functions: if a declaration is shared across translation units, move it into the owning header instead of leaving ad-hoc local prototypes in `.cpp` files. - Prefer moving helper template declarations next to their real use site instead of leaving them in an unrelated file. ### Pointer style @@ -117,10 +125,13 @@ Foo::Foo() - Preserve the original `class` / `struct` kind from existing headers or Dwarf / PS2 evidence; do not treat it as a cosmetic style choice. - Treat header declarations as the repo source of truth. If the repo only has local `.cpp` partial declarations, verify the kind with the PS2 dump instead of copying them blindly. - Even forward declarations and local partial declarations should use the accurate keyword when known. +- Keep the `// total size: 0x...` comment above the recovered type declaration instead of burying it inside the body. +- When a recovered type is a `class`, keep explicit access sections and put the method/accessor block before the member layout block unless existing repo evidence shows otherwise. - Preserve the member naming style that DWARF shows. Some types use `mMember`, others use `m_member`; do not normalize them. - Preserve recovered member names, types, order, and offset comments. Do not invent placeholder members named `pad`, `unk`, `unknown`, or `field_XXXX` for game code just to make a layout compile. - If a member is genuinely unknown, stop and verify it with `find-symbol.py`, GC Dwarf, and PS2 data. If the layout is still incomplete, add a short TODO above the type instead of burying uncertainty in fake member names. - Add offset / size comments when you are writing recovered type layouts from DWARF. +- In recovered layouts, prefer explicit-width aliases such as `uint8` / `uint16` when the field width is known. Use plain `char` for text / byte buffers and `signed char` when the field is a signed 8-bit counter. - Define inline member functions in headers only when DWARF shows that they are genuinely inlined in the binary. - Use `struct` for POD-like data carriers with public fields; use `class` for behavior-heavy types only when that matches the recovered type information. - Keep tiny placeholder methods as concise inline bodies when that is already the local pattern. @@ -134,13 +145,27 @@ Foo::Foo() ### Dense local code - Expand dense one-line helper structs, declaration blocks, and function bodies in non-match-sensitive files into normal multiline formatting. +- In low-level headers, prefer normal multi-line bodies for touched inline operators and accessors instead of stacking `{ return ...; }` on one line, unless the surrounding file clearly uses intentional placeholder one-liners. - Prefer readable blocks over stacked one-line statements when behavior does not depend on exact source shape. +- In touched validation/parsing code, prefer explicit min/max or boundary checks over equivalent magic-constant arithmetic when the clearer form still compiles to the verified result. +- In parser/state-table code, prefer named enums and enum-typed state variables over anonymous integer state codes when that rewrite is verified safe. + +### Recovery markers + +- Remove stale recovery markers such as `// TODO`, `// UNSOLVED`, or `// STRIPPED` when the touched code is now implemented or understood. +- If a marker still needs to stay, give it short context such as ownership uncertainty, a Dwarf caveat, a platform/config note, or a scratch/link reference. Avoid bare marker-only comments. +- Do not leave `// TODO` hanging off a declaration or helper you just implemented; either finish the thought or remove the marker. ### Uncertain ownership - If a declaration or global clearly compiles but its original home is uncertain, add a short TODO comment instead of inventing structure you cannot justify yet. - When ownership matters, verify it with `decomp-workflow.py`, `decomp-context.py`, and `line-lookup` before moving code. +### Readable helper extraction + +- When touched recovered code repeats the same pointer/boundary arithmetic, prefer a short named helper or accessor such as `GetTop`, `GetBot`, `GetNext`, `GetPrev`, `GetStringTableStart`, or `GetStringTableEnd` if that shape is already supported by Dwarf/inlining evidence. +- Prefer call sites that use those helpers or existing container APIs over re-encoding the same arithmetic or link manipulation inline. + ## Phase 3: Things Not To "Clean Up" Blindly - Do not move an inline method out of a header just because it looks cleaner. @@ -172,3 +197,9 @@ Keep the cleanup only if the build succeeds and the relevant match status is unc - The trailing `//` initializer-list markers are an intentional repo convention, not noise to remove. - Small `if (ptr)` cleanup batches can be kept in match-sensitive code, but only after rebuilding the affected unit. - Dense frontend shim files benefit from multiline struct/prototype/function formatting. +- Header prologues should keep the `EA_PRAGMA_ONCE_SUPPORTED` block ahead of includes, not after them. +- Bare `#if MACRO` presence checks are review bait; use `#ifdef` / `#ifndef` unless you are intentionally testing a numeric config value. +- Reviewed recovered headers tend to keep total-size comments above the type, methods before fields, explicit access sections, and fixed-width aliases for width-known narrow integer members. +- Reviewed fixups also remove stale bare recovery markers or replace them with context, and prefer existing list/node helpers over hand-written pointer/link rewiring. +- Some reviewed fixups improved readability without losing match by replacing opaque range-check arithmetic with explicit bounds and by moving repeated pointer/boundary math behind short named helpers. +- Other recurring review churn came from plain-`int` address helpers, stray local `.cpp` prototypes for shared functions, and integer-coded parser states where named enums were clearer but still matched. diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 7abf9cb3f..5ba0bd156 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -11,6 +11,9 @@ the produced C++ compiles to byte-identical object code against the original ret For each function, "done" means both objdiff and normalized DWARF are exact. +Human review is not a substitute for running `dwarf compare`. Each function should hit +its own `verify` gate before you treat it as ready to hand off, commit, or move past. + ## Overview This workflow combines several smaller workflows: @@ -152,6 +155,10 @@ python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName If it fails, follow up with `decomp-workflow.py diff` and `decomp-workflow.py dwarf` until both checks pass. +Do not queue up several "probably done" functions and leave the DWARF check for later. +Close the `verify` gate per function before moving on whenever feasible; otherwise the +reviewer ends up doing avoidable DWARF triage. + ### 3g. Periodic reassessment After every few functions, re-run the full status check: @@ -189,6 +196,8 @@ For any remaining nonmatching functions, make one final pass using the implement or refiner workflow with all context accumulated during the session. Do not report a function as complete unless its per-function `verify` check also passes. +Do not hand a function to review as "done except maybe DWARF" — either resolve the DWARF +failure yourself or explicitly call out the blocker and why it remains. ## Phase 5: Report diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index 25f56b926..cbd94da78 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -9,6 +9,11 @@ Your goal is to decompile a specific function: writing C++ source that compiles A function is not done until it is exact in both objdiff and normalized DWARF. +Reviewers should not be spending their time rediscovering DWARF mismatches. Before you +report progress, ask for review, hand the function off, or switch to another target, you +must run the per-function verification gate yourself and treat any DWARF failure as your +next task, not as review debt. + ## Phase 1: Gather Context Collect data from **all** of these sources in parallel where possible. @@ -156,6 +161,16 @@ python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName If the build fails, fix compilation errors first. +As soon as you have a compiling draft, run the combined verification gate immediately: + +```sh +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +Do this before you spend a long time polishing late instruction mismatches. If `verify` +already shows a DWARF failure, fix that first so you are not polishing code the reviewer +will bounce anyway. + ### Check the diff ```sh @@ -203,6 +218,17 @@ debug-line owner files for each DWARF `// Range:` block, which makes it much eas spot inlines that are coming from the wrong header or owner file. Exact line-number agreement is a useful secondary hint, but file ownership is the first thing to check. +Use this as the default loop when the function compiles but `verify` is still failing: + +1. Run `verify`. +2. If DWARF fails, run `dwarf`. +3. Fix the structural issue the DWARF diff is pointing at first: missing/extra locals, + wrong qualifiers or parameter types, wrong inline ownership, wrong helper/header owner, + or a source shape that outlined something that should be inlined. +4. Rebuild and rerun `verify`. +5. Only return to instruction-by-instruction cleanup once the remaining failures are no + longer obvious DWARF-compare issues. + Manual fallback: After writing your code, you can also run the dwarf dump on the compiled output and then query your output dump with lookup.py to compare your decompiled functions against the originals. Since the address of the function you're working on can keep changing @@ -233,6 +259,9 @@ Every mismatched instruction is a signal — don't settle for "close enough". Reaching 100% instruction matching status is not enough. Stay in the loop until `verify` passes, which means the DWARF of the function also matches after normalization. +Do not leave a function in a "review-ready" or "good enough for now" state with a known +DWARF failure unless you are explicitly blocked and you document that blocker clearly. + ## Phase 5: Report Summarize: diff --git a/.github/skills/refiner/SKILL.md b/.github/skills/refiner/SKILL.md index a6aeb2125..0054e6e18 100644 --- a/.github/skills/refiner/SKILL.md +++ b/.github/skills/refiner/SKILL.md @@ -15,9 +15,25 @@ approaches that were tried before — instead, apply systematic lateral analysis - A diff is available (`decomp-diff.py -u -d `). - The "obvious" translation from Ghidra has been attempted. - You have been given the current source code and the diff. +- You have already run the per-function `verify` gate and know whether the remaining work + is still structural DWARF cleanup or true late-stage instruction cleanup. + +Refiner is not the place to dump unresolved DWARF debt on a reviewer. If `verify` or +`dwarf` is still showing obvious structural mismatches (missing locals, wrong types, +wrong inline ownership, wrong helper/header owner), fix those first or drop back to the +implementer workflow before doing late instruction polish. ## Phase 1: Read the full diff without collapsing +Before you start a refiner pass, confirm the gate status: + +```sh +python tools/decomp-workflow.py verify -u main/Path/To/TU -f FunctionName +``` + +If the combined gate is failing for reasons that are still clearly visible in the DWARF +diff, address those first instead of treating them as reviewer follow-up. + Preferred shortcut: ```sh @@ -151,6 +167,9 @@ DWARF mismatches to watch for: - Wrong return type - Missing inlined function records (means an inline call was outlined) +If these mismatches are still present, you are not in pure refiner territory yet. Resolve +them before you ask a reviewer to spend time on the function. + ## Phase 4: Report Summarize: diff --git a/.github/skills/scaffold/SKILL.md b/.github/skills/scaffold/SKILL.md index a2f062f50..3fe3d9256 100644 --- a/.github/skills/scaffold/SKILL.md +++ b/.github/skills/scaffold/SKILL.md @@ -39,6 +39,9 @@ Preserve the real `class` / `struct` kind while scaffolding. Check existing head then use Dwarf plus PS2 visibility / vtable info to decide the type kind. Even temporary forward declarations should match the known original kind. +Keep the header prologue in repo order: header guard, `EA_PRAGMA_ONCE_SUPPORTED` block, +then includes. Do not drop project includes ahead of `#pragma once`. + If the repo already has a header for a type you need, include that header instead of adding a new local forward declaration. Only forward-declare when no canonical repo header exists yet and you have verified that the ownership is still unresolved. @@ -47,11 +50,26 @@ Preserve real member names, types, order, and offset comments while scaffolding. fill gaps with invented `pad`, `unk`, or `field_XXXX` members for game types; verify the layout from Dwarf / PS2 data and leave a TODO over the type if a field is still uncertain. +Keep the `// total size: 0x...` comment above the recovered type declaration. When the +recovered type is a `class`, keep explicit access sections and prefer putting methods / +accessors before the member layout block unless existing repo evidence says otherwise. + +When a recovered field width is known, prefer explicit-width aliases such as `uint8` / +`uint16` over raw `unsigned char` / `unsigned short`. Use plain `char` for string or byte +buffers and `signed char` when the field is a signed 8-bit counter. + +If a recovered type repeatedly walks neighbors, boundaries, or in-object offsets, prefer +small named helpers such as `GetTop`, `GetBot`, `GetNext`, `GetPrev`, or boundary getters +instead of repeating raw pointer arithmetic at each call site. + +When those helpers operate on addresses or byte offsets, prefer `intptr_t` / `uintptr_t` +or explicit byte-pointer arithmetic instead of plain `int` address parameters. + Only create headers if it's really necessary (the struct doesn't have inlines so you can't determine in which header file it goes and it's thematically very different from the other structs that use it), otherwise put it into the one you determined to be correct. The dwarf often has duplicated inlines, clean those up according to the order in the PS2 info. -Write a TODO comment over the struct/class if you aren't 100% sure that it belongs to the correct header. +Write a TODO comment over the struct/class if you aren't 100% sure that it belongs to the correct header, and say why (ownership uncertainty, circular dependency, dwarf caveat, etc.) instead of leaving a bare marker. ## Phase 3: Add needed files to jumbo file and compile diff --git a/tools/code_style.py b/tools/code_style.py index ecb85f713..61c3f2ee3 100644 --- a/tools/code_style.py +++ b/tools/code_style.py @@ -84,6 +84,7 @@ class Finding: ) USING_NAMESPACE_PATTERN = re.compile(r"^\s*using\s+namespace\b") NULL_PATTERN = re.compile(r"\bNULL\b") +BARE_PRESENCE_IF_PATTERN = re.compile(r"^\s*#if\s+([A-Za-z_][A-Za-z0-9_]*)\s*$") HEADER_GUARD_IFNDEF_PATTERN = re.compile(r"^\s*#ifndef\s+[A-Za-z0-9_]+\s*$", re.MULTILINE) HEADER_GUARD_DEFINE_PATTERN = re.compile(r"^\s*#define\s+[A-Za-z0-9_]+\s*$", re.MULTILINE) EA_PRAGMA_BLOCK_PATTERN = re.compile( @@ -92,6 +93,16 @@ class Finding: r".*?^\s*#endif\s*$", re.MULTILINE | re.DOTALL, ) +EA_PRAGMA_IFDEF_PATTERN = re.compile( + r"^\s*#ifdef\s+EA_PRAGMA_ONCE_SUPPORTED\s*$", re.MULTILINE +) +RECOVERED_LAYOUT_COMMENT_PATTERN = re.compile( + r"//\s*offset 0x[0-9A-Fa-f]+,\s*size 0x[0-9A-Fa-f]+" +) +RECOVERED_NARROW_UNSIGNED_PATTERN = re.compile(r"\bunsigned\s+(char|short)\b") +BARE_RECOVERY_MARKER_PATTERN = re.compile( + r"//\s*(TODO|UNSOLVED|STRIPPED)\b(?:\s*[.:,-]*)?\s*$" +) SUSPICIOUS_MEMBER_PATTERN = re.compile( r"^(?:" r"_?pad(?:ding)?[0-9A-Fa-f_]*" @@ -441,6 +452,16 @@ def audit_style_guide_rules( if touched_lines is not None and idx not in touched_lines: continue stripped = line.strip() + bare_recovery_marker_match = BARE_RECOVERY_MARKER_PATTERN.search(line) + if bare_recovery_marker_match is not None: + findings.append( + Finding( + path, + idx, + "INFO", + f"`// {bare_recovery_marker_match.group(1)}` has no context; add a short reason or remove the stale recovery marker", + ) + ) if stripped.startswith("//"): continue @@ -471,9 +492,35 @@ def audit_style_guide_rules( "use `nullptr` instead of `NULL`", ) ) + bare_presence_if_match = BARE_PRESENCE_IF_PATTERN.match(line) + if bare_presence_if_match is not None: + findings.append( + Finding( + path, + idx, + "WARN", + f"bare `#if {bare_presence_if_match.group(1)}` looks like a presence check; prefer `#ifdef {bare_presence_if_match.group(1)}` unless a numeric test is intentional", + ) + ) + narrow_type_match = RECOVERED_NARROW_UNSIGNED_PATTERN.search(line) + if ( + narrow_type_match is not None + and RECOVERED_LAYOUT_COMMENT_PATTERN.search(line) is not None + ): + preferred = "uint8" if narrow_type_match.group(1) == "char" else "uint16" + findings.append( + Finding( + path, + idx, + "INFO", + f"recovered layout member uses `{narrow_type_match.group(0)}`; prefer explicit-width `{preferred}` when the field width is known", + ) + ) if ext in HEADER_EXTS: - should_check_guard = touched_lines is None or any(line_no <= 8 for line_no in touched_lines) + should_check_guard = touched_lines is None or any( + line_no <= 12 for line_no in touched_lines + ) if should_check_guard: has_ifndef = HEADER_GUARD_IFNDEF_PATTERN.search(text) is not None has_define = HEADER_GUARD_DEFINE_PATTERN.search(text) is not None @@ -487,6 +534,20 @@ def audit_style_guide_rules( "header guard should use `#ifndef` / `#define` plus the `EA_PRAGMA_ONCE_SUPPORTED` `#pragma once` block", ) ) + pragma_ifdef_match = EA_PRAGMA_IFDEF_PATTERN.search(text) + if pragma_ifdef_match is not None: + pragma_ifdef_line = text[: pragma_ifdef_match.start()].count("\n") + 1 + for idx, line in enumerate(text.splitlines(), 1): + if line.strip().startswith("#include ") and idx < pragma_ifdef_line: + findings.append( + Finding( + path, + idx, + "WARN", + "header include appears before the `EA_PRAGMA_ONCE_SUPPORTED` block; keep the guard / pragma block ahead of includes", + ) + ) + break return findings diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index 84f2f83d5..6be2b3d80 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -292,6 +292,14 @@ def choose_objdiff_row(unit_name: str, function_name: str, reloc_diffs: str = "n return matches[0] +def resolve_exact_function_name( + unit_name: str, function_name: str, reloc_diffs: str = "none" +) -> str: + return str( + choose_objdiff_row(unit_name, function_name, reloc_diffs=reloc_diffs)["name"] + ) + + def load_dwarf_report( unit_name: str, function_name: str, @@ -642,6 +650,9 @@ def command_function(args: argparse.Namespace) -> None: ensure_decomp_prereqs() print_section(f"Function Workflow: {args.function}") ensure_shared_unit_output(args.unit) + resolved_function_name = resolve_exact_function_name( + args.unit, args.function, reloc_diffs=args.reloc_diffs + ) cmd = python_tool("decomp-context.py", "-u", args.unit, "-f", args.function) if args.no_source: cmd.append("--no-source") @@ -661,9 +672,14 @@ def command_function(args: argparse.Namespace) -> None: print(flush=True) print( "Required completion check: python tools/decomp-workflow.py verify " - f"-u {shlex.quote(args.unit)} -f {shlex.quote(args.function)}", + f"-u {shlex.quote(args.unit)} -f {shlex.quote(resolved_function_name)}", flush=True, ) + if resolved_function_name != args.function: + print( + f"(Resolved exact function name for DWARF-safe follow-up: {resolved_function_name})", + flush=True, + ) def command_unit(args: argparse.Namespace) -> None: @@ -810,8 +826,11 @@ def command_dwarf(args: argparse.Namespace) -> None: print_section(f"DWARF Workflow: {args.unit} / {args.function}") if not args.rebuilt_dwarf_file: ensure_shared_unit_output(args.unit) + resolved_function_name = resolve_exact_function_name(args.unit, args.function) - cmd: List[str] = python_tool("dwarf-compare.py", "-u", args.unit, "-f", args.function) + cmd: List[str] = python_tool( + "dwarf-compare.py", "-u", args.unit, "-f", resolved_function_name + ) if args.summary: cmd.append("--summary") if args.json: @@ -833,18 +852,24 @@ def command_verify(args: argparse.Namespace) -> None: ensure_shared_unit_output(args.unit) objdiff_row = choose_objdiff_row(args.unit, args.function, reloc_diffs=args.reloc_diffs) - dwarf_report = load_dwarf_report( - args.unit, - args.function, - rebuilt_dwarf_file=args.rebuilt_dwarf_file, - ) + resolved_function_name = str(objdiff_row["name"]) + dwarf_load_error: Optional[str] = None + dwarf_report: Optional[Dict[str, Any]] = None + try: + dwarf_report = load_dwarf_report( + args.unit, + resolved_function_name, + rebuilt_dwarf_file=args.rebuilt_dwarf_file, + ) + except WorkflowError as e: + dwarf_load_error = str(e) objdiff_exact = ( objdiff_row["status"] == "match" and objdiff_row["match_percent"] is not None and float(objdiff_row["match_percent"]) >= 100.0 ) - dwarf_exact = bool(dwarf_report["normalized_exact_match"]) + dwarf_exact = bool(dwarf_report["normalized_exact_match"]) if dwarf_report else False overall_ok = objdiff_exact and dwarf_exact objdiff_percent = ( @@ -852,34 +877,56 @@ def command_verify(args: argparse.Namespace) -> None: if objdiff_row["match_percent"] is not None else "-" ) - dwarf_percent = f"{float(dwarf_report['match_percent']):.1f}%" + dwarf_percent = ( + f"{float(dwarf_report['match_percent']):.1f}%" if dwarf_report else "-" + ) print( f"objdiff: {'PASS' if objdiff_exact else 'FAIL'} | " f"{objdiff_percent} | status={objdiff_row['status']} | " f"unmatched~{objdiff_row['unmatched_bytes_est']}B" ) - print( - f"DWARF: {'PASS' if dwarf_exact else 'FAIL'} | " - f"{dwarf_percent} | normalized exact={'yes' if dwarf_exact else 'no'} | " - f"change groups={dwarf_report['changed_groups']}" - ) + if dwarf_report: + print( + f"DWARF: {'PASS' if dwarf_exact else 'FAIL'} | " + f"{dwarf_percent} | normalized exact={'yes' if dwarf_exact else 'no'} | " + f"change groups={dwarf_report['changed_groups']}" + ) + else: + print("DWARF: FAIL | unable to compare rebuilt vs original DWARF", flush=True) + if resolved_function_name != args.function: + print(f"Resolved DWARF symbol: {resolved_function_name}") print(f"Overall: {'PASS' if overall_ok else 'FAIL'}") print("Done means both objdiff and normalized DWARF are exact for the function.") if overall_ok: return + if dwarf_load_error: + print(flush=True) + print("DWARF compare could not complete:", flush=True) + print(dwarf_load_error, flush=True) + if ( + objdiff_row["status"] == "missing" + and "rebuilt DWARF: function" in dwarf_load_error + and "not found" in dwarf_load_error + ): + print( + "Hint: the rebuilt object does not contain this function yet. " + "Implement the function or fix its ownership/signature first, then rerun verify.", + flush=True, + ) + print(flush=True) print("Follow-up commands:", flush=True) print( f" python tools/decomp-workflow.py diff -u {shlex.quote(args.unit)} " - f"-d {shlex.quote(args.function)}", + f"-d {shlex.quote(resolved_function_name)}", flush=True, ) print( f" python tools/decomp-workflow.py dwarf -u {shlex.quote(args.unit)} " - f"-f {shlex.quote(args.function)}", + f"-f {shlex.quote(resolved_function_name)}", flush=True, ) raise WorkflowError( @@ -941,7 +988,12 @@ def build_parser() -> argparse.ArgumentParser: help="Run decomp-context.py for one function", ) function.add_argument("-u", "--unit", required=True, help="Translation unit name") - function.add_argument("-f", "--function", required=True, help="Function name to inspect") + function.add_argument( + "-f", + "--function", + required=True, + help="Function name to inspect (full name or a unique substring)", + ) function.add_argument( "--no-source", action="store_true", @@ -1086,7 +1138,12 @@ def build_parser() -> argparse.ArgumentParser: help="Compare original vs rebuilt DWARF for one function", ) dwarf.add_argument("-u", "--unit", required=True, help="Translation unit name") - dwarf.add_argument("-f", "--function", required=True, help="Function name to compare") + dwarf.add_argument( + "-f", + "--function", + required=True, + help="Function name to compare (full name or a unique substring)", + ) dwarf.add_argument( "--summary", action="store_true", @@ -1127,7 +1184,12 @@ def build_parser() -> argparse.ArgumentParser: help="Fail unless one function matches in both objdiff and DWARF", ) verify.add_argument("-u", "--unit", required=True, help="Translation unit name") - verify.add_argument("-f", "--function", required=True, help="Function name to verify") + verify.add_argument( + "-f", + "--function", + required=True, + help="Function name to verify (full name or a unique substring)", + ) verify.add_argument( "--reloc-diffs", choices=RELOC_DIFF_CHOICES, From 1b8ec13c1bf0453e18f17d12875cc9b1cd599d24 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:51:52 +0100 Subject: [PATCH 39/71] symlink --- .claude | 1 + 1 file changed, 1 insertion(+) create mode 120000 .claude diff --git a/.claude b/.claude new file mode 120000 index 000000000..c0ca46856 --- /dev/null +++ b/.claude @@ -0,0 +1 @@ +.agents \ No newline at end of file From 0bab6b8da55355215734602b5ec9057edf02f82f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 16:22:06 +0100 Subject: [PATCH 40/71] tooling: bootstrap PS2 and Xbox assets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/share_worktree_assets.py | 117 +++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/tools/share_worktree_assets.py b/tools/share_worktree_assets.py index 374d168e5..4bfdcf055 100644 --- a/tools/share_worktree_assets.py +++ b/tools/share_worktree_assets.py @@ -13,6 +13,8 @@ python tools/share_worktree_assets.py status --all python tools/share_worktree_assets.py link --all python tools/share_worktree_assets.py bootstrap + python tools/share_worktree_assets.py bootstrap --version EUROPEGERMILESTONE --xbox-xex /path/to/NfsMWEuropeGerMilestone.xex + python tools/share_worktree_assets.py bootstrap --version SLES-53558-A124 --ps2-toolchain-zip /path/to/PS2.zip """ import argparse @@ -21,6 +23,7 @@ import shutil import subprocess import sys +import zipfile from dataclasses import dataclass from typing import Dict, Iterable, List, Optional, Set @@ -39,11 +42,16 @@ class AssetSpec: FIXED_ASSETS = ( AssetSpec(os.path.join("orig", "GOWE69", "NFSMWRELEASE.ELF"), "file"), + AssetSpec( + os.path.join("orig", "EUROPEGERMILESTONE", "NfsMWEuropeGerMilestone.xex"), + "file", + ), AssetSpec(os.path.join("orig", "SLES-53558-A124", "NFS.ELF"), "file"), AssetSpec(os.path.join("orig", "SLES-53558-A124", "NFS.MAP"), "file"), AssetSpec(os.path.join("build", "tools"), "dir"), AssetSpec(os.path.join("build", "compilers"), "dir"), AssetSpec(os.path.join("build", "ppc_binutils"), "dir"), + AssetSpec(os.path.join("build", "mips_binutils"), "dir"), ) @@ -120,6 +128,85 @@ def ensure_parent(path: str) -> None: os.makedirs(parent, exist_ok=True) +def seed_shared_file(shared_path: str, source_path: str, description: str) -> None: + ensure_parent(shared_path) + if os.path.isfile(shared_path) and not os.path.islink(shared_path): + if not filecmp.cmp(shared_path, source_path, shallow=False): + raise RuntimeError( + f"Refusing to replace existing shared {description}: {shared_path}" + ) + return + if os.path.islink(shared_path): + if not same_symlink(shared_path, source_path): + raise RuntimeError( + f"Refusing to replace existing shared {description}: {shared_path}" + ) + os.unlink(shared_path) + elif lexists(shared_path): + raise RuntimeError( + f"Refusing to replace existing shared {description}: {shared_path}" + ) + shutil.copy2(source_path, shared_path) + + +def extract_zip_into(zip_path: str, output_dir: str) -> None: + os.makedirs(output_dir, exist_ok=True) + with zipfile.ZipFile(zip_path) as archive: + for member in archive.infolist(): + member_name = member.filename.rstrip("/") + if not member_name: + continue + + output_path = os.path.join(output_dir, *member_name.split("/")) + if member.is_dir(): + os.makedirs(output_path, exist_ok=True) + continue + + ensure_parent(output_path) + if os.path.exists(output_path): + continue + + with archive.open(member) as src, open(output_path, "wb") as dst: + shutil.copyfileobj(src, dst) + os.chmod(output_path, 0o755) + + +def seed_bootstrap_assets( + shared_root: str, xbox_xex: Optional[str], ps2_toolchain_zip: Optional[str] +) -> None: + if xbox_xex: + if not os.path.isfile(xbox_xex): + raise RuntimeError(f"Xbox XEX not found: {xbox_xex}") + seed_shared_file( + os.path.join( + shared_root, + "orig", + "EUROPEGERMILESTONE", + "NfsMWEuropeGerMilestone.xex", + ), + xbox_xex, + "Xbox XEX", + ) + + if ps2_toolchain_zip: + if not os.path.isfile(ps2_toolchain_zip): + raise RuntimeError(f"PS2 toolchain zip not found: {ps2_toolchain_zip}") + extract_zip_into(ps2_toolchain_zip, os.path.join(shared_root, "build", "compilers")) + expected_ee_gcc = os.path.join( + shared_root, + "build", + "compilers", + "PS2", + "ee-gcc2.9-991111", + "bin", + "ee-gcc.exe", + ) + if not os.path.isfile(expected_ee_gcc): + raise RuntimeError( + "PS2 toolchain zip did not produce build/compilers/PS2/ee-gcc2.9-991111/bin/ee-gcc.exe" + ) + + def merge_file(src: str, dst: str, relpath: str) -> None: ensure_parent(dst) if not os.path.exists(dst): @@ -342,18 +429,23 @@ def bootstrap_generated_files(worktree: str, version: str) -> None: objdiff_json = os.path.join(worktree, "objdiff.json") compile_commands = os.path.join(worktree, "compile_commands.json") config_target = os.path.join("build", version, "config.json") + configure_cmd = [sys.executable, "configure.py", "--version", version] - print(f"{worktree}: running configure.py") - run_command([sys.executable, "configure.py"], worktree, "configure.py") + print(f"{worktree}: running {' '.join(configure_cmd)}") + run_command(configure_cmd, worktree, "configure.py") if not os.path.isfile(build_ninja): raise RuntimeError(f"{worktree}: configure.py did not create build.ninja") - if not os.path.isfile(objdiff_json) or not os.path.isfile(compile_commands): + if ( + not os.path.isfile(config_target) + or not os.path.isfile(objdiff_json) + or not os.path.isfile(compile_commands) + ): print(f"{worktree}: generating {config_target} for local objdiff metadata") run_command(["ninja", config_target], worktree, f"ninja {config_target}") - print(f"{worktree}: rerunning configure.py") - run_command([sys.executable, "configure.py"], worktree, "configure.py") + print(f"{worktree}: rerunning {' '.join(configure_cmd)}") + run_command(configure_cmd, worktree, "configure.py") missing = [] if not os.path.isfile(objdiff_json): @@ -373,7 +465,10 @@ def bootstrap_worktrees( version: str, run_health: bool, smoke_build: Optional[str], + xbox_xex: Optional[str], + ps2_toolchain_zip: Optional[str], ) -> int: + seed_bootstrap_assets(shared_root, xbox_xex, ps2_toolchain_zip) link_assets(target_worktrees, seed_worktrees, shared_root) for worktree in target_worktrees: bootstrap_generated_files(worktree, version) @@ -418,6 +513,16 @@ def main() -> int: metavar="UNIT", help="Also run `decomp-workflow.py health --smoke-build UNIT` after bootstrap.", ) + parser.add_argument( + "--xbox-xex", + metavar="PATH", + help="Seed the shared Xbox XEX from a local file before linking/bootstrap.", + ) + parser.add_argument( + "--ps2-toolchain-zip", + metavar="PATH", + help="Extract a local PS2 EE-GCC zip into shared build/compilers before linking/bootstrap.", + ) args = parser.parse_args() common_dir = git_common_dir(root_dir) @@ -437,6 +542,8 @@ def main() -> int: args.version, args.health, args.smoke_build, + args.xbox_xex, + args.ps2_toolchain_zip, ) except RuntimeError as e: print(f"Error: {e}", file=sys.stderr) From 45403b80bb976309d78d85c7b32cb563dc3e9093 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 16:46:11 +0100 Subject: [PATCH 41/71] tooling: add cross-platform build matrix checker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 8 ++ tools/build_matrix.py | 307 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 tools/build_matrix.py diff --git a/AGENTS.md b/AGENTS.md index fc4f7af24..bf0cba804 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,16 @@ ninja all_source # build all objects ninja # build all objects, hash check and progress report ninja baseline # generates baseline report for regression checking ninja changes # check for regressions after code changes (empty = no regressions) +python tools/build_matrix.py # sequential full `ninja` across GC/Xbox/PS2, then restore GOWE69 +python tools/build_matrix.py --all-source # sequential compile-only smoke check across GC/Xbox/PS2 ``` +Use `python tools/build_matrix.py` when you want one command that verifies the current +worktree across all supported platforms. It runs `configure.py --version ...` and the +selected ninja target sequentially, writes per-platform logs under `build//logs/`, +prints failure tails with the exact failing command, and restores the worktree to +`GOWE69` by default when it finishes. + ## Project Layout ``` diff --git a/tools/build_matrix.py b/tools/build_matrix.py new file mode 100644 index 000000000..135bce18c --- /dev/null +++ b/tools/build_matrix.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 + +""" +Run sequential build checks across supported platforms. + +Examples: + python tools/build_matrix.py + python tools/build_matrix.py --version GOWE69 --version SLES-53558-A124 + python tools/build_matrix.py --all-source +""" + +import argparse +import os +import subprocess +import sys +import time +from dataclasses import dataclass +from typing import List, Optional, Sequence + + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +ROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, "..")) +DEFAULT_RESTORE_VERSION = "GOWE69" + + +@dataclass(frozen=True) +class PlatformCheck: + version: str + label: str + required_assets: Sequence[str] + + +@dataclass +class StepResult: + name: str + command: List[str] + returncode: int + elapsed: float + log_path: str + output: str + + @property + def ok(self) -> bool: + return self.returncode == 0 + + +@dataclass +class PlatformResult: + platform: PlatformCheck + configure: Optional[StepResult] = None + build: Optional[StepResult] = None + preflight_error: Optional[str] = None + + @property + def ok(self) -> bool: + return ( + self.preflight_error is None + and self.configure is not None + and self.configure.ok + and self.build is not None + and self.build.ok + ) + + +PLATFORMS = ( + PlatformCheck( + version="GOWE69", + label="GameCube", + required_assets=("orig/GOWE69/NFSMWRELEASE.ELF",), + ), + PlatformCheck( + version="EUROPEGERMILESTONE", + label="Xbox 360", + required_assets=("orig/EUROPEGERMILESTONE/NfsMWEuropeGerMilestone.xex",), + ), + PlatformCheck( + version="SLES-53558-A124", + label="PS2", + required_assets=("orig/SLES-53558-A124/NFS.ELF",), + ), +) + +PLATFORM_BY_VERSION = {platform.version: platform for platform in PLATFORMS} + + +def print_section(title: str) -> None: + print(f"\n== {title} ==", flush=True) + + +def tail_lines(text: str, count: int) -> str: + lines = text.rstrip().splitlines() + if len(lines) <= count: + return "\n".join(lines) + return "\n".join(lines[-count:]) + + +def run_logged(command: List[str], log_path: str) -> StepResult: + start = time.monotonic() + try: + completed = subprocess.run( + command, + cwd=ROOT_DIR, + capture_output=True, + text=True, + errors="replace", + ) + output = completed.stdout + if completed.stderr: + if output and not output.endswith("\n"): + output += "\n" + output += completed.stderr + returncode = completed.returncode + except OSError as exc: + output = str(exc) + returncode = 127 + elapsed = time.monotonic() - start + + os.makedirs(os.path.dirname(log_path), exist_ok=True) + with open(log_path, "w", encoding="utf-8") as log_file: + log_file.write(output) + + return StepResult( + name=os.path.basename(log_path), + command=command, + returncode=returncode, + elapsed=elapsed, + log_path=log_path, + output=output, + ) + + +def missing_assets(platform: PlatformCheck) -> List[str]: + missing = [] + for rel_path in platform.required_assets: + abs_path = os.path.join(ROOT_DIR, rel_path) + if not os.path.exists(abs_path): + missing.append(rel_path) + return missing + + +def describe_failure(step: StepResult, tail_count: int) -> None: + print(f"FAIL {step.name}: exit {step.returncode} in {step.elapsed:.2f}s") + print(f"Command: {' '.join(step.command)}") + print(f"Log: {step.log_path}") + if step.output.strip(): + print("--- output tail ---") + print(tail_lines(step.output, tail_count)) + + +def run_platform( + platform: PlatformCheck, build_target: Optional[str], jobs: int, tail_count: int +) -> PlatformResult: + result = PlatformResult(platform=platform) + logs_dir = os.path.join(ROOT_DIR, "build", platform.version, "logs") + + print_section(f"{platform.label} ({platform.version})") + + missing = missing_assets(platform) + if missing: + result.preflight_error = ( + "Missing required assets: " + + ", ".join(missing) + + " (hint: seed shared assets or run worktree bootstrap first)" + ) + print(f"FAIL preflight: {result.preflight_error}") + return result + + configure_cmd = [sys.executable, "configure.py", "--version", platform.version] + configure_log = os.path.join(logs_dir, "build-matrix-configure.log") + print(f"RUN configure: {' '.join(configure_cmd)}") + result.configure = run_logged(configure_cmd, configure_log) + if result.configure.ok: + print(f"OK configure: {result.configure.elapsed:.2f}s ({configure_log})") + else: + describe_failure(result.configure, tail_count) + return result + + build_cmd = ["ninja", "-j", str(jobs)] + if build_target is not None: + build_cmd.append(build_target) + build_name = build_target or "default" + build_log = os.path.join(logs_dir, f"build-matrix-{build_name}.log") + print(f"RUN build: {' '.join(build_cmd)}") + result.build = run_logged(build_cmd, build_log) + if result.build.ok: + print(f"OK build: {result.build.elapsed:.2f}s ({build_log})") + else: + describe_failure(result.build, tail_count) + + return result + + +def restore_version(version: str, tail_count: int) -> bool: + print_section(f"Restore {version}") + log_path = os.path.join(ROOT_DIR, "build", version, "logs", "build-matrix-restore.log") + step = run_logged([sys.executable, "configure.py", "--version", version], log_path) + if step.ok: + print(f"OK restore: {step.elapsed:.2f}s ({log_path})") + return True + + describe_failure(step, tail_count) + return False + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Check sequential builds across all supported platforms." + ) + parser.add_argument( + "--version", + dest="versions", + action="append", + choices=sorted(PLATFORM_BY_VERSION.keys()), + help="Limit the run to one or more versions (default: all platforms).", + ) + parser.add_argument( + "--all-source", + action="store_true", + help="Run `ninja all_source` instead of the default full `ninja`.", + ) + parser.add_argument( + "--jobs", + type=int, + default=1, + help="Parallelism passed to ninja (default: 1).", + ) + parser.add_argument( + "--tail", + type=int, + default=40, + help="How many output lines to print when a step fails (default: 40).", + ) + parser.add_argument( + "--restore-version", + default=DEFAULT_RESTORE_VERSION, + choices=sorted(PLATFORM_BY_VERSION.keys()), + help=f"Version to restore at the end (default: {DEFAULT_RESTORE_VERSION}).", + ) + parser.add_argument( + "--no-restore", + action="store_true", + help="Leave the worktree configured for the last checked version.", + ) + return parser.parse_args() + + +def print_summary( + results: Sequence[PlatformResult], restore_version_name: str, restore_ok: Optional[bool] +) -> None: + print_section("Summary") + for result in results: + if result.preflight_error is not None: + print(f"FAIL {result.platform.version}: {result.preflight_error}") + continue + if result.configure is None or not result.configure.ok: + assert result.configure is not None + print( + f"FAIL {result.platform.version}: configure exit {result.configure.returncode} " + f"({result.configure.elapsed:.2f}s)" + ) + continue + if result.build is None or not result.build.ok: + assert result.build is not None + print( + f"FAIL {result.platform.version}: build exit {result.build.returncode} " + f"({result.build.elapsed:.2f}s)" + ) + continue + total = result.configure.elapsed + result.build.elapsed + print(f"OK {result.platform.version}: {total:.2f}s") + + if restore_ok is not None: + status = "OK" if restore_ok else "FAIL" + print(f"{status:4} restore: {restore_version_name}") + + +args = parse_args() + + +def main() -> int: + selected_versions = args.versions or [platform.version for platform in PLATFORMS] + platforms = [PLATFORM_BY_VERSION[version] for version in selected_versions] + build_target = "all_source" if args.all_source else None + results: List[PlatformResult] = [] + restore_ok: Optional[bool] = None + + print(f"Root: {ROOT_DIR}") + print(f"Build target: {build_target or 'ninja default'}") + + try: + for platform in platforms: + results.append(run_platform(platform, build_target, args.jobs, args.tail)) + finally: + if not args.no_restore: + restore_ok = restore_version(args.restore_version, args.tail) + + print_summary(results, args.restore_version, restore_ok) + + if restore_ok is False: + return 1 + if any(not result.ok for result in results): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 6b35f229c3bf26032fbedc9862e246abcec93fa5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 16:51:05 +0100 Subject: [PATCH 42/71] tooling: extend health checks for Xbox and PS2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/decomp-workflow.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index 6be2b3d80..a00215d82 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -56,6 +56,9 @@ PS2_SYMBOLS = os.path.join(ROOT_DIR, "config", "SLES-53558-A124", "symbols.txt") GC_DWARF = os.path.join(ROOT_DIR, "symbols", "Dwarf") DEBUG_LINES = os.path.join(ROOT_DIR, "symbols", "debug_lines.txt") +X360_COMPILER_DIR = os.path.join(ROOT_DIR, "build", "compilers", "X360", "14.00.2110") +PS2_COMPILER_DIR = os.path.join(ROOT_DIR, "build", "compilers", "PS2", "ee-gcc2.9-991111") +MIPS_BINUTILS_DIR = os.path.join(ROOT_DIR, "build", "mips_binutils") DEFAULT_SMOKE_UNIT = "main/Speed/Indep/SourceLists/zCamera" DEBUG_SYMBOL_PROBE_MANGLED = "UpdateAll__6Cameraf" @@ -70,10 +73,32 @@ SHARED_ASSET_REQUIREMENTS = [ (os.path.join("build", "tools"), "downloaded tooling"), (os.path.join("orig", "GOWE69", "NFSMWRELEASE.ELF"), "GameCube original ELF"), + ( + os.path.join("orig", "EUROPEGERMILESTONE", "NfsMWEuropeGerMilestone.xex"), + "Xbox original XEX", + ), (os.path.join("orig", "SLES-53558-A124", "NFS.ELF"), "PS2 original ELF"), (os.path.join("symbols", "Dwarf"), "DWARF dump"), ] +PLATFORM_BUILD_REQUIREMENTS = [ + ( + "x360-compiler", + X360_COMPILER_DIR, + "missing (seed build/compilers in this worktree for Xbox builds)", + ), + ( + "ps2-compiler", + PS2_COMPILER_DIR, + "missing (seed build/compilers in this worktree for PS2 builds)", + ), + ( + "ps2-binutils", + MIPS_BINUTILS_DIR, + "missing (seed build/mips_binutils in this worktree for PS2 builds)", + ), +] + class WorkflowError(RuntimeError): pass @@ -403,6 +428,14 @@ def build_shared_unit_cached(unit: str) -> str: except WorkflowError as e: report(False, "ghidra", str(e)) + print_section("Platform Build Inputs") + for label, abs_path, missing_detail in PLATFORM_BUILD_REQUIREMENTS: + report( + os.path.exists(abs_path), + label, + describe_path(abs_path) if os.path.exists(abs_path) else missing_detail, + ) + print_section("Debug Symbol Checks") try: gc_addr = lookup_symbol_address(GC_SYMBOLS, DEBUG_SYMBOL_PROBE_MANGLED) @@ -944,7 +977,7 @@ def build_parser() -> argparse.ArgumentParser: health = subparsers.add_parser( "health", - help="Check whether the current worktree is ready for GC and PS2 decomp work", + help="Check whether the current worktree is ready for GC, Xbox, and PS2 work", ) health.add_argument( "--full", From c9b94f54c6a3794b273a909b5dd3c14664783ee3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 18:50:31 +0100 Subject: [PATCH 43/71] disallow sub agents --- AGENTS.md | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf0cba804..c55e27cb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,16 +39,7 @@ objdiff.json Generated build/diff configuration ## Sub-Agent Usage -Sub-agents are allowed only for **read-only exploration** tasks such as: - -- searching the codebase for symbols, call sites, or include relationships -- inspecting decomp output, assembly, DWARF, PS2 dumps, or line mappings -- gathering context from Ghidra, `tools/decomp-workflow.py`, `lookup.py`, `decomp-diff.py`, or similar tools -- summarizing findings that help the main worker decide what to change - -Sub-agents must **not** write or edit code files, headers, configs, or other repository files. -All persistent file changes, decomp implementations, scaffolding, and follow-up fixes must be -done by the main worker after reviewing the read-only findings. +Sub-agents are **strictly prohibited**. Do not use sub-agents for any tasks (whether read-only exploration or editing). All work must be performed by the main worker directly. ## Forbidden Changes @@ -382,28 +373,10 @@ Examples: Do not batch up multiple percentage milestones into one commit — commit as each improvement lands. -## Parallel Sub-Agent Matching - -When working on a translation unit with multiple non-matching functions, use sub-agents selectively for **read-only exploration** around individual functions. Each sub-agent should focus on **exactly one function** — do not assign a sub-agent more than one function at a time. - -**Limit: never run more than 5 sub-agents concurrently.** Spawning too many at once causes resource contention and makes it harder to reason about progress. - -Guidelines: - -- Prefer solving difficult matching work in the main worker. Use sub-agents to inspect one function's context, diff, DWARF, or related call paths without editing files. -- Spawn a sub-agent per function only when the functions are independent (no shared edits to the same source lines). -- Sub-agents stay read-only. Let them inspect existing diff/context output rather than compiling or rebuilding. -- Do not sit idle waiting for sub-agents to finish. Continue with other independent investigation while they run. -- After a useful result lands and you make a real improvement, check the updated match percentage and commit if it improved. - ## Matching Philosophy You should take the Ghidra decompiler output for the initial translation step, get it to compile, make sure that the dwarf of the function matches and only then look for binary matching problems in the assembly. Be aware Ghidra usually gets the order of branches incorrect in if statements (it inverts the logic and the two bodies are swapped), this needs to be fixed to achieve bytematching status. -You may use sub-agents to gather read-only context during this process, but they must not -edit files. Treat their output as analysis input for the main worker, not as a path to -delegate source changes. - A function is only done when both objdiff and normalized DWARF are exact. Treat a 100% instruction match with a DWARF mismatch as unfinished work, not a near-complete result. From 9eef732bc56396b4dacb11bf303dc10f253471e8 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:55:08 +0100 Subject: [PATCH 44/71] dwarf TU scan tool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 90.6%: improve decomp-workflow::dwarf-scan signature filter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/decomp-workflow.py | 411 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index a00215d82..78e7fee04 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -16,12 +16,15 @@ python tools/decomp-workflow.py function -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll --no-source python tools/decomp-workflow.py diff -u main/Speed/Indep/SourceLists/zCamera -d UpdateAll --reloc-diffs all python tools/decomp-workflow.py dwarf -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll + python tools/decomp-workflow.py dwarf-scan -u main/Speed/Indep/SourceLists/zCamera + python tools/decomp-workflow.py dwarf-scan -u main/Speed/Indep/SourceLists/zCamera --objdiff-status match python tools/decomp-workflow.py dwarf -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' --full-diff python tools/decomp-workflow.py verify -u main/Speed/Indep/SourceLists/zCamera -f UpdateAll python tools/decomp-workflow.py unit -u main/Speed/Indep/SourceLists/zCamera """ import argparse +import difflib import json import re import os @@ -45,6 +48,8 @@ make_abs, run_objdiff_json, ) +from lookup import _candidate_func_names, _sig_contains_name, read_text, split_functions +from split_dwarf_info import apply_umath_fixups SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -65,10 +70,12 @@ DEBUG_SYMBOL_PROBE_DEMANGLED = "Camera::UpdateAll(float)" DEBUG_SYMBOL_PROBE_GC_ADDR = "0x80065A84" REBUILT_DEBUG_LINE_RE = re.compile(r"^\s*([0-9A-Fa-f]+)\s*:") +DWARF_HEX_RE = re.compile(r"0x[0-9A-Fa-f]+") LOW_MATCH_PRIORITY_THRESHOLD = 60.0 VERY_LOW_MATCH_PRIORITY_THRESHOLD = 40.0 HIGH_MATCH_CLEANUP_THRESHOLD = 85.0 VERY_HIGH_MATCH_CLEANUP_THRESHOLD = 95.0 +FunctionBlock = Tuple[str, str, str, str] SHARED_ASSET_REQUIREMENTS = [ (os.path.join("build", "tools"), "downloaded tooling"), @@ -340,6 +347,355 @@ def load_dwarf_report( raise WorkflowError(f"dwarf-compare.py returned invalid JSON: {e}") +def load_dwarf_blocks( + path: str, folder_mode: bool, apply_split_fixups_in_ram: bool = False +) -> List[FunctionBlock]: + if folder_mode: + text = read_text(os.path.join(path, "functions.nothpp")) + else: + text = read_text(path) + if apply_split_fixups_in_ram: + text = apply_umath_fixups(text) + return split_functions(text) + + +def find_dwarf_function_blocks( + funcs: Sequence[FunctionBlock], query: str +) -> List[FunctionBlock]: + candidates = _candidate_func_names(query) + exact_matches: List[FunctionBlock] = [] + fuzzy_matches: List[FunctionBlock] = [] + + for func in funcs: + sig_line = func[2] + if sig_line == query: + exact_matches.append(func) + elif any(_sig_contains_name(sig_line, candidate) for candidate in candidates): + fuzzy_matches.append(func) + + if exact_matches: + return exact_matches + return fuzzy_matches + + +def choose_dwarf_function_block( + funcs: Sequence[FunctionBlock], query: str, label: str +) -> FunctionBlock: + matches = find_dwarf_function_blocks(funcs, query) + if not matches: + raise WorkflowError(f"{label}: function '{query}' not found.") + if len(matches) > 1: + preview = "\n".join(f" - {match[2]}" for match in matches[:8]) + extra = "" + if len(matches) > 8: + extra = f"\n ... {len(matches) - 8} more" + raise WorkflowError( + f"{label}: function query '{query}' matched multiple DWARF blocks.\n" + f"Use a more specific function name.\n{preview}{extra}" + ) + return matches[0] + + +def normalize_dwarf_line(line: str) -> str: + stripped = line.rstrip("\n").rstrip() + if stripped.startswith("// Range:"): + return "// Range: " + return DWARF_HEX_RE.sub("0xADDR", stripped) + + +def normalize_dwarf_block(block: str) -> List[str]: + return [normalize_dwarf_line(line) for line in block.splitlines()] + + +def count_dwarf_opcodes( + opcodes: Sequence[Tuple[str, int, int, int, int]] +) -> Dict[str, int]: + matching = 0 + original_only = 0 + rebuilt_only = 0 + changed_groups = 0 + for tag, i1, i2, j1, j2 in opcodes: + if tag == "equal": + matching += i2 - i1 + continue + changed_groups += 1 + if tag in ("replace", "delete"): + original_only += i2 - i1 + if tag in ("replace", "insert"): + rebuilt_only += j2 - j1 + return { + "matching_lines": matching, + "original_only_lines": original_only, + "rebuilt_only_lines": rebuilt_only, + "changed_groups": changed_groups, + } + + +def build_dwarf_scan_row( + row: Dict[str, Any], + original_funcs: Sequence[FunctionBlock], + rebuilt_funcs: Sequence[FunctionBlock], +) -> Dict[str, Any]: + function_name = str(row["name"]) + result: Dict[str, Any] = { + "function": function_name, + "symbol_name": row["symbol_name"], + "objdiff_status": row["status"], + "objdiff_match_percent": row["match_percent"], + "unmatched_bytes_est": row["unmatched_bytes_est"], + "size": row["size"], + } + + try: + original_block = choose_dwarf_function_block( + original_funcs, function_name, "original DWARF" + ) + rebuilt_block = choose_dwarf_function_block( + rebuilt_funcs, function_name, "rebuilt DWARF" + ) + original_lines = normalize_dwarf_block(original_block[3]) + rebuilt_lines = normalize_dwarf_block(rebuilt_block[3]) + matcher = difflib.SequenceMatcher(a=original_lines, b=rebuilt_lines) + counts = count_dwarf_opcodes(matcher.get_opcodes()) + total_lines = max(len(original_lines), len(rebuilt_lines), 1) + result.update( + { + "dwarf_status": "exact" + if original_lines == rebuilt_lines + else "mismatch", + "dwarf_match_percent": 100.0 * counts["matching_lines"] / total_lines, + "changed_groups": counts["changed_groups"], + "matching_lines": counts["matching_lines"], + "total_lines": total_lines, + "original_line_count": len(original_lines), + "rebuilt_line_count": len(rebuilt_lines), + "signature_match": normalize_dwarf_line(original_block[2]) + == normalize_dwarf_line(rebuilt_block[2]), + } + ) + except WorkflowError as e: + result.update( + { + "dwarf_status": "error", + "dwarf_match_percent": None, + "changed_groups": None, + "matching_lines": None, + "total_lines": None, + "original_line_count": None, + "rebuilt_line_count": None, + "signature_match": None, + "error": str(e), + } + ) + return result + + +def filter_dwarf_scan_rows( + rows: Sequence[Dict[str, Any]], dwarf_status: str +) -> List[Dict[str, Any]]: + if dwarf_status == "all": + return list(rows) + if dwarf_status == "problem": + return [row for row in rows if row["dwarf_status"] in ("mismatch", "error")] + return [row for row in rows if row["dwarf_status"] == dwarf_status] + + +def filter_dwarf_signature_rows( + rows: Sequence[Dict[str, Any]], signature_status: str +) -> List[Dict[str, Any]]: + if signature_status == "all": + return list(rows) + want_match = signature_status == "match" + return [ + row + for row in rows + if row.get("signature_match") is not None + and bool(row["signature_match"]) == want_match + ] + + +def sort_dwarf_scan_rows(rows: List[Dict[str, Any]]) -> None: + status_rank = {"error": 0, "mismatch": 1, "exact": 2} + rows.sort( + key=lambda row: ( + status_rank.get(str(row["dwarf_status"]), 3), + row["dwarf_match_percent"] + if row["dwarf_match_percent"] is not None + else -1.0, + 0 + if row.get("signature_match") is True + else 1 + if row.get("signature_match") is False + else 2, + -(row["changed_groups"] or 0), + -(row["unmatched_bytes_est"] or 0), + row["objdiff_match_percent"] + if row["objdiff_match_percent"] is not None + else -1.0, + row["function"].lower(), + ) + ) + + +def command_dwarf_scan(args: argparse.Namespace) -> None: + ensure_decomp_prereqs() + if not args.json: + print_section(f"DWARF Scan: {args.unit}") + ensure_shared_unit_output(args.unit) + + rebuilt_dwarf_path = ( + os.path.abspath(args.rebuilt_dwarf_file) if args.rebuilt_dwarf_file else None + ) + cleanup_rebuilt_dwarf = False + try: + if not rebuilt_dwarf_path: + rebuilt_dwarf_path = dtk_dwarf_dump(get_unit_build_output(args.unit)) + cleanup_rebuilt_dwarf = True + + data = run_objdiff_json( + OBJDIFF_CLI, + args.unit, + reloc_diffs=args.reloc_diffs, + root_dir=ROOT_DIR, + ) + rows = [ + row + for row in build_objdiff_symbol_rows(data) + if row["type"] == "function" and row["side"] == "left" + ] + if args.objdiff_status != "all": + rows = [row for row in rows if row["status"] == args.objdiff_status] + if args.search: + rows = [ + row + for row in rows + if fuzzy_match(args.search, row["name"]) + or fuzzy_match(args.search, row["symbol_name"]) + ] + if not rows: + raise WorkflowError("No functions match the given filters.") + + original_funcs = load_dwarf_blocks(GC_DWARF, folder_mode=True) + rebuilt_funcs = load_dwarf_blocks( + rebuilt_dwarf_path, folder_mode=False, apply_split_fixups_in_ram=True + ) + scan_rows = [ + build_dwarf_scan_row(row, original_funcs, rebuilt_funcs) for row in rows + ] + + summary = { + "scanned_functions": len(scan_rows), + "exact_functions": sum( + 1 for row in scan_rows if row["dwarf_status"] == "exact" + ), + "mismatch_functions": sum( + 1 for row in scan_rows if row["dwarf_status"] == "mismatch" + ), + "error_functions": sum( + 1 for row in scan_rows if row["dwarf_status"] == "error" + ), + "byte_matched_dwarf_problems": sum( + 1 + for row in scan_rows + if row["objdiff_status"] == "match" + and row["dwarf_status"] in ("mismatch", "error") + ), + "signature_mismatch_functions": sum( + 1 for row in scan_rows if row.get("signature_match") is False + ), + } + + filtered_rows = filter_dwarf_scan_rows(scan_rows, args.dwarf_status) + filtered_rows = filter_dwarf_signature_rows( + filtered_rows, args.signature_status + ) + sort_dwarf_scan_rows(filtered_rows) + if args.limit is not None: + filtered_rows = filtered_rows[: args.limit] + + if args.json: + print( + json.dumps( + { + "unit": args.unit, + "summary": summary, + "rows": filtered_rows, + }, + indent=2, + ) + ) + return + + print( + f"Scanned {summary['scanned_functions']} function(s): " + f"{summary['exact_functions']} exact, " + f"{summary['mismatch_functions']} mismatched, " + f"{summary['error_functions']} errors." + ) + print( + "Byte-matched but DWARF-problem functions: " + f"{summary['byte_matched_dwarf_problems']}" + ) + print( + "Signature-mismatch functions: " + f"{summary['signature_mismatch_functions']}" + ) + + if not filtered_rows: + print("No functions match the given filters.") + return + + print() + print( + f"{'DSTAT':<8} {'DWARF':>7} {'SIG':>3} {'CHG':>4} {'OBJ':>7} {'OSTAT':<10} {'UNM':>6} FUNCTION" + ) + print("-" * 120) + for row in filtered_rows: + dwarf_percent = ( + f"{row['dwarf_match_percent']:.1f}%" + if row["dwarf_match_percent"] is not None + else "ERR" + ) + objdiff_percent = ( + f"{row['objdiff_match_percent']:.1f}%" + if row["objdiff_match_percent"] is not None + else "-" + ) + changed_groups = ( + str(row["changed_groups"]) if row["changed_groups"] is not None else "-" + ) + signature_state = ( + "yes" + if row.get("signature_match") is True + else "no" + if row.get("signature_match") is False + else "-" + ) + print( + f"{row['dwarf_status']:<8} {dwarf_percent:>7} {signature_state:>3} {changed_groups:>4} " + f"{objdiff_percent:>7} {row['objdiff_status']:<10} " + f"{row['unmatched_bytes_est']:>5}B {row['function']}" + ) + if args.show_errors and row.get("error"): + first_line = str(row["error"]).splitlines()[0] + print(f" error: {first_line}") + + print() + print( + "Tip: focus matched-byte functions first with " + "`python tools/decomp-workflow.py dwarf-scan " + f"-u {shlex.quote(args.unit)} --objdiff-status match`" + ) + if summary["signature_mismatch_functions"]: + print( + "Tip: add `--signature-status match` to focus body/local DWARF mismatches " + "instead of signature-only trouble." + ) + finally: + if cleanup_rebuilt_dwarf: + maybe_remove(rebuilt_dwarf_path) + + def lookup_symbol_address(symbols_file: str, mangled_name: str) -> Optional[str]: if not os.path.exists(symbols_file): return None @@ -1212,6 +1568,61 @@ def build_parser() -> argparse.ArgumentParser: ) dwarf.set_defaults(func=command_dwarf) + dwarf_scan = subparsers.add_parser( + "dwarf-scan", + help="Scan one translation unit and rank per-function DWARF problem areas", + ) + dwarf_scan.add_argument("-u", "--unit", required=True, help="Translation unit name") + dwarf_scan.add_argument( + "--search", + help="Only include functions whose name or symbol contains this text", + ) + dwarf_scan.add_argument( + "--objdiff-status", + choices=["all", "match", "nonmatching", "missing"], + default="all", + help="Filter functions by objdiff status before scanning (default: all)", + ) + dwarf_scan.add_argument( + "--dwarf-status", + choices=["all", "problem", "exact", "mismatch", "error"], + default="problem", + help="Filter scan results by DWARF outcome after scanning (default: problem)", + ) + dwarf_scan.add_argument( + "--signature-status", + choices=["all", "match", "mismatch"], + default="all", + help="Filter scan results by whether the DWARF signature already matches (default: all)", + ) + dwarf_scan.add_argument( + "--limit", + type=int, + default=20, + help="Maximum rows to print after sorting (default: 20)", + ) + dwarf_scan.add_argument( + "--json", + action="store_true", + help="Print the scan summary and rows as JSON", + ) + dwarf_scan.add_argument( + "--show-errors", + action="store_true", + help="Print one-line error details under rows that could not be compared", + ) + dwarf_scan.add_argument( + "--reloc-diffs", + choices=RELOC_DIFF_CHOICES, + default="none", + help="Pass through objdiff relocation diff mode when loading unit symbols", + ) + dwarf_scan.add_argument( + "--rebuilt-dwarf-file", + help="Use an existing rebuilt DWARF dump instead of dumping the unit object", + ) + dwarf_scan.set_defaults(func=command_dwarf_scan) + verify = subparsers.add_parser( "verify", help="Fail unless one function matches in both objdiff and DWARF", From eae477086f4bb4df1b004221b329f626c8caa8a4 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 16:25:29 +0100 Subject: [PATCH 45/71] mac safe --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 547d049c6..1e78f634d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ .idea/ .vs/ +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + # Caches __pycache__ .mypy_cache From c4256f442b4f5a27e18908b271caca322d4d0b8f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sun, 22 Mar 2026 13:14:36 +0100 Subject: [PATCH 46/71] improve matching guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c55e27cb4..3e97d44b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -462,6 +462,20 @@ register assignments but does NOT affect integer register assignments (and vice Every local that is NOT in the DWARF is a spurious temporary — remove it. - Every local that IS in the DWARF must exist in the source, even if you don't use the name. Name it exactly as the DWARF shows. +- When objdiff is already exact but a local only differs by lexical scope, try an equivalent + loop form that keeps the temporary inside the same block as the original DWARF. In practice, + changing a `for (...; ...; x = next)` into a `while (...) { T *next = ...; ...; x = next; }` + can fix DWARF-only scope mismatches without changing codegen. + +### Slot-pooled delete paths + +- If a recovered local/project type participates in `delete` paths or container/list teardown, + check whether the original type exposed inline `operator new` / `operator delete`. Missing + slot-pool-backed operators often makes GCC emit `__builtin_delete` instead of the original + allocator/free path and can also move destructor/delete DWARF ownership out of the TU. +- This applies even when the TU mostly allocates the type manually through `bOMalloc` or a + pool helper. Restoring the inline operators can still be necessary so `delete` expressions + and synthesized cleanup paths match the original code and DWARF. ### Virtual vs direct calls From 07af42d6ecdc054197ff4ed791c93e8e6d1a99a2 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Mon, 23 Mar 2026 12:13:44 +0100 Subject: [PATCH 47/71] review adjustments --- .github/skills/code_style/SKILL.md | 3 +++ .github/skills/execute/SKILL.md | 7 +++++++ .github/skills/implement/SKILL.md | 4 ++++ .github/skills/scaffold/SKILL.md | 14 +++++++++++++- AGENTS.md | 13 +++++++++++-- 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index d5b2cbbb3..15f4779af 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -122,6 +122,7 @@ Foo::Foo() - Use the repo's header guard form when writing headers: `#ifndef` / `#define` plus the `#ifdef EA_PRAGMA_ONCE_SUPPORTED` / `#pragma once` block. - Keep member layout comments aligned and intact in decomp headers. +- When writing a recovered layout, start from a pasted GC DWARF dump instead of hand-reconstructing a cleaner version. Treat the dump as source-of-truth data entry, then make only small verified fixes from PS2 or existing headers. - Preserve the original `class` / `struct` kind from existing headers or Dwarf / PS2 evidence; do not treat it as a cosmetic style choice. - Treat header declarations as the repo source of truth. If the repo only has local `.cpp` partial declarations, verify the kind with the PS2 dump instead of copying them blindly. - Even forward declarations and local partial declarations should use the accurate keyword when known. @@ -129,6 +130,7 @@ Foo::Foo() - When a recovered type is a `class`, keep explicit access sections and put the method/accessor block before the member layout block unless existing repo evidence shows otherwise. - Preserve the member naming style that DWARF shows. Some types use `mMember`, others use `m_member`; do not normalize them. - Preserve recovered member names, types, order, and offset comments. Do not invent placeholder members named `pad`, `unk`, `unknown`, or `field_XXXX` for game code just to make a layout compile. +- Preserve the dumped declaration order too. Do not regroup methods, helpers, enums, or fields for readability unless an existing repo header or PS2 evidence proves the original order differs. - If a member is genuinely unknown, stop and verify it with `find-symbol.py`, GC Dwarf, and PS2 data. If the layout is still incomplete, add a short TODO above the type instead of burying uncertainty in fake member names. - Add offset / size comments when you are writing recovered type layouts from DWARF. - In recovered layouts, prefer explicit-width aliases such as `uint8` / `uint16` when the field width is known. Use plain `char` for text / byte buffers and `signed char` when the field is a signed 8-bit counter. @@ -200,6 +202,7 @@ Keep the cleanup only if the build succeeds and the relevant match status is unc - Header prologues should keep the `EA_PRAGMA_ONCE_SUPPORTED` block ahead of includes, not after them. - Bare `#if MACRO` presence checks are review bait; use `#ifdef` / `#ifndef` unless you are intentionally testing a numeric config value. - Reviewed recovered headers tend to keep total-size comments above the type, methods before fields, explicit access sections, and fixed-width aliases for width-known narrow integer members. +- Recent `zMisc` review cleanup also showed that hand-reconstructed structs and reordered declarations create avoidable churn; copy recovered layouts from DWARF into the owner header first and keep the dumped order unless PS2/header evidence proves a correction. - Reviewed fixups also remove stale bare recovery markers or replace them with context, and prefer existing list/node helpers over hand-written pointer/link rewiring. - Some reviewed fixups improved readability without losing match by replacing opaque range-check arithmetic with explicit bounds and by moving repeated pointer/boundary math behind short named helpers. - Other recurring review churn came from plain-`int` address helpers, stray local `.cpp` prototypes for shared functions, and integer-coded parser states where named enums were clearer but still matched. diff --git a/.github/skills/execute/SKILL.md b/.github/skills/execute/SKILL.md index 5ba0bd156..ea6fef41b 100644 --- a/.github/skills/execute/SKILL.md +++ b/.github/skills/execute/SKILL.md @@ -91,6 +91,10 @@ definition does not yet exist in the project, follow the scaffold workflow in `.github/skills/scaffold/SKILL.md` to create the needed header/source definitions before moving on. +Treat recovered types here as copied reference data, not as hand-designed headers. Copy +the GC DWARF type body into the canonical owner header first and preserve its declaration +order unless PS2 or existing repo-header evidence proves a specific correction. + ## Phase 3: Implement Functions ### 3a. Get the updated function list @@ -116,6 +120,9 @@ For each missing or nonmatching function, follow the implementation workflow in - **One at a time.** Keep the tree in a coherent state as you work through the list. - **Balance new vs fixing.** Don't get stuck on one stubborn function — sometimes implementing the next function reveals patterns that make the previous one click. +- **Recovered types are not freeform.** If a function forces you to add or fix a type, + copy the DWARF layout into the owner header first. Do not sketch structs/classes from + use sites or reorder declarations just to make the header look nicer. - **Mismatch triage:** - `@stringBase0` offset mismatches often resolve as more string literals are added - If you need to inspect the original string or rodata at a virtual address, use `python tools/elf_lookup.py 0xADDR` diff --git a/.github/skills/implement/SKILL.md b/.github/skills/implement/SKILL.md index cbd94da78..8915ac246 100644 --- a/.github/skills/implement/SKILL.md +++ b/.github/skills/implement/SKILL.md @@ -90,6 +90,8 @@ Reference the skill for the usage. It gives info based on the virtual address of - If a repo header already exists for the type, include that header instead of introducing a local forward declaration. - Preserve the original `class` vs `struct` kind. If the existing header is missing or incomplete, verify the type kind from GC Dwarf and PS2 info before writing a local declaration. - Preserve real member names and field types too. Do not introduce `pad`, `unk`, or `field_XXXX` members as placeholders for guessed layout; verify the member list from GC Dwarf / PS2 data and leave a TODO when something is still uncertain. +- When a type is missing or incomplete, dump the full class/struct body from GC DWARF and paste that as the starting point. Do not reconstruct the layout from one function's field accesses or from guessed semantics. +- Preserve the dumped declaration order as well as the member order. Do not re-sort methods, group fields by guessed meaning, or otherwise "clean up" the layout unless an existing repo header or PS2 evidence proves a specific correction. ### 1e. Assembly reference @@ -130,6 +132,8 @@ and assembly: Utilize the dwarf information that you get from the lookup skill heavily. +For any recovered type you touch while implementing the function, treat the DWARF body as source material to copy, not prose to paraphrase. Start from the dumped layout in the canonical owner header, then make only the minimal verified fixes. + Don't add explanatory comments during implementation unless you need to document a remaining DWARF mismatch. Don't use any temporary local variables that don't exist in the dwarf. diff --git a/.github/skills/scaffold/SKILL.md b/.github/skills/scaffold/SKILL.md index 3fe3d9256..52cdf0e9a 100644 --- a/.github/skills/scaffold/SKILL.md +++ b/.github/skills/scaffold/SKILL.md @@ -29,7 +29,14 @@ Collect data from **all** of these sources in parallel where possible: ## Phase 2: Setup class -Copy and cleanup the header that you got from running the `lookup` skill using the `symbols/Dwarf` folder. Fix visibility, function order and vtable related things based on using `lookup` on the PS2 types. +Copy the header/type body that you got from running the `lookup` skill using the +`symbols/Dwarf` folder into the canonical owner header first. Do not retype or +reconstruct the layout from memory, from scattered callsites, or from guessed +semantics. + +Then do the minimum cleanup backed by evidence: fix visibility, function order and +vtable related things based on using `lookup` on the PS2 types, and clean up duplicated +inline copies when the DWARF emitted both versions. For formatting and local cleanup while writing the header, consult `.github/skills/code_style/SKILL.md`. Use it for member-comment alignment, declaration @@ -50,6 +57,11 @@ Preserve real member names, types, order, and offset comments while scaffolding. fill gaps with invented `pad`, `unk`, or `field_XXXX` members for game types; verify the layout from Dwarf / PS2 data and leave a TODO over the type if a field is still uncertain. +Preserve the declaration order from the dumped type body as well, not just the member +order. Do not regroup methods, fields, enums, or helper declarations for readability +unless an existing repo header or PS2 evidence proves the original owner header used a +different order. + Keep the `// total size: 0x...` comment above the recovered type declaration. When the recovered type is a `class`, keep explicit access sections and prefer putting methods / accessors before the member layout block unless existing repo evidence says otherwise. diff --git a/AGENTS.md b/AGENTS.md index 3e97d44b3..771ecd9a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -342,8 +342,15 @@ This is a **C++98** codebase compiled with ProDG GC 3.9.3 (GCC 2.95 under the ho - Inline assembly is acceptable when needed to reproduce dead code or compiler scheduling that source alone cannot express cleanly - Preserve the original `class` vs `struct` kind. Check existing headers first, then Dwarf / PS2 info when needed. Even forward declarations and local partial declarations should use the accurate keyword when known. - Prefer including the real repo header over introducing a local forward declaration for a project type. If a type already has a header in `src/`, include it instead of redeclaring it locally. -- If a subsystem already has a stub owner header and the debug line info points back at that subsystem, fill the owner header instead of keeping a recovered project type declaration in a `.cpp`. +- If a subsystem already has a stub or umbrella owner header and the debug line info points back at that subsystem, fill the owner header instead of keeping a recovered project type declaration in a `.cpp` or spinning up a one-off micro-header just for that type. +- Apply the same owner-header rule to shared enums, globals, callback typedefs, and free functions. If multiple TUs need the declaration, put it in the canonical owner header once and include that header instead of duplicating enum bodies or `extern` blocks across `.cpp`s. - Preserve original member names, types, order, and proven layout comments. Do not invent `pad`, `unk`, or `field_XXXX` members just to satisfy a guessed size or offset; verify the real members with `find-symbol.py`, GC Dwarf, and PS2 data, and leave a short TODO if a layout detail is still uncertain. +- When recovering a type, start by copying the GC DWARF struct/class body into the canonical owner header. Treat that dump as the source of truth for declaration order too; only apply targeted fixes that are backed by existing repo headers or PS2 data, such as visibility, virtual/function order, duplicate-inline cleanup, or owner-header placement. +- Do not hand-reconstruct recovered layouts from scattered field accesses, guessed semantics, or a "cleaned up" reordering. If you do not have enough evidence to paste the type confidently, stop and gather more DWARF / PS2 info first. +- Preserve the original scope and nesting of recovered declarations too. Keep class-owned enums/types nested when the original did, and move subsystem/global enums into their real owner header instead of flattening or duplicating them near one caller. +- Use the narrowest correct home for recovered declarations: shared project-facing types in headers, TU-private helper structs/classes and allocator metadata in the `.cpp`. Do not dump implementation-only helpers into public headers just because they were convenient to write there. +- Prefer real subsystem or vendor headers over ad-hoc local typedef/prototype blocks. If an external API is shared and the project is missing the proper header, add that header in the correct subtree instead of stashing declarations in an unrelated gameplay file. +- Do not leave repeated `// TODO move`, `// TODO where should this go`, or "I just made this up" markers around declarations. Either move the declaration to its owner now or leave one short targeted TODO above the owner declaration if ownership is still genuinely unresolved. - Follow DWARF member naming exactly (`mMember` vs `m_member`) instead of normalizing names - Omit the `this` pointer. - Use `nullptr` and `override`. If they are missing, you need to include `types.h`. @@ -381,7 +388,7 @@ A function is only done when both objdiff and normalized DWARF are exact. Treat 100% instruction match with a DWARF mismatch as unfinished work, not a near-complete result. -The dwarf of your structs doesn't have to neccessarily match the original due to various reasons, just make sure that you copied everything correctly. +The DWARF of your structs does not always compare cleanly in every detail, but the recovery process still starts by copying the dumped layout correctly. Do not freehand-reconstruct a struct from call sites or guessed semantics; paste the DWARF body into the real owner header first, then make only the minimal PS2/header-backed fixes such as visibility, function order, vtable order, or duplicate-inline cleanup. Never dismiss a diff as "close enough" or "just register allocation." Every mismatched instruction is a signal that the source doesn't perfectly represent the original. Even @@ -417,6 +424,8 @@ Virtual table layout is also missing from the dwarf but there on PS2. Be aware t The inline information in the dwarf is incredibly useful. When you encounter one, you should look up its body in the project. If it doesn't exist yet, deduce how the code should look like and add it to the correct header (you can use your address lookup skill or if that doesn't succeed and the inline is a member function, just find the corresponding class in the project). +For recovered structs and classes, treat DWARF as copied source material rather than a loose sketch. Paste the dumped type into the owner header first and keep its declaration/member order unless PS2 or an existing repo header proves a specific correction. + It's very important that you use math inlines from bMath and UMath as shown in the dwarf. UVector inlines use temporaries that the compiler couldn't optimize out. You can see in the dwarf on which stack address they are and deduce final destination they are copied to. ### Store instruction order hints From 7414acfae64acea5b6c95a45f3c10004a714a8cf Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 12:49:18 +0100 Subject: [PATCH 48/71] 99.911%: improve zAttribSys VecHashMap DWARF ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 5 ++++ .../Indep/Tools/AttribSys/Runtime/AttribSys.h | 3 ++- .../AttribSys/Runtime/Common/AttribClass.cpp | 5 ---- .../Runtime/Common/AttribDatabase.cpp | 2 ++ .../AttribSys/Runtime/Common/AttribHashMap.h | 6 +++-- .../AttribSys/Runtime/Common/AttribPrivate.h | 23 ++++++++++++++--- .../Tools/AttribSys/Runtime/VecHashMap64.h | 25 +++++++++++-------- 8 files changed, 49 insertions(+), 21 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 15f4779af..f6a7108f2 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -122,6 +122,7 @@ Foo::Foo() - Use the repo's header guard form when writing headers: `#ifndef` / `#define` plus the `#ifdef EA_PRAGMA_ONCE_SUPPORTED` / `#pragma once` block. - Keep member layout comments aligned and intact in decomp headers. +- In match-sensitive headers, do not add class/member placement-`new` or unsized `operator delete` overloads just because the implementation uses placement new or delete expressions. Prefer the platform/global overloads that the original headers already pulled in unless DWARF proves the type exposed a custom overload. - When writing a recovered layout, start from a pasted GC DWARF dump instead of hand-reconstructing a cleaner version. Treat the dump as source-of-truth data entry, then make only small verified fixes from PS2 or existing headers. - Preserve the original `class` / `struct` kind from existing headers or Dwarf / PS2 evidence; do not treat it as a cosmetic style choice. - Treat header declarations as the repo source of truth. If the repo only has local `.cpp` partial declarations, verify the kind with the PS2 dump instead of copying them blindly. diff --git a/AGENTS.md b/AGENTS.md index 37b24b3fc..a93fe5070 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -529,6 +529,11 @@ TU: | Function: TU: zAttribSys | Function: \_STL::\_Rb_tree::\_M_insert If an STL node insertion path refuses to match, check whether the element type is missing explicit inline special members that the original source exposed. Adding the Dwarf-backed `operator new`, `operator delete`, placement `new`, copy constructor, and tiny accessors to `TypeDesc` made the tree node creation/insertion path match exactly. +### WrapperOwnedVecHashMapHelpers + +TU: zAttribSys | Function: Class::SetTableBuffer / Class::AddCollection / Database::AddClass +For `VecHashMap`-backed wrapper tables, keep `VecHashMap::Clear()` as a named helper, keep the `CollectionHashMap` constructor and scan helpers on the thin wrapper in `AttribPrivate.h`, and rely on the global placement `operator new` from SN's `` instead of adding a `Node::operator new` member. That combination restored `Class::SetTableBuffer`, `Class::AddCollection`, and `Database::AddClass` to PASS/PASS without changing objdiff. + ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index 2e62f0405..32429231a 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -607,7 +607,8 @@ class Node { // total size: 0xC class Class { public: - struct TablePolicy { + class TablePolicy { + public: static void *Alloc(std::size_t bytes) { return TableAllocFunc(bytes); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 7e305653b..f78dc2d72 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -49,11 +49,6 @@ Key Class::GetNextDefinition(Key prev) const { return 0; } -ClassPrivate::CollectionHashMap::~CollectionHashMap() {} - -ClassPrivate::CollectionHashMap::CollectionHashMap(unsigned int reserve) - : VecHashMap(reserve) {} - const Collection *Class::GetCollection(Key key) const { return mPrivates.mCollections.Find(key); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index e0d597bef..3d06dab49 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -243,4 +243,6 @@ Key StringToKey(const char *str) { TypeTable::~TypeTable() {} +ClassTable::~ClassTable() {} + }; // namespace Attrib diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index e8d1d7bad..f055e8aae 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -10,8 +10,10 @@ // Credit: Brawltendo namespace Attrib { -struct HashMap { - struct HashMapTablePolicy { +class HashMap { + public: + class HashMapTablePolicy { + public: static std::size_t KeyIndex(Key k, std::size_t tableSize, unsigned int keyShift) { return RotateNTo32(k, keyShift) % tableSize; } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index df5177132..5c921cdac 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -191,10 +191,26 @@ class ClassPrivate : public Class { } // total size: 0x10 - struct CollectionHashMap : public VecHashMap { - CollectionHashMap(unsigned int reserve); + class CollectionHashMap : public VecHashMap { + public: + CollectionHashMap(std::size_t reserve) : VecHashMap(reserve) {} - ~CollectionHashMap(); + ~CollectionHashMap() {} + + unsigned int GetNextValidIndex(unsigned int startPoint) const { + unsigned int index = startPoint + 1; + for (; index < mTableSize && !mTable[index].IsValid(); index++) { + } + return index; + } + + unsigned int GetKeyAtIndex(unsigned int index) const { + if (ValidIndex(index)) { + (void)ValidIndex(index); + return mTable[index].Key(); + } + return 0; + } }; void *operator new(std::size_t bytes) { @@ -227,6 +243,7 @@ class ClassPrivate : public Class { class ClassTable : public VecHashMap { public: ClassTable(std::size_t capacity) : VecHashMap(capacity) {} + ~ClassTable(); void operator delete(void *ptr, std::size_t bytes) { Free(ptr, bytes, "Attrib::ClassTable"); diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 59e356fb6..07821907a 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -8,9 +8,10 @@ #include "Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h" // total size: 0x10 -template struct VecHashMap { +template class VecHashMap { // total size: 0xC or 0x10 - struct Node { + class Node { + public: Node() : mKey(0), mPtr(reinterpret_cast(this)), mMax(0) {} Node(KeyType key, T *ptr) : mKey(key), mPtr(ptr) {} @@ -62,7 +63,13 @@ template Date: Tue, 24 Mar 2026 13:11:40 +0100 Subject: [PATCH 49/71] 99.911%: restore CollectionHashMap destructor emission Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 5 +++++ .../Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp | 2 ++ .../Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index f6a7108f2..1bf34812d 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -149,6 +149,7 @@ Foo::Foo() - Expand dense one-line helper structs, declaration blocks, and function bodies in non-match-sensitive files into normal multiline formatting. - In low-level headers, prefer normal multi-line bodies for touched inline operators and accessors instead of stacking `{ return ...; }` on one line, unless the surrounding file clearly uses intentional placeholder one-liners. +- In match-sensitive `.cpp` files, do not slide a restored tiny out-of-line special member above the file's first real top-level function just for tidiness. On `zAttribSys`, moving `CollectionHashMap::~CollectionHashMap()` above `Class::Class` was enough to rename a `global constructors keyed to ...` helper and drop the TU until the original first-function order was restored. - Prefer readable blocks over stacked one-line statements when behavior does not depend on exact source shape. - In touched validation/parsing code, prefer explicit min/max or boundary checks over equivalent magic-constant arithmetic when the clearer form still compiles to the verified result. - In parser/state-table code, prefer named enums and enum-typed state variables over anonymous integer state codes when that rewrite is verified safe. diff --git a/AGENTS.md b/AGENTS.md index a93fe5070..dc3b741bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -534,6 +534,11 @@ If an STL node insertion path refuses to match, check whether the element type i TU: zAttribSys | Function: Class::SetTableBuffer / Class::AddCollection / Database::AddClass For `VecHashMap`-backed wrapper tables, keep `VecHashMap::Clear()` as a named helper, keep the `CollectionHashMap` constructor and scan helpers on the thin wrapper in `AttribPrivate.h`, and rely on the global placement `operator new` from SN's `` instead of adding a `Node::operator new` member. That combination restored `Class::SetTableBuffer`, `Class::AddCollection`, and `Database::AddClass` to PASS/PASS without changing objdiff. +### FirstFunctionAnchorsKeyedConstructors + +TU: zAttribSys | Function: global constructors keyed to Attrib::Class::Class / ClassPrivate::CollectionHashMap::~CollectionHashMap +In a jumbo TU, do not move a newly restored out-of-line wrapper special member above the file's first real top-level function without rechecking symbol order. In `AttribClass.cpp`, restoring `CollectionHashMap::~CollectionHashMap()` was necessary to emit the destructor and keep `Class::Delete` matched, but placing it above `Class::Class` renamed the 44-byte `global constructors keyed to ...` helper. Keeping `Class::Class` first and moving the wrapper dtor below it restored the helper name and the TU's `99.91143%` floor. + ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index f78dc2d72..7663df5f1 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -13,6 +13,8 @@ Class::~Class() { Database::Get().RemoveClass(this); } +ClassPrivate::CollectionHashMap::~CollectionHashMap() {} + const Definition *Class::GetDefinition(Key key) const { Definition target(key); const Definition *b = mPrivates.mDefinitions; diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index 5c921cdac..b2d5bd897 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -195,7 +195,7 @@ class ClassPrivate : public Class { public: CollectionHashMap(std::size_t reserve) : VecHashMap(reserve) {} - ~CollectionHashMap() {} + ~CollectionHashMap(); unsigned int GetNextValidIndex(unsigned int startPoint) const { unsigned int index = startPoint + 1; From 1f223921562f837a9eb9060c75bfcda5264c663d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 13:13:25 +0100 Subject: [PATCH 50/71] 99.911%: match Class::AllocLayout dwarf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 7663df5f1..95bbd2598 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -111,7 +111,7 @@ void *Class::AllocLayout() const { void *data = Attrib::Alloc(mPrivates.mLayoutSize, "Attrib::Class"); memset(data, 0, mPrivates.mLayoutSize); - Definition *defs = mPrivates.mDefinitions; + const Definition *defs = mPrivates.mDefinitions; for (std::size_t d = 0; d < mPrivates.mNumDefinitions; d++) { if (defs[d].IsArray() && defs[d].InLayout()) { char *arrayptr = (char *)data + defs[d].GetOffset(); // or maybe the offset is added later From 0a5e12642e0062d9ccf707802ae7468144d7f4fa Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 13:22:01 +0100 Subject: [PATCH 51/71] 99.911%: match collection helper DWARF Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 5 +++++ .../Tools/AttribSys/Runtime/Common/AttribCollection.cpp | 3 ++- .../Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index 1bf34812d..ac45b287e 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -136,6 +136,7 @@ Foo::Foo() - Add offset / size comments when you are writing recovered type layouts from DWARF. - In recovered layouts, prefer explicit-width aliases such as `uint8` / `uint16` when the field width is known. Use plain `char` for text / byte buffers and `signed char` when the field is a signed 8-bit counter. - Define inline member functions in headers only when DWARF shows that they are genuinely inlined in the binary. +- In touched shared inlines/templates, preserve recovered parameter names too. In `zAttribSys`, changing `HashMapTablePolicy::WrapIndex` from `k` back to DWARF's `index` cleared several matched-function DWARF mismatches without changing codegen. - Use `struct` for POD-like data carriers with public fields; use `class` for behavior-heavy types only when that matches the recovered type information. - Keep tiny placeholder methods as concise inline bodies when that is already the local pattern. diff --git a/AGENTS.md b/AGENTS.md index dc3b741bf..3b5a2ba66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -539,6 +539,11 @@ For `VecHashMap`-backed wrapper tables, keep `VecHashMap::Clear()` as a named he TU: zAttribSys | Function: global constructors keyed to Attrib::Class::Class / ClassPrivate::CollectionHashMap::~CollectionHashMap In a jumbo TU, do not move a newly restored out-of-line wrapper special member above the file's first real top-level function without rechecking symbol order. In `AttribClass.cpp`, restoring `CollectionHashMap::~CollectionHashMap()` was necessary to emit the destructor and keep `Class::Delete` matched, but placing it above `Class::Class` renamed the 44-byte `global constructors keyed to ...` helper. Keeping `Class::Class` first and moving the wrapper dtor below it restored the helper name and the TU's `99.91143%` floor. +### SharedInlineParameterNamesMatter + +TU: zAttribSys | Function: Collection::Contains / Collection::NextKey / Collection::GetNode / HashMap::UpdateSearchLength +When a byte-exact mismatch fans out through the same shared inline helper, verify the helper's recovered parameter names before restructuring callers. Renaming `HashMapTablePolicy::WrapIndex`'s first parameter from `k` back to DWARF's `index` cleared four matched-function DWARF mismatches at once without changing objdiff. + ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp index 31ff372b2..7bdfef386 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribCollection.cpp @@ -201,9 +201,10 @@ bool Collection::AddAttribute(Key attributeKey, unsigned int count) { if (Contains(attributeKey)) { return false; } + const Class *c = mClass; bool result = false; unsigned char flags = 0; - const Attrib::Definition *d = mClass->GetDefinition(attributeKey); + const Attrib::Definition *d = c->GetDefinition(attributeKey); if (d) { d->InLayout(); if (d->IsArray()) { diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index f055e8aae..75c65b61d 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -18,8 +18,8 @@ class HashMap { return RotateNTo32(k, keyShift) % tableSize; } - static std::size_t WrapIndex(Key k, std::size_t tableSize, unsigned int keyShift) { - return k % tableSize; + static std::size_t WrapIndex(Key index, std::size_t tableSize, unsigned int keyShift) { + return index % tableSize; } static void *Alloc(std::size_t bytes) { From c99e5be3557949e2f3f2b681356092979319037a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 13:28:41 +0100 Subject: [PATCH 52/71] 99.911%: match HashMap::PreFlightAdd dwarf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index 75c65b61d..3f988073e 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -329,14 +329,15 @@ class HashMap { std::size_t PreFlightAdd(Key key, std::size_t targetIndex, std::size_t &searchLen) { searchLen = 0; - while (mTable[targetIndex].IsValid()) { - if (mTable[targetIndex].GetKey() == key) { + std::size_t actualIndex = targetIndex; + while (mTable[actualIndex].IsValid()) { + if (mTable[actualIndex].GetKey() == key) { return static_cast(-1); } - targetIndex = HashMapTablePolicy::WrapIndex(targetIndex + 1, mTableSize, 0); + actualIndex = HashMapTablePolicy::WrapIndex(actualIndex + 1, mTableSize, 0); searchLen++; } - return targetIndex; + return actualIndex; } void PostFlightAdd(std::size_t targetIndex, std::size_t searchLen) { From c7ac70b975b05813522e42d4f29ac099ef7fb774 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 13:31:45 +0100 Subject: [PATCH 53/71] 99.911%: match Vault::Initialize dwarf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tools/AttribSys/Runtime/Common/AttribLoadAndGo.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribLoadAndGo.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribLoadAndGo.cpp index a3c302400..4923b9caf 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribLoadAndGo.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribLoadAndGo.cpp @@ -265,9 +265,9 @@ void Vault::Initialize() { break; case 4: { Vault *depVault = reinterpret_cast(mDepData[ptr->mIndex].mData); - unsigned int exportIndex = depVault->FindExportID(ptr->mExportID); - if (exportIndex != -1) { - *targetptr = reinterpret_cast(depVault->GetExportData(exportIndex)); + unsigned int exportindex = depVault->FindExportID(ptr->mExportID); + if (exportindex != -1) { + *targetptr = reinterpret_cast(depVault->GetExportData(exportindex)); } else { *targetptr = nullptr; } From 25f6df06459ec9c0da5fb576dcf0d0300b6ebc6d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 13:50:08 +0100 Subject: [PATCH 54/71] 99.911%: fix HashMap delete-path dwarf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/code_style/SKILL.md | 1 + AGENTS.md | 5 +++++ .../Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h | 3 --- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index ac45b287e..1121d173b 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -123,6 +123,7 @@ Foo::Foo() - Use the repo's header guard form when writing headers: `#ifndef` / `#define` plus the `#ifdef EA_PRAGMA_ONCE_SUPPORTED` / `#pragma once` block. - Keep member layout comments aligned and intact in decomp headers. - In match-sensitive headers, do not add class/member placement-`new` or unsized `operator delete` overloads just because the implementation uses placement new or delete expressions. Prefer the platform/global overloads that the original headers already pulled in unless DWARF proves the type exposed a custom overload. +- If a delete-path DWARF diff keeps collapsing to an empty unsized `operator delete()` helper, check whether the touched type still has a stray unsized `operator delete(void *)` overload. In `zAttribSys`, removing the extra unsized `HashMap::operator delete(void *)` restored the original-sized delete path and made multiple matched functions exact. - When writing a recovered layout, start from a pasted GC DWARF dump instead of hand-reconstructing a cleaner version. Treat the dump as source-of-truth data entry, then make only small verified fixes from PS2 or existing headers. - Preserve the original `class` / `struct` kind from existing headers or Dwarf / PS2 evidence; do not treat it as a cosmetic style choice. - Treat header declarations as the repo source of truth. If the repo only has local `.cpp` partial declarations, verify the kind with the PS2 dump instead of copying them blindly. diff --git a/AGENTS.md b/AGENTS.md index 3b5a2ba66..bfa306b17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -544,6 +544,11 @@ In a jumbo TU, do not move a newly restored out-of-line wrapper special member a TU: zAttribSys | Function: Collection::Contains / Collection::NextKey / Collection::GetNode / HashMap::UpdateSearchLength When a byte-exact mismatch fans out through the same shared inline helper, verify the helper's recovered parameter names before restructuring callers. Renaming `HashMapTablePolicy::WrapIndex`'s first parameter from `k` back to DWARF's `index` cleared four matched-function DWARF mismatches at once without changing objdiff. +### SizedDeletePathBeatsStrayUnsizedDelete + +TU: zAttribSys | Function: Collection::~Collection / Class::Delete / HashMap::PreFlightAdd +If delete-path DWARF keeps collapsing to an empty unsized `operator delete()` helper, re-check whether the type still has a stray unsized `operator delete(void *)` overload that the original source never exposed. In `AttribHashMap.h`, removing the unsized `HashMap::operator delete(void *)` let ProDG reuse the sized delete path again and made `Collection::~Collection` and `Class::Delete` DWARF-exact without moving objdiff. + ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h index 3f988073e..8f8ccc52f 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribHashMap.h @@ -55,9 +55,6 @@ class HashMap { return ptr; } - void operator delete(void *) { - } - HashMap(std::size_t reservationSize, unsigned int keyShift, bool exactFit) : mTable(nullptr), mTableSize(0), mNumEntries(0), mWorstCollision(0), mKeyShift(keyShift) { if (reservationSize != 0) { From b243580e71f0437314e818fa09140a079e5d4fad Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 14:33:33 +0100 Subject: [PATCH 55/71] dwarf --- src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h | 8 ++------ .../Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h | 8 ++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index 32429231a..8d18e02ea 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -629,13 +629,9 @@ class Class { return AdjustHashTableSize(entries); } - static unsigned int KeyIndex(unsigned int k, unsigned int tableSize, unsigned int keyShift) { - return RotateNTo32(k, keyShift) % tableSize; - } + static unsigned int KeyIndex(unsigned int k, unsigned int tableSize, unsigned int keyShift); - static unsigned int WrapIndex(unsigned int index, unsigned int tableSize, unsigned int keyShift) { - return index % tableSize; - } + static unsigned int WrapIndex(unsigned int index, unsigned int tableSize, unsigned int keyShift); }; Class(Key k, ClassPrivate &privates); diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index b2d5bd897..6b1588b58 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -13,6 +13,14 @@ // Credit: Brawltendo namespace Attrib { +inline unsigned int Class::TablePolicy::KeyIndex(unsigned int k, unsigned int tableSize, unsigned int keyShift) { + return RotateNTo32(k, keyShift) % tableSize; +} + +inline unsigned int Class::TablePolicy::WrapIndex(unsigned int index, unsigned int tableSize, unsigned int keyShift) { + return index % tableSize; +} + // total size: 0x20 class CollectionLoadData { public: From d6f618f7b8c60c40f8ad634c821aa9c5572f076a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 15:37:20 +0100 Subject: [PATCH 56/71] 99.9116%: improve zAttribSys remove-path matching Refine the late VecHashMap remove path in zAttribSys. - write Node::Get() as a ternary to drop the stray RemoveCollection DWARF mismatch - rewrite the second UpdateSearchLength loop header as maxSearch > searchLen to make RemoveCollection DWARF-exact and raise the unit text floor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 12 +++++++++++- .../Tools/AttribSys/Runtime/Common/AttribClass.cpp | 2 +- .../AttribSys/Runtime/Common/AttribDatabase.cpp | 2 +- .../Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 7 ++----- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bfa306b17..7b18c92bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -544,6 +544,16 @@ In a jumbo TU, do not move a newly restored out-of-line wrapper special member a TU: zAttribSys | Function: Collection::Contains / Collection::NextKey / Collection::GetNode / HashMap::UpdateSearchLength When a byte-exact mismatch fans out through the same shared inline helper, verify the helper's recovered parameter names before restructuring callers. Renaming `HashMapTablePolicy::WrapIndex`'s first parameter from `k` back to DWARF's `index` cleared four matched-function DWARF mismatches at once without changing objdiff. +### TernaryNodeGetClearsWrapperDWARFNoise + +TU: zAttribSys | Function: Class::RemoveCollection / VecHashMap::RemoveIndex +When `VecHashMap::Node::Get()` is written as `return IsValid() ? mPtr : nullptr;` instead of an `if (IsValid()) return mPtr; return nullptr;` ladder, `Class::RemoveCollection` drops the stray `RemoveIndex::result // r26` DWARF mismatch while leaving objdiff and `Database::RemoveClass` unchanged. This does not solve the final shared `searchLen r6/r7` tie-break, but it is a safe retained cleanup that reduces the last `RemoveCollection` DWARF debt to the same single mismatch as `RemoveClass`. + +### MaxSearchGreaterFixesRemoveCollectionDwarf + +TU: zAttribSys | Function: Class::RemoveCollection / VecHashMap::UpdateSearchLength +Writing the second `UpdateSearchLength` search loop as `for (unsigned int searchLen = 1; maxSearch > searchLen; searchLen++)` instead of `searchLen < maxSearch` is a real retained partial win. On `zAttribSys` it makes `Class::RemoveCollection` DWARF-exact, improves that wrapper's objdiff slightly, and nudges the unit text floor from `99.91143%` to `99.9116%`, but it does not help `Database::RemoveClass`. + ### SizedDeletePathBeatsStrayUnsizedDelete TU: zAttribSys | Function: Collection::~Collection / Class::Delete / HashMap::PreFlightAdd @@ -552,7 +562,7 @@ If delete-path DWARF keeps collapsing to an empty unsized `operator delete()` he ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass -If two near-matching functions differ only because the same inlined helper chain lands `mTableSize` in `r6` in the original but `r7` in the rebuild, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, `VecHashMap::FindIndex` inlined through `Remove -> RemoveIndex -> UpdateSearchLength` produced a stable `lwz r6, 4(r3)` vs `lwz r7, 4(r3)` split, which then propagated into later `UpdateSearchLength` control-flow differences. This survived 300+ source experiments: loop-form changes, adding/removing temporaries, splitting/merging expressions, helper inline/outline changes, declaration-order tweaks, member type changes, access-control changes, template method reorderings, and inline vs out-of-line ctor/dtor placement. Once the diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only consider flag permutation or compiler-level investigation as a last resort. +If two near-matching functions differ only because the same inlined helper chain lands `mTableSize` in `r6` in the original but `r7` in the rebuild, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, `VecHashMap::FindIndex` inlined through `Remove -> RemoveIndex -> UpdateSearchLength` produced a stable `lwz r6, 4(r3)` vs `lwz r7, 4(r3)` split, which then propagated into later `UpdateSearchLength` control-flow differences. This survived 300+ source experiments plus later focused sweeps of `RemoveIndex` statement order, `FindIndex` local declaration order, second-call expression sugar, `UpdateSearchLength` prelude condition forms, `register` hints on params/locals, per-instantiation `UpdateSearchLength` specializations with identical bodies, and combination searches across individually neutral source toggles. Temporary compiler-side probing was no better: single-flag ProDG toggles around `gcse`/scheduling/CSE/regmove/caller-saves/thread-jumps/delayed-branch either regressed or produced no change, a named-register local only added DWARF debt without moving objdiff, and binding the parameter itself to a named register emitted an unreadable object that objdiff could not load. One real exception is the retained `maxSearch > searchLen` loop-header rewrite above: it makes `Class::RemoveCollection` DWARF-exact and bumps the unit floor slightly, but `Database::RemoveClass` still behaves like the same allocator tie-break family. Once the remaining diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only revisit the area if you have a genuinely new source shape or stronger compiler evidence than the dead ends above. ### NamedRodataForInlinedAllocatorStrings TU: zAttribSys | Function: DatabaseExportPolicy::Initialize diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 95bbd2598..8fdd8e106 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -99,7 +99,7 @@ bool Class::AddCollection(Collection *c) { return mPrivates.mCollections.Add(c->GetKey(), c); } -// NON_MATCHING: 98.6% - r6/r7 register swap in VecHashMap::FindIndex inlined into Remove +// NON_MATCHING: 98.7% - shared VecHashMap remove path still has ~21B of objdiff after the maxSearch > searchLen loop-header fix made DWARF exact bool Class::RemoveCollection(Collection *c) { return mPrivates.mCollections.Remove(c->GetKey()); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index 3d06dab49..91583a2de 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -219,7 +219,7 @@ bool Database::AddClass(Class *c) { return mPrivates.mClasses.Add(c->GetKey(), c); } -// NON_MATCHING: 98.5% - r6/r7 register swap in VecHashMap::FindIndex inlined into Remove +// NON_MATCHING: 98.5% - second VecHashMap::UpdateSearchLength inline keeps searchLen in r7 instead of r6 void Database::RemoveClass(const Class *c) { mPrivates.mClasses.Remove(c->GetKey()); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 07821907a..4cae8403b 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -34,10 +34,7 @@ template searchLen; searchLen++) { unsigned int index = Policy::WrapIndex(targetIndex + searchLen, mTableSize, 0); if (Policy::KeyIndex(mTable[index].Key(), mTableSize, 0) == targetIndex) { newMaxSearch = searchLen; From af94da19e3233361976b99b49454a15cdd6b7eb4 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 22:05:15 +0100 Subject: [PATCH 57/71] tools: add prodg_dump helper --- AGENTS.md | 53 +++- tools/prodg_dump.py | 678 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 tools/prodg_dump.py diff --git a/AGENTS.md b/AGENTS.md index 7b18c92bf..87d920914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,6 +258,29 @@ It also compares the debug-line ownership of each `// Range:` block. Treat the strong evidence that an inline body came from the wrong header or owner file. The exact file+line count is stricter and mainly useful as a secondary hint, not as the main gate. +### prodg_dump.py — ProDG compiler-state dump helper + +When you need the exact ProDG compiler state for one unit, prefer this helper over +reconstructing long `ngccc` / `cc1plus` command lines by hand. It recovers the real +`ngccc.exe` invocation from `ninja -t commands`, derives the matching preprocess step, +runs `cc1plus.exe -da`, and can extract or diff one function across dump sets. + +```sh +python tools/prodg_dump.py command -u main/Speed/Indep/SourceLists/zAttribSys +python tools/prodg_dump.py dump -u main/Speed/Indep/SourceLists/zAttribSys -o /tmp/zattrib_base +python tools/prodg_dump.py extract /tmp/zattrib_base --stage lreg \ + -f 'VecHashMap::UpdateSearchLength' +python tools/prodg_dump.py diff /tmp/zattrib_dumps /tmp/zattrib_dumps \ + --left-base-name base --right-base-name preinc --stages lreg,greg,rtl \ + -f 'VecHashMap::UpdateSearchLength' +``` + +Use `extract --grep ... -C ` when you only want a few interesting lines inside a +function block, such as stack-slot references or one pseudo register family. `diff` +prints a unified diff for each requested stage, and for `lreg` it also summarizes +register-preference and final hard-register assignment changes so allocator/regclass +shifts stand out immediately. + When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work faster, shorter, or more reliable, prefer implementing that improvement as part of the task @@ -547,13 +570,39 @@ When a byte-exact mismatch fans out through the same shared inline helper, verif ### TernaryNodeGetClearsWrapperDWARFNoise TU: zAttribSys | Function: Class::RemoveCollection / VecHashMap::RemoveIndex -When `VecHashMap::Node::Get()` is written as `return IsValid() ? mPtr : nullptr;` instead of an `if (IsValid()) return mPtr; return nullptr;` ladder, `Class::RemoveCollection` drops the stray `RemoveIndex::result // r26` DWARF mismatch while leaving objdiff and `Database::RemoveClass` unchanged. This does not solve the final shared `searchLen r6/r7` tie-break, but it is a safe retained cleanup that reduces the last `RemoveCollection` DWARF debt to the same single mismatch as `RemoveClass`. +When `VecHashMap::Node::Get()` is written as `return IsValid() ? mPtr : nullptr;` instead of an `if (IsValid()) return mPtr; return nullptr;` ladder, `Class::RemoveCollection` drops the stray `RemoveIndex::result // r26` DWARF mismatch while leaving objdiff and `Database::RemoveClass` unchanged. This is a safe retained cleanup, but it does not touch the remaining `Database::RemoveClass` first-inline `newMaxSearch r31 -> r30` DWARF debt. ### MaxSearchGreaterFixesRemoveCollectionDwarf TU: zAttribSys | Function: Class::RemoveCollection / VecHashMap::UpdateSearchLength Writing the second `UpdateSearchLength` search loop as `for (unsigned int searchLen = 1; maxSearch > searchLen; searchLen++)` instead of `searchLen < maxSearch` is a real retained partial win. On `zAttribSys` it makes `Class::RemoveCollection` DWARF-exact, improves that wrapper's objdiff slightly, and nudges the unit text floor from `99.91143%` to `99.9116%`, but it does not help `Database::RemoveClass`. +### SearchFirstHoistIsFalseFriend + +TU: zAttribSys | Function: Database::RemoveClass / VecHashMap::UpdateSearchLength +Hoisting the second-loop `searchLen` declaration above `newMaxSearch` (`unsigned int searchLen = 1; unsigned int newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++)`) is a false friend. On the current endgame floor it improves the **second** `UpdateSearchLength` inline in `Database::RemoveClass` and nudges objdiff up to `98.6%`, but it also hoists `searchLen` out of the anonymous loop block in **both** inlined `UpdateSearchLength` bodies and drops normalized DWARF to about `86.6%`, so do not keep or build on that variant. + +### BlockScopedSearchLenTradesOneDebtForAnother + +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength +The block-scoped variant `unsigned int newMaxSearch; { unsigned int searchLen = 1; newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++) ... }` is another false friend. On the current floor it fixes `Database::RemoveClass`'s **second-inline** `newMaxSearch // r31` placement and raises that wrapper to `98.6%`, but it simultaneously reintroduces the old second-inline `searchLen // r7 -> r6` mismatch in **both** wrappers and drops `Class::RemoveCollection` back to `98.6% / 99.6%`, so it is not a real endgame win. +On that higher-text plateau (`99.91307%`, `42B` remaining), a whole batch of "safe" follow-up nudges stayed completely inert: empty `if (Unk2)` / `if (!Unk2)` hooks inside the late loop, `searchLen = searchLen + 0`, `newMaxSearch = newMaxSearch + 0`, `register` hints on `searchLen` / `newMaxSearch`, typed-pointer `IsValid()`, `Move`'s C-style cast, `Key()`'s zero spellings, local `asm("r7")` register variables, and even an empty `asm volatile("" : "+r"(searchLen))` constraint. If you revisit this plateau, skip those low-risk hooks and look for a genuinely different source shape. + +### PreincrementLessThanFixesHeaderButMovesGlobalRegs + +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength +Writing the late scan as `for (unsigned int searchLen = 0; ++searchLen < maxSearch; )` is the first condition-only rewrite that makes the second `UpdateSearchLength` loop header itself line up: it fixes the `cmplwi r4, 1` fold, restores the `li r7, 1` / `cmplw r7, r4` shape, and also fixes `Database::RemoveClass`'s `newMaxSearch // r31` placement in that late inline. But it is still a false friend overall: it moves both wrappers onto a broader `r6`/`r7` swap family, drops `Class::RemoveCollection` to `98.6% / 99.6%`, leaves `Database::RemoveClass` at `98.5% / 99.6%`, and changes the remaining mismatches rather than removing them. + +### Unk2SelectiveLoopBodyHooksAreRealButUnsafe + +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::RemoveIndex +The second empty `for` body in `VecHashMap::RemoveIndex` is a real per-instantiation regalloc lever via the `Unk2` template boolean, but the obvious source shapes are false friends. In this endgame, baseline `if (Unk2) {}` inside that empty body perturbs only `Database::RemoveClass` while leaving `Class::RemoveCollection` at the retained baseline, and baseline `if (!Unk2) {}` perturbs only `Class::RemoveCollection` while leaving `Database::RemoveClass` at the retained baseline. On top of the block-scoped `UpdateSearchLength` near-miss, `if (!Unk2) {}` still perturbs only `Class::RemoveCollection` while leaving `Database::RemoveClass` at the block state. That proves the `Unk2` polarity and the body-CFG selectivity are real, but the tested body forms (`{}`, expression statements, `switch (0)`, `continue`) all regressed, so do not assume an `Unk2`-guarded loop-body tweak is a safe hybrid fix by itself. By contrast, wrapping an empty body as `if (Unk2) { do {} while (0); }` or `if (!Unk2) { do {} while (0); }` was fully inert on the retained floor and is not a useful perturbation. + +### HeaderOnlyZAttribSweepsNeedForcedRebuild + +TU: zAttribSys | Function: any header-defined inline in `VecHashMap64.h` +Header-only sweeps against `VecHashMap64.h` can silently compare stale code unless you force a rebuild of the rebuilt jumbo object first. The generated `build.ninja` rule for `build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o` tracks only `src/Speed/Indep/SourceLists/zAttribSys.cpp`, so after editing included headers you must delete that rebuilt `.o` (and, if needed for tooling, refresh the `.ctx`) before trusting `verify` or `diff` output. + ### SizedDeletePathBeatsStrayUnsizedDelete TU: zAttribSys | Function: Collection::~Collection / Class::Delete / HashMap::PreFlightAdd @@ -562,7 +611,7 @@ If delete-path DWARF keeps collapsing to an empty unsized `operator delete()` he ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass -If two near-matching functions differ only because the same inlined helper chain lands `mTableSize` in `r6` in the original but `r7` in the rebuild, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, `VecHashMap::FindIndex` inlined through `Remove -> RemoveIndex -> UpdateSearchLength` produced a stable `lwz r6, 4(r3)` vs `lwz r7, 4(r3)` split, which then propagated into later `UpdateSearchLength` control-flow differences. This survived 300+ source experiments plus later focused sweeps of `RemoveIndex` statement order, `FindIndex` local declaration order, second-call expression sugar, `UpdateSearchLength` prelude condition forms, `register` hints on params/locals, per-instantiation `UpdateSearchLength` specializations with identical bodies, and combination searches across individually neutral source toggles. Temporary compiler-side probing was no better: single-flag ProDG toggles around `gcse`/scheduling/CSE/regmove/caller-saves/thread-jumps/delayed-branch either regressed or produced no change, a named-register local only added DWARF debt without moving objdiff, and binding the parameter itself to a named register emitted an unreadable object that objdiff could not load. One real exception is the retained `maxSearch > searchLen` loop-header rewrite above: it makes `Class::RemoveCollection` DWARF-exact and bumps the unit floor slightly, but `Database::RemoveClass` still behaves like the same allocator tie-break family. Once the remaining diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only revisit the area if you have a genuinely new source shape or stronger compiler evidence than the dead ends above. +If two near-matching functions differ only because the same inlined helper chain lands long-lived locals in different caller-saved registers, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, the retained `maxSearch > searchLen` rewrite moved the live endgame debt into the **second** `UpdateSearchLength` inline reached from `RemoveIndex`'s empty follow-up loop: `Class::RemoveCollection` is left with the shared late-loop compare/branch shape, while `Database::RemoveClass` also wants `newMaxSearch // r31` where the rebuild keeps it in `r30`. This survived 300+ source experiments plus later focused sweeps of `RemoveIndex` statement order, `FindIndex` local declaration order, wrapper-result liveness, first-call shapes, second-loop declaration/scope variants, second-call expression sugar, `UpdateSearchLength` prelude condition forms, `register` hints on params/locals, per-instantiation `UpdateSearchLength` specializations with identical bodies, and combination searches across individually neutral source toggles. Temporary compiler-side probing was no better: single-flag ProDG toggles around `gcse`/scheduling/CSE/regmove/caller-saves/thread-jumps/delayed-branch either regressed or produced no change, local register variables did not reserve hard registers, and explicit `UpdateSearchLength` specialization was catastrophic even with an identical body. One real exception is the retained `maxSearch > searchLen` loop-header rewrite above: it makes `Class::RemoveCollection` DWARF-exact and bumps the unit floor slightly, but the remaining `RemoveCollection` / `RemoveClass` debt still behaves like the same allocator tie-break family. Once the remaining diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only revisit the area if you have a genuinely new source shape or stronger compiler evidence than the dead ends above. ### NamedRodataForInlinedAllocatorStrings TU: zAttribSys | Function: DatabaseExportPolicy::Initialize diff --git a/tools/prodg_dump.py b/tools/prodg_dump.py new file mode 100644 index 000000000..5ce50e910 --- /dev/null +++ b/tools/prodg_dump.py @@ -0,0 +1,678 @@ +#!/usr/bin/env python3 + +""" +Generate and compare ProDG compiler-state dumps for one translation unit. + +Examples: + python tools/prodg_dump.py command -u main/Speed/Indep/SourceLists/zAttribSys + python tools/prodg_dump.py dump -u main/Speed/Indep/SourceLists/zAttribSys -o /tmp/zattrib_base + python tools/prodg_dump.py extract /tmp/zattrib_base --stage lreg \ + -f 'VecHashMap::UpdateSearchLength' + python tools/prodg_dump.py diff /tmp/zattrib_base /tmp/zattrib_trial \ + -f 'VecHashMap::UpdateSearchLength' +""" + +import argparse +import difflib +import os +import re +import shlex +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Sequence + +from _common import ( + ROOT_DIR, + ToolError, + ensure_project_prereqs, + find_objdiff_unit, + format_subprocess_error, + load_objdiff_config, + make_abs, +) + + +FUNCTION_HEADER_RE = re.compile(r"^;; Function (.+)$") +REGISTER_PREF_RE = re.compile(r"^Register (\d+) used .*; pref (.+)$") +REGISTER_ASSIGN_RE = re.compile(r"^;; Register (\d+) in ([^.]+)\.$") +DEFAULT_STAGES = ("rtl", "greg", "lreg") + + +class DumpToolError(RuntimeError): + pass + + +@dataclass +class UnitCompileInfo: + unit_name: str + source_path: Path + target_path: Path + compile_shell: str + compile_argv: List[str] + ngccc_index: int + + +@dataclass +class FunctionBlock: + header: str + start_line: int + lines: List[str] + + +def print_section(title: str) -> None: + print(flush=True) + print("=" * 60, flush=True) + print(f" {title}", flush=True) + print("=" * 60, flush=True) + + +def fail(message: str) -> None: + print(message, file=sys.stderr) + sys.exit(1) + + +def run_capture(cmd: Sequence[str]) -> subprocess.CompletedProcess[str]: + result = subprocess.run(cmd, cwd=ROOT_DIR, text=True, capture_output=True) + if result.returncode != 0: + raise DumpToolError( + format_subprocess_error(cmd, result.returncode, result.stdout, result.stderr) + ) + return result + + +def run_stream(cmd: Sequence[str]) -> None: + result = subprocess.run(cmd, cwd=ROOT_DIR, text=True) + if result.returncode != 0: + raise DumpToolError(format_subprocess_error(cmd, result.returncode)) + + +def get_unit_paths(unit_name: str) -> tuple[Path, Path]: + ensure_project_prereqs(require_build_ninja=True) + config = load_objdiff_config() + unit = find_objdiff_unit(config, unit_name) + if unit is None: + raise DumpToolError(f"Unit not found in objdiff.json: {unit_name}") + + metadata = unit.get("metadata") or {} + source_path = make_abs(metadata.get("source_path")) + if source_path is None: + raise DumpToolError(f"Unit has no source_path metadata: {unit_name}") + + target = unit.get("base_path") or unit.get("target_path") + target_path = make_abs(target) + if target_path is None: + raise DumpToolError(f"Unit has no build target in objdiff.json: {unit_name}") + + return Path(source_path), Path(target_path) + + +def rel_for_ninja(path: Path) -> str: + try: + return path.relative_to(ROOT_DIR).as_posix() + except ValueError: + return str(path) + + +def split_shell_pipeline(shell_line: str) -> str: + if "&&" in shell_line: + return shell_line.split("&&", 1)[0].strip() + return shell_line.strip() + + +def resolve_compile_info(unit_name: str) -> UnitCompileInfo: + source_path, target_path = get_unit_paths(unit_name) + target_rel = rel_for_ninja(target_path) + source_rel = rel_for_ninja(source_path) + + result = run_capture(["ninja", "-t", "commands", target_rel]) + compile_shell = "" + for raw_line in result.stdout.splitlines(): + line = raw_line.strip() + if "ngccc.exe" not in line: + continue + if source_rel in line or target_rel in line: + compile_shell = split_shell_pipeline(line) + break + + if not compile_shell: + raise DumpToolError( + "Failed to locate the ProDG compiler command for " + f"{unit_name} via `ninja -t commands {target_rel}`" + ) + + compile_argv = shlex.split(compile_shell) + for index, arg in enumerate(compile_argv): + if arg.endswith("ngccc.exe"): + return UnitCompileInfo( + unit_name=unit_name, + source_path=source_path, + target_path=target_path, + compile_shell=compile_shell, + compile_argv=compile_argv, + ngccc_index=index, + ) + + raise DumpToolError(f"Compiler line does not contain ngccc.exe:\n{compile_shell}") + + +def derived_base_name(info: UnitCompileInfo) -> str: + return info.source_path.stem + + +def derive_preprocess_command(info: UnitCompileInfo, ii_path: Path) -> List[str]: + compile_argv = list(info.compile_argv) + compiler_args = compile_argv[info.ngccc_index + 1 :] + filtered: List[str] = [] + + i = 0 + while i < len(compiler_args): + arg = compiler_args[i] + if arg in ("-MMD", "-MD", "-c"): + i += 1 + continue + if arg == "-o": + i += 2 + continue + filtered.append(arg) + i += 1 + + filtered.extend(["-E", "-o", str(ii_path)]) + return [*compile_argv[: info.ngccc_index + 1], *filtered] + + +def derive_cc1plus_command( + info: UnitCompileInfo, ii_path: Path, dumpbase: Path, asm_path: Path +) -> List[str]: + compile_argv = list(info.compile_argv) + prefix = compile_argv[: info.ngccc_index] + ngccc_path = Path(compile_argv[info.ngccc_index]) + cc1plus_path = ngccc_path.with_name("cc1plus.exe") + if not cc1plus_path.exists(): + raise DumpToolError(f"Missing cc1plus.exe next to ngccc.exe: {cc1plus_path}") + + return [ + *prefix, + str(cc1plus_path), + str(ii_path), + "-da", + "-dumpbase", + str(dumpbase), + "-o", + str(asm_path), + ] + + +def ensure_output_dir(path: Path, force: bool) -> None: + if path.exists() and not path.is_dir(): + raise DumpToolError(f"Output path is not a directory: {path}") + if path.exists() and any(path.iterdir()) and not force: + raise DumpToolError( + f"Output directory is not empty: {path}\n" + "Use --force to reuse it." + ) + path.mkdir(parents=True, exist_ok=True) + + +def parse_stages(value: str) -> List[str]: + stages = [stage.strip() for stage in value.split(",") if stage.strip()] + if not stages: + raise DumpToolError("Stage list must not be empty") + for stage in stages: + if not re.fullmatch(r"[A-Za-z0-9_+-]+", stage): + raise DumpToolError(f"Invalid stage name: {stage}") + return stages + + +def load_function_blocks(path: Path) -> List[FunctionBlock]: + lines = path.read_text(errors="replace").splitlines() + blocks: List[FunctionBlock] = [] + current_header: Optional[str] = None + current_start = 0 + current_lines: List[str] = [] + + for index, line in enumerate(lines, start=1): + match = FUNCTION_HEADER_RE.match(line) + if match: + if current_header is not None: + blocks.append( + FunctionBlock( + header=current_header, + start_line=current_start, + lines=current_lines, + ) + ) + current_header = match.group(1) + current_start = index + current_lines = [line] + continue + + if current_header is not None: + current_lines.append(line) + + if current_header is not None: + blocks.append( + FunctionBlock(header=current_header, start_line=current_start, lines=current_lines) + ) + + return blocks + + +def choose_block(blocks: Sequence[FunctionBlock], query: str, exact: bool) -> FunctionBlock: + if exact: + matches = [block for block in blocks if block.header == query] + else: + matches = [ + block + for block in blocks + if block.header == query or block.header.endswith(query) or query in block.header + ] + + if not matches: + raise DumpToolError(f"Function not found in dump: {query}") + if len(matches) > 1: + options = "\n".join(f" - {block.header}" for block in matches[:10]) + more = "" if len(matches) <= 10 else f"\n ... (+{len(matches) - 10} more)" + raise DumpToolError( + f"Function query matched multiple dump blocks: {query}\n{options}{more}" + ) + return matches[0] + + +def resolve_stage_file(path_value: str, stage: str, base_name: Optional[str]) -> Path: + path = Path(path_value) + if path.is_file(): + return path + if not path.is_dir(): + raise DumpToolError(f"Dump path is neither a file nor a directory: {path}") + + candidates: List[Path] = [] + if base_name: + candidate = path / f"{base_name}.{stage}" + if candidate.exists(): + return candidate + candidates = sorted(path.glob(f"*.{stage}")) + if not candidates: + raise DumpToolError(f"No *.{stage} file found under {path}") + if len(candidates) > 1: + names = "\n".join(f" - {candidate.name}" for candidate in candidates[:10]) + more = "" if len(candidates) <= 10 else f"\n ... (+{len(candidates) - 10} more)" + raise DumpToolError( + f"Multiple *.{stage} files found under {path}; pass --base-name to disambiguate.\n" + f"{names}{more}" + ) + return candidates[0] + + +def format_block_lines( + block: FunctionBlock, line_numbers: bool, grep: Optional[re.Pattern[str]], context: int +) -> List[str]: + if grep is None: + if not line_numbers: + return list(block.lines) + return [ + f"{block.start_line + index}: {line}" + for index, line in enumerate(block.lines) + ] + + marks = [False] * len(block.lines) + for index, line in enumerate(block.lines): + if grep.search(line): + start = max(0, index - context) + end = min(len(block.lines), index + context + 1) + for mark_index in range(start, end): + marks[mark_index] = True + + if not any(marks): + return [] + + output: List[str] = [] + previous_selected = False + for index, (line, selected) in enumerate(zip(block.lines, marks)): + if selected: + prefix = f"{block.start_line + index}: " if line_numbers else "" + output.append(f"{prefix}{line}") + elif previous_selected: + output.append("--") + previous_selected = selected + + while output and output[-1] == "--": + output.pop() + return output + + +def parse_register_preferences(block: FunctionBlock) -> Dict[int, str]: + prefs: Dict[int, str] = {} + for line in block.lines: + match = REGISTER_PREF_RE.match(line.strip()) + if not match: + continue + reg = int(match.group(1)) + prefs[reg] = match.group(2).rstrip(".").strip() + return prefs + + +def parse_register_assignments(block: FunctionBlock) -> Dict[int, str]: + assignments: Dict[int, str] = {} + for line in block.lines: + match = REGISTER_ASSIGN_RE.match(line.strip()) + if not match: + continue + assignments[int(match.group(1))] = match.group(2).strip() + return assignments + + +def print_register_change_summary(left: FunctionBlock, right: FunctionBlock) -> None: + left_prefs = parse_register_preferences(left) + right_prefs = parse_register_preferences(right) + left_assign = parse_register_assignments(left) + right_assign = parse_register_assignments(right) + + pref_changes = sorted( + reg + for reg in set(left_prefs) | set(right_prefs) + if left_prefs.get(reg) != right_prefs.get(reg) + ) + assign_changes = sorted( + reg + for reg in set(left_assign) | set(right_assign) + if left_assign.get(reg) != right_assign.get(reg) + ) + + if not pref_changes and not assign_changes: + return + + print("Register summary:", flush=True) + if pref_changes: + print(" Preference changes:", flush=True) + for reg in pref_changes: + print( + f" r{reg}: {left_prefs.get(reg, '')} -> " + f"{right_prefs.get(reg, '')}", + flush=True, + ) + if assign_changes: + print(" Final assignments:", flush=True) + for reg in assign_changes: + print( + f" r{reg}: {left_assign.get(reg, '')} -> " + f"{right_assign.get(reg, '')}", + flush=True, + ) + print(flush=True) + + +def command_command(args: argparse.Namespace) -> None: + info = resolve_compile_info(args.unit) + base_name = args.base_name or derived_base_name(info) + ii_path = Path(args.ii_path) if args.ii_path else Path(f"{base_name}.ii") + dumpbase = Path(args.dumpbase) if args.dumpbase else Path(base_name) + asm_path = Path(args.asm_path) if args.asm_path else Path(f"{base_name}.s") + + preprocess_cmd = derive_preprocess_command(info, ii_path) + cc1plus_cmd = derive_cc1plus_command(info, ii_path, dumpbase, asm_path) + + print_section(f"ProDG command: {args.unit}") + print(f"Source: {info.source_path}", flush=True) + print(f"Target: {info.target_path}", flush=True) + print(flush=True) + print("ngccc:", flush=True) + print(info.compile_shell, flush=True) + print(flush=True) + print("preprocess:", flush=True) + print(shlex.join(preprocess_cmd), flush=True) + print(flush=True) + print("cc1plus:", flush=True) + print(shlex.join(cc1plus_cmd), flush=True) + + +def command_dump(args: argparse.Namespace) -> None: + info = resolve_compile_info(args.unit) + out_dir = Path(args.out_dir).resolve() + ensure_output_dir(out_dir, force=args.force) + + base_name = args.base_name or derived_base_name(info) + ii_path = out_dir / f"{base_name}.ii" + asm_path = out_dir / f"{base_name}.s" + dumpbase = out_dir / base_name + log_path = out_dir / f"{base_name}.log" + + preprocess_cmd = derive_preprocess_command(info, ii_path) + cc1plus_cmd = derive_cc1plus_command(info, ii_path, dumpbase, asm_path) + + print_section(f"Dumping ProDG state: {args.unit}") + print(f"Output directory: {out_dir}", flush=True) + if args.print_commands: + print(flush=True) + print("preprocess:", flush=True) + print(shlex.join(preprocess_cmd), flush=True) + print(flush=True) + print("cc1plus:", flush=True) + print(shlex.join(cc1plus_cmd), flush=True) + print(flush=True) + + run_stream(preprocess_cmd) + cc1_result = subprocess.run(cc1plus_cmd, cwd=ROOT_DIR, text=True, capture_output=True) + log_text = "" + if cc1_result.stdout: + log_text += cc1_result.stdout + if cc1_result.stderr: + if log_text: + log_text += "\n" + log_text += cc1_result.stderr + log_path.write_text(log_text) + if cc1_result.returncode != 0: + raise DumpToolError( + format_subprocess_error( + cc1plus_cmd, cc1_result.returncode, cc1_result.stdout, cc1_result.stderr + ) + ) + + generated = sorted(path.name for path in out_dir.glob(f"{base_name}.*")) + print("Generated files:", flush=True) + for name in generated: + print(f" - {name}", flush=True) + print(f"Log: {log_path}", flush=True) + + +def command_extract(args: argparse.Namespace) -> None: + dump_path = resolve_stage_file(args.path, args.stage, args.base_name) + blocks = load_function_blocks(dump_path) + block = choose_block(blocks, args.function, exact=args.exact) + grep = re.compile(args.grep) if args.grep else None + lines = format_block_lines( + block, + line_numbers=args.line_numbers, + grep=grep, + context=args.context, + ) + + print_section(f"{dump_path.name}: {block.header}") + if grep and not lines: + print(f"No matches for {args.grep!r} inside {block.header}", flush=True) + return + print("\n".join(lines), flush=True) + + +def command_diff(args: argparse.Namespace) -> None: + stages = parse_stages(args.stages) + for stage in stages: + left_path = resolve_stage_file(args.left, stage, args.left_base_name) + right_path = resolve_stage_file(args.right, stage, args.right_base_name) + left_block = choose_block( + load_function_blocks(left_path), args.function, exact=args.exact + ) + right_block = choose_block( + load_function_blocks(right_path), args.function, exact=args.exact + ) + + print_section(f"{stage.upper()} diff: {left_block.header}") + if stage == "lreg": + print_register_change_summary(left_block, right_block) + + diff_lines = list( + difflib.unified_diff( + left_block.lines, + right_block.lines, + fromfile=str(left_path), + tofile=str(right_path), + n=args.context, + lineterm="", + ) + ) + if not diff_lines: + print("No differences.", flush=True) + continue + print("\n".join(diff_lines), flush=True) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Generate and compare ProDG compiler-state dumps using the exact " + "ngccc command recovered from ninja." + ) + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + command = subparsers.add_parser( + "command", + help="Show the recovered ngccc command plus derived preprocess/cc1plus invocations", + ) + command.add_argument("-u", "--unit", required=True, help="objdiff unit name") + command.add_argument( + "--base-name", + help="base filename used when showing derived dump outputs (default: source stem)", + ) + command.add_argument("--ii-path", help="override the displayed preprocessed .ii path") + command.add_argument("--asm-path", help="override the displayed assembly output path") + command.add_argument("--dumpbase", help="override the displayed cc1plus -dumpbase path") + command.set_defaults(func=command_command) + + dump = subparsers.add_parser( + "dump", + help="Run ngccc -E and cc1plus -da for one unit into a dump directory", + ) + dump.add_argument("-u", "--unit", required=True, help="objdiff unit name") + dump.add_argument( + "-o", + "--out-dir", + required=True, + help="directory that will receive the .ii/.s/.rtl/.greg/.lreg dump files", + ) + dump.add_argument( + "--base-name", + help="base filename for generated dumps (default: source stem)", + ) + dump.add_argument( + "--force", + action="store_true", + help="allow writing into a non-empty output directory", + ) + dump.add_argument( + "--print-commands", + action="store_true", + help="print the derived preprocess and cc1plus commands before running them", + ) + dump.set_defaults(func=command_dump) + + extract = subparsers.add_parser( + "extract", + help="Extract one function block from a dump file or dump directory", + ) + extract.add_argument( + "path", + help="dump file path or dump directory produced by this tool", + ) + extract.add_argument( + "--stage", + default="lreg", + help="dump stage when PATH is a directory (default: lreg)", + ) + extract.add_argument( + "--base-name", + help="base filename used to disambiguate PATH when it contains multiple dump sets", + ) + extract.add_argument( + "-f", + "--function", + required=True, + help="function header query; exact or substring match", + ) + extract.add_argument( + "--exact", + action="store_true", + help="require an exact function-header match", + ) + extract.add_argument( + "--grep", + help="only print lines matching this regex, plus --context lines of surrounding dump text", + ) + extract.add_argument( + "-C", + "--context", + type=int, + default=2, + help="context lines to keep around --grep matches (default: 2)", + ) + extract.add_argument( + "--line-numbers", + action="store_true", + help="prefix output lines with their original dump file line numbers", + ) + extract.set_defaults(func=command_extract) + + diff = subparsers.add_parser( + "diff", + help="Diff one function across two dump files or dump directories", + ) + diff.add_argument("left", help="left dump file or dump directory") + diff.add_argument("right", help="right dump file or dump directory") + diff.add_argument( + "-f", + "--function", + required=True, + help="function header query; exact or substring match", + ) + diff.add_argument( + "--exact", + action="store_true", + help="require an exact function-header match", + ) + diff.add_argument( + "--stages", + default=",".join(DEFAULT_STAGES), + help=f"comma-separated dump stages to diff (default: {','.join(DEFAULT_STAGES)})", + ) + diff.add_argument( + "--left-base-name", + help="base filename used to disambiguate the left dump directory", + ) + diff.add_argument( + "--right-base-name", + help="base filename used to disambiguate the right dump directory", + ) + diff.add_argument( + "-C", + "--context", + type=int, + default=3, + help="unified diff context lines (default: 3)", + ) + diff.set_defaults(func=command_diff) + + return parser + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + try: + args.func(args) + except (DumpToolError, ToolError) as e: + fail(str(e)) + + +if __name__ == "__main__": + main() From e28e08d28cd928240d3d230cf99d1809bf842ef9 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 22:05:32 +0100 Subject: [PATCH 58/71] 99.91307%: improve late UpdateSearchLength floor --- .../Tools/AttribSys/Runtime/Common/AttribClass.cpp | 2 +- .../AttribSys/Runtime/Common/AttribDatabase.cpp | 2 +- .../Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp index 8fdd8e106..4d21a0917 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribClass.cpp @@ -99,7 +99,7 @@ bool Class::AddCollection(Collection *c) { return mPrivates.mCollections.Add(c->GetKey(), c); } -// NON_MATCHING: 98.7% - shared VecHashMap remove path still has ~21B of objdiff after the maxSearch > searchLen loop-header fix made DWARF exact +// NON_MATCHING: 98.6% / 99.6% - best known floor uses the block-scoped late VecHashMap::UpdateSearchLength scan, but the shared searchLen local still lands in r6 instead of r7 bool Class::RemoveCollection(Collection *c) { return mPrivates.mCollections.Remove(c->GetKey()); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index 91583a2de..efc56f466 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -219,7 +219,7 @@ bool Database::AddClass(Class *c) { return mPrivates.mClasses.Add(c->GetKey(), c); } -// NON_MATCHING: 98.5% - second VecHashMap::UpdateSearchLength inline keeps searchLen in r7 instead of r6 +// NON_MATCHING: 98.6% / 99.6% - best known floor uses the block-scoped late VecHashMap::UpdateSearchLength scan, but the shared searchLen local still lands in r6 instead of r7 void Database::RemoveClass(const Class *c) { mPrivates.mClasses.Remove(c->GetKey()); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 4cae8403b..1e4826961 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -249,11 +249,15 @@ template searchLen; searchLen++) { - unsigned int index = Policy::WrapIndex(targetIndex + searchLen, mTableSize, 0); - if (Policy::KeyIndex(mTable[index].Key(), mTableSize, 0) == targetIndex) { - newMaxSearch = searchLen; + unsigned int newMaxSearch; + { + unsigned int searchLen = 1; + newMaxSearch = 0; + for (; maxSearch > searchLen; searchLen++) { + unsigned int index = Policy::WrapIndex(targetIndex + searchLen, mTableSize, 0); + if (Policy::KeyIndex(mTable[index].Key(), mTableSize, 0) == targetIndex) { + newMaxSearch = searchLen; + } } } From 80a6d17c35f8eb8ca9ed2e65577a477a7eeb8d9a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 22:07:15 +0100 Subject: [PATCH 59/71] docs: update zAttribSys endgame notes --- AGENTS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 87d920914..d0a1f544b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -575,18 +575,18 @@ When `VecHashMap::Node::Get()` is written as `return IsValid() ? mPtr : nullptr; ### MaxSearchGreaterFixesRemoveCollectionDwarf TU: zAttribSys | Function: Class::RemoveCollection / VecHashMap::UpdateSearchLength -Writing the second `UpdateSearchLength` search loop as `for (unsigned int searchLen = 1; maxSearch > searchLen; searchLen++)` instead of `searchLen < maxSearch` is a real retained partial win. On `zAttribSys` it makes `Class::RemoveCollection` DWARF-exact, improves that wrapper's objdiff slightly, and nudges the unit text floor from `99.91143%` to `99.9116%`, but it does not help `Database::RemoveClass`. +Writing the second `UpdateSearchLength` search loop as `for (unsigned int searchLen = 1; maxSearch > searchLen; searchLen++)` instead of `searchLen < maxSearch` was the first real retained partial win. On `zAttribSys` it made `Class::RemoveCollection` DWARF-exact, improved that wrapper's objdiff slightly, and nudged the unit text floor from `99.91143%` to `99.9116%`, but it did not help `Database::RemoveClass`. ### SearchFirstHoistIsFalseFriend TU: zAttribSys | Function: Database::RemoveClass / VecHashMap::UpdateSearchLength Hoisting the second-loop `searchLen` declaration above `newMaxSearch` (`unsigned int searchLen = 1; unsigned int newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++)`) is a false friend. On the current endgame floor it improves the **second** `UpdateSearchLength` inline in `Database::RemoveClass` and nudges objdiff up to `98.6%`, but it also hoists `searchLen` out of the anonymous loop block in **both** inlined `UpdateSearchLength` bodies and drops normalized DWARF to about `86.6%`, so do not keep or build on that variant. -### BlockScopedSearchLenTradesOneDebtForAnother +### BlockScopedSearchLenRaisesTextButSharesR6Debt TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength -The block-scoped variant `unsigned int newMaxSearch; { unsigned int searchLen = 1; newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++) ... }` is another false friend. On the current floor it fixes `Database::RemoveClass`'s **second-inline** `newMaxSearch // r31` placement and raises that wrapper to `98.6%`, but it simultaneously reintroduces the old second-inline `searchLen // r7 -> r6` mismatch in **both** wrappers and drops `Class::RemoveCollection` back to `98.6% / 99.6%`, so it is not a real endgame win. -On that higher-text plateau (`99.91307%`, `42B` remaining), a whole batch of "safe" follow-up nudges stayed completely inert: empty `if (Unk2)` / `if (!Unk2)` hooks inside the late loop, `searchLen = searchLen + 0`, `newMaxSearch = newMaxSearch + 0`, `register` hints on `searchLen` / `newMaxSearch`, typed-pointer `IsValid()`, `Move`'s C-style cast, `Key()`'s zero spellings, local `asm("r7")` register variables, and even an empty `asm volatile("" : "+r"(searchLen))` constraint. If you revisit this plateau, skip those low-risk hooks and look for a genuinely different source shape. +The block-scoped variant `unsigned int newMaxSearch; { unsigned int searchLen = 1; newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++) ... }` is the current best-known text floor. On `zAttribSys` it fixes `Database::RemoveClass`'s **second-inline** `newMaxSearch // r31` placement, raises both remaining wrappers to `98.6%`, and lifts the TU to `99.91307%` (`42B` remaining), but it also collapses the remaining debt in **both** wrappers to the same single mismatch: the shared second-inline `searchLen // r7 -> r6` register-home swap. +On that `99.91307%` plateau, a whole batch of "safe" follow-up nudges stayed completely inert: empty `if (Unk2)` / `if (!Unk2)` hooks inside the late loop, `searchLen = searchLen + 0`, `newMaxSearch = newMaxSearch + 0`, `register` hints on `searchLen` / `newMaxSearch`, typed-pointer `IsValid()`, `Move`'s C-style cast, `Key()`'s zero spellings, local `asm("r7")` register variables, and even an empty `asm volatile("" : "+r"(searchLen))` constraint. If you revisit this plateau, skip those low-risk hooks and look for a genuinely different source shape. ### PreincrementLessThanFixesHeaderButMovesGlobalRegs @@ -611,7 +611,7 @@ If delete-path DWARF keeps collapsing to an empty unsized `operator delete()` he ### RegisterAllocatorTieBreakDeadEnd TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass -If two near-matching functions differ only because the same inlined helper chain lands long-lived locals in different caller-saved registers, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, the retained `maxSearch > searchLen` rewrite moved the live endgame debt into the **second** `UpdateSearchLength` inline reached from `RemoveIndex`'s empty follow-up loop: `Class::RemoveCollection` is left with the shared late-loop compare/branch shape, while `Database::RemoveClass` also wants `newMaxSearch // r31` where the rebuild keeps it in `r30`. This survived 300+ source experiments plus later focused sweeps of `RemoveIndex` statement order, `FindIndex` local declaration order, wrapper-result liveness, first-call shapes, second-loop declaration/scope variants, second-call expression sugar, `UpdateSearchLength` prelude condition forms, `register` hints on params/locals, per-instantiation `UpdateSearchLength` specializations with identical bodies, and combination searches across individually neutral source toggles. Temporary compiler-side probing was no better: single-flag ProDG toggles around `gcse`/scheduling/CSE/regmove/caller-saves/thread-jumps/delayed-branch either regressed or produced no change, local register variables did not reserve hard registers, and explicit `UpdateSearchLength` specialization was catastrophic even with an identical body. One real exception is the retained `maxSearch > searchLen` loop-header rewrite above: it makes `Class::RemoveCollection` DWARF-exact and bumps the unit floor slightly, but the remaining `RemoveCollection` / `RemoveClass` debt still behaves like the same allocator tie-break family. Once the remaining diff has collapsed to this kind of isolated register swap and DWARF locals/inlining already match, stop attacking each caller separately. Document the functions as `NON_MATCHING`, note the shared inlined root cause, and only revisit the area if you have a genuinely new source shape or stronger compiler evidence than the dead ends above. +If two near-matching functions differ only because the same inlined helper chain lands long-lived locals in different caller-saved registers, treat it as a likely ProDG/GCC 2.95 register-allocation tie-break, not a normal source mismatch. In `zAttribSys`, the earlier retained `maxSearch > searchLen` rewrite moved the live endgame debt into the **second** `UpdateSearchLength` inline reached from `RemoveIndex`'s empty follow-up loop, and the current retained block-scoped floor collapses the remaining debt even further: **both** `Class::RemoveCollection` and `Database::RemoveClass` now fail on the same shared `searchLen // r7 -> r6` swap in that late inline. This survived 300+ source experiments plus later focused sweeps of `RemoveIndex` statement order, `FindIndex` local declaration order, wrapper-result liveness, first-call shapes, second-loop declaration/scope variants, second-call expression sugar, `UpdateSearchLength` prelude condition forms, `register` hints on params/locals, per-instantiation `UpdateSearchLength` specializations with identical bodies, and combination searches across individually neutral source toggles. Temporary compiler-side probing was no better: single-flag ProDG toggles around `gcse`/scheduling/CSE/regmove/caller-saves/thread-jumps/delayed-branch either regressed or produced no change, local register variables did not reserve hard registers, empty asm constraints on the loop-carried local were inert, and explicit `UpdateSearchLength` specialization was catastrophic even with an identical body. Once the remaining diff has collapsed to this kind of isolated shared register-home swap, stop attacking each caller separately. Document the wrappers as `NON_MATCHING`, note the shared inlined root cause, and only revisit the area if you have a genuinely new source shape or stronger compiler evidence than the dead ends above. ### NamedRodataForInlinedAllocatorStrings TU: zAttribSys | Function: DatabaseExportPolicy::Initialize From 26091278739837d07d4a7f8118ddb993d415c83b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 22:20:10 +0100 Subject: [PATCH 60/71] tools: extend prodg_dump summaries --- AGENTS.md | 21 ++- tools/prodg_dump.py | 370 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 380 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d0a1f544b..965134c02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -263,23 +263,34 @@ file+line count is stricter and mainly useful as a secondary hint, not as the ma When you need the exact ProDG compiler state for one unit, prefer this helper over reconstructing long `ngccc` / `cc1plus` command lines by hand. It recovers the real `ngccc.exe` invocation from `ninja -t commands`, derives the matching preprocess step, -runs `cc1plus.exe -da`, and can extract or diff one function across dump sets. +runs `cc1plus.exe -da`, and can extract, summarize, or diff one function across dump +sets. ```sh python tools/prodg_dump.py command -u main/Speed/Indep/SourceLists/zAttribSys python tools/prodg_dump.py dump -u main/Speed/Indep/SourceLists/zAttribSys -o /tmp/zattrib_base python tools/prodg_dump.py extract /tmp/zattrib_base --stage lreg \ -f 'VecHashMap::UpdateSearchLength' +python tools/prodg_dump.py summary /tmp/zattrib_base --stage rtl \ + -f 'VecHashMap::UpdateSearchLength' python tools/prodg_dump.py diff /tmp/zattrib_dumps /tmp/zattrib_dumps \ --left-base-name base --right-base-name preinc --stages lreg,greg,rtl \ -f 'VecHashMap::UpdateSearchLength' +python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_dump /tmp/zattrib_block_dump \ + --exact --summary-only --stages rtl,greg,lreg \ + -f 'unsigned int VecHashMap::UpdateSearchLength(unsigned int, unsigned int)' ``` Use `extract --grep ... -C ` when you only want a few interesting lines inside a -function block, such as stack-slot references or one pseudo register family. `diff` -prints a unified diff for each requested stage, and for `lreg` it also summarizes -register-preference and final hard-register assignment changes so allocator/regclass -shifts stand out immediately. +function block, such as stack-slot references or one pseudo register family. `summary` +prints one function's user pseudos, hard-register refs, frame-slot traffic, and compare +signatures for a given stage. `diff --summary-only` is the quickest way to compare two +variants structurally without drowning in full unified diffs; it highlights changed +frame-slot counts and compare operand/order signatures, while plain `diff` still prints +the raw stage diff underneath. `diff --skip-missing` is useful when one side is a partial +saved dump set that only contains some stages or functions. For `lreg`, `diff` also +summarizes register-preference and final hard-register assignment changes so +allocator/regclass shifts stand out immediately. When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work diff --git a/tools/prodg_dump.py b/tools/prodg_dump.py index 5ce50e910..52974bee9 100644 --- a/tools/prodg_dump.py +++ b/tools/prodg_dump.py @@ -37,6 +37,19 @@ FUNCTION_HEADER_RE = re.compile(r"^;; Function (.+)$") REGISTER_PREF_RE = re.compile(r"^Register (\d+) used .*; pref (.+)$") REGISTER_ASSIGN_RE = re.compile(r"^;; Register (\d+) in ([^.]+)\.$") +USER_PSEUDO_RE = re.compile( + r"\(reg/(?P[a-z/]+):(?P[A-Za-z0-9_]+) (?P\d+)(?: r(?P\d+))?\)" +) +HARD_REG_RE = re.compile( + r"\(reg(?:/[a-z/]+)?:(?P[A-Za-z0-9_]+) (?P\d+) r(?P\d+)\)" +) +PLAIN_REG_RE = re.compile(r"\(reg:(?P[A-Za-z0-9_]+) (?P\d+)\)") +FRAME_SLOT_PLUS_RE = re.compile( + r"mem/f:[A-Za-z0-9_]+ \(\s*plus:[A-Za-z0-9_]+ \(\s*reg:SI (?P\d+)\)\s*" + r"\(\s*const_int (?P-?\d+) \[[^]]+\]\)\s*\) 0\)", + re.MULTILINE, +) +FRAME_SLOT_DIRECT_RE = re.compile(r"mem/f:[A-Za-z0-9_]+ \(\s*reg:SI (?P\d+)\) 0\)") DEFAULT_STAGES = ("rtl", "greg", "lreg") @@ -61,6 +74,21 @@ class FunctionBlock: lines: List[str] +@dataclass +class SummaryStats: + count: int + first_line: int + last_line: int + + +@dataclass +class StageSummary: + pseudos: Dict[tuple[int, str, str], SummaryStats] + hard_regs: Dict[int, SummaryStats] + frame_slots: Dict[tuple[int, int], SummaryStats] + compares: Dict[tuple[str, str, str], SummaryStats] + + def print_section(title: str) -> None: print(flush=True) print("=" * 60, flush=True) @@ -363,6 +391,267 @@ def parse_register_assignments(block: FunctionBlock) -> Dict[int, str]: return assignments +def iter_block_entries(block: FunctionBlock) -> List[tuple[int, str]]: + entries: List[tuple[int, str]] = [] + current: List[str] = [] + current_start = block.start_line + + for index, line in enumerate(block.lines): + if not line.strip(): + if current: + entries.append((current_start, "\n".join(current))) + current = [] + continue + if not current: + current_start = block.start_line + index + current.append(line) + + if current: + entries.append((current_start, "\n".join(current))) + return entries + + +def update_summary_stats(mapping: Dict, key, line: int) -> None: + stats = mapping.get(key) + if stats is None: + mapping[key] = SummaryStats(count=1, first_line=line, last_line=line) + return + stats.count += 1 + stats.last_line = line + + +def read_paren_expr(text: str, start_index: int) -> tuple[str, int]: + index = start_index + while index < len(text) and text[index].isspace(): + index += 1 + if index >= len(text) or text[index] != "(": + raise DumpToolError(f"Expected '(' while parsing compare expression: {text[start_index:]}") + + depth = 0 + end = index + while end < len(text): + char = text[end] + if char == "(": + depth += 1 + elif char == ")": + depth -= 1 + if depth == 0: + return text[index : end + 1], end + 1 + end += 1 + raise DumpToolError(f"Unbalanced compare expression: {text[start_index:]}") + + +def simplify_compare_operand(expr: str) -> str: + expr = expr.strip() + + hard_match = HARD_REG_RE.fullmatch(expr) + if hard_match: + flags_match = USER_PSEUDO_RE.fullmatch(expr) + if flags_match: + return ( + f"reg/{flags_match.group('flags')}:{flags_match.group('mode')}" + f"#{flags_match.group('num')}/r{hard_match.group('hard')}" + ) + return f"reg:{hard_match.group('mode')}#{hard_match.group('num')}/r{hard_match.group('hard')}" + + pseudo_match = USER_PSEUDO_RE.fullmatch(expr) + if pseudo_match: + return ( + f"reg/{pseudo_match.group('flags')}:{pseudo_match.group('mode')}" + f"#{pseudo_match.group('num')}" + ) + + plain_reg_match = PLAIN_REG_RE.fullmatch(expr) + if plain_reg_match: + return f"reg:{plain_reg_match.group('mode')}#{plain_reg_match.group('num')}" + + const_match = re.fullmatch(r"\(const_int (-?\d+) \[[^]]+\]\)", expr) + if const_match: + return f"const:{const_match.group(1)}" + + frame_plus_match = FRAME_SLOT_PLUS_RE.fullmatch(expr) + if frame_plus_match: + return ( + f"frame:r{frame_plus_match.group('base')}" + f"{format_offset(int(frame_plus_match.group('offset')))}" + ) + + frame_direct_match = FRAME_SLOT_DIRECT_RE.fullmatch(expr) + if frame_direct_match: + return f"frame:r{frame_direct_match.group('base')}+0x0" + + head_match = re.match(r"\(([A-Za-z0-9_/:+-]+)", expr) + if head_match: + return head_match.group(1) + return expr + + +def parse_compare_signatures(entry_text: str) -> List[tuple[str, str, str]]: + flat = " ".join(entry_text.split()) + signatures: List[tuple[str, str, str]] = [] + search_from = 0 + while True: + index = flat.find("compare:", search_from) + if index < 0: + return signatures + kind_start = index + len("compare:") + kind_end = kind_start + while kind_end < len(flat) and not flat[kind_end].isspace(): + kind_end += 1 + kind = flat[kind_start:kind_end] + left_expr, next_index = read_paren_expr(flat, kind_end) + right_expr, next_index = read_paren_expr(flat, next_index) + signatures.append( + ( + kind, + simplify_compare_operand(left_expr), + simplify_compare_operand(right_expr), + ) + ) + search_from = next_index + + +def summarize_block(block: FunctionBlock) -> StageSummary: + pseudos: Dict[tuple[int, str, str], SummaryStats] = {} + hard_regs: Dict[int, SummaryStats] = {} + frame_slots: Dict[tuple[int, int], SummaryStats] = {} + compares: Dict[tuple[str, str, str], SummaryStats] = {} + + for start_line, entry_text in iter_block_entries(block): + for match in USER_PSEUDO_RE.finditer(entry_text): + key = ( + int(match.group("num")), + match.group("flags"), + match.group("mode"), + ) + update_summary_stats(pseudos, key, start_line) + for match in HARD_REG_RE.finditer(entry_text): + update_summary_stats(hard_regs, int(match.group("hard")), start_line) + for match in FRAME_SLOT_PLUS_RE.finditer(entry_text): + key = (int(match.group("base")), int(match.group("offset"))) + update_summary_stats(frame_slots, key, start_line) + for match in FRAME_SLOT_DIRECT_RE.finditer(entry_text): + key = (int(match.group("base")), 0) + update_summary_stats(frame_slots, key, start_line) + for signature in parse_compare_signatures(entry_text): + update_summary_stats(compares, signature, start_line) + + return StageSummary( + pseudos=pseudos, + hard_regs=hard_regs, + frame_slots=frame_slots, + compares=compares, + ) + + +def format_line_range(stats: SummaryStats) -> str: + if stats.first_line == stats.last_line: + return str(stats.first_line) + return f"{stats.first_line}-{stats.last_line}" + + +def format_offset(offset: int) -> str: + sign = "-" if offset < 0 else "+" + return f"{sign}0x{abs(offset):X}" + + +def print_stage_summary(block: FunctionBlock) -> None: + summary = summarize_block(block) + if not summary.pseudos and not summary.hard_regs and not summary.frame_slots and not summary.compares: + print("No summary data found.", flush=True) + return + + if summary.pseudos: + print("User pseudos:", flush=True) + for reg_num, flags, mode in sorted(summary.pseudos): + stats = summary.pseudos[(reg_num, flags, mode)] + print( + f" - r{reg_num} ({flags}:{mode}): {stats.count} refs " + f"[lines {format_line_range(stats)}]", + flush=True, + ) + if summary.hard_regs: + print("Hard registers:", flush=True) + for reg_num in sorted(summary.hard_regs): + stats = summary.hard_regs[reg_num] + print( + f" - r{reg_num}: {stats.count} refs [lines {format_line_range(stats)}]", + flush=True, + ) + if summary.frame_slots: + print("Frame slots (mem/f):", flush=True) + for base, offset in sorted(summary.frame_slots): + stats = summary.frame_slots[(base, offset)] + print( + f" - base r{base}{format_offset(offset)}: {stats.count} refs " + f"[lines {format_line_range(stats)}]", + flush=True, + ) + if summary.compares: + print("Compare signatures:", flush=True) + for kind, left_operand, right_operand in sorted(summary.compares): + stats = summary.compares[(kind, left_operand, right_operand)] + print( + f" - {kind}: {left_operand} vs {right_operand}: {stats.count} refs " + f"[lines {format_line_range(stats)}]", + flush=True, + ) + + +def print_summary_changes(left: FunctionBlock, right: FunctionBlock) -> None: + left_summary = summarize_block(left) + right_summary = summarize_block(right) + + def changed_counts(left_map: Dict, right_map: Dict) -> List[tuple]: + return sorted( + ( + key, + left_map.get(key).count if left_map.get(key) else 0, + right_map.get(key).count if right_map.get(key) else 0, + ) + for key in set(left_map) | set(right_map) + if (left_map.get(key).count if left_map.get(key) else 0) + != (right_map.get(key).count if right_map.get(key) else 0) + ) + + pseudo_changes = changed_counts(left_summary.pseudos, right_summary.pseudos) + hard_changes = changed_counts(left_summary.hard_regs, right_summary.hard_regs) + frame_changes = changed_counts(left_summary.frame_slots, right_summary.frame_slots) + compare_changes = changed_counts(left_summary.compares, right_summary.compares) + + if not pseudo_changes and not hard_changes and not frame_changes and not compare_changes: + return + + print("Stage summary changes:", flush=True) + if pseudo_changes: + print(" User pseudos:", flush=True) + for (reg_num, flags, mode), left_count, right_count in pseudo_changes: + print( + f" r{reg_num} ({flags}:{mode}): {left_count} -> {right_count}", + flush=True, + ) + if hard_changes: + print(" Hard registers:", flush=True) + for reg_num, left_count, right_count in hard_changes: + print(f" r{reg_num}: {left_count} -> {right_count}", flush=True) + if frame_changes: + print(" Frame slots:", flush=True) + for (base, offset), left_count, right_count in frame_changes: + print( + f" base r{base}{format_offset(offset)}: {left_count} -> {right_count}", + flush=True, + ) + if compare_changes: + print(" Compare signatures:", flush=True) + for (kind, left_operand, right_operand), left_count, right_count in compare_changes: + print( + f" {kind}: {left_operand} vs {right_operand}: " + f"{left_count} -> {right_count}", + flush=True, + ) + print(flush=True) + + def print_register_change_summary(left: FunctionBlock, right: FunctionBlock) -> None: left_prefs = parse_register_preferences(left) right_prefs = parse_register_preferences(right) @@ -495,21 +784,45 @@ def command_extract(args: argparse.Namespace) -> None: print("\n".join(lines), flush=True) +def command_summary(args: argparse.Namespace) -> None: + dump_path = resolve_stage_file(args.path, args.stage, args.base_name) + blocks = load_function_blocks(dump_path) + block = choose_block(blocks, args.function, exact=args.exact) + + print_section(f"{dump_path.name}: {block.header}") + print_stage_summary(block) + + def command_diff(args: argparse.Namespace) -> None: stages = parse_stages(args.stages) for stage in stages: left_path = resolve_stage_file(args.left, stage, args.left_base_name) right_path = resolve_stage_file(args.right, stage, args.right_base_name) - left_block = choose_block( - load_function_blocks(left_path), args.function, exact=args.exact - ) - right_block = choose_block( - load_function_blocks(right_path), args.function, exact=args.exact - ) + try: + left_block = choose_block( + load_function_blocks(left_path), args.function, exact=args.exact + ) + right_block = choose_block( + load_function_blocks(right_path), args.function, exact=args.exact + ) + except DumpToolError as e: + if args.skip_missing: + print_section(f"{stage.upper()} diff: skipped") + print(f"{e}", flush=True) + print(f"Left: {left_path}", flush=True) + print(f"Right: {right_path}", flush=True) + continue + raise DumpToolError( + f"{e}\nStage: {stage}\nLeft: {left_path}\nRight: {right_path}" + ) print_section(f"{stage.upper()} diff: {left_block.header}") if stage == "lreg": print_register_change_summary(left_block, right_block) + if args.summary or args.summary_only: + print_summary_changes(left_block, right_block) + if args.summary_only: + continue diff_lines = list( difflib.unified_diff( @@ -623,6 +936,36 @@ def build_parser() -> argparse.ArgumentParser: ) extract.set_defaults(func=command_extract) + summary = subparsers.add_parser( + "summary", + help="Summarize one function's pseudos, hard regs, and frame-slot usage in a dump", + ) + summary.add_argument( + "path", + help="dump file path or dump directory produced by this tool", + ) + summary.add_argument( + "--stage", + default="rtl", + help="dump stage when PATH is a directory (default: rtl)", + ) + summary.add_argument( + "--base-name", + help="base filename used to disambiguate PATH when it contains multiple dump sets", + ) + summary.add_argument( + "-f", + "--function", + required=True, + help="function header query; exact or substring match", + ) + summary.add_argument( + "--exact", + action="store_true", + help="require an exact function-header match", + ) + summary.set_defaults(func=command_summary) + diff = subparsers.add_parser( "diff", help="Diff one function across two dump files or dump directories", @@ -660,6 +1003,21 @@ def build_parser() -> argparse.ArgumentParser: default=3, help="unified diff context lines (default: 3)", ) + diff.add_argument( + "--summary", + action="store_true", + help="print per-stage pseudo/hard-reg/frame-slot count changes before the diff", + ) + diff.add_argument( + "--summary-only", + action="store_true", + help="print summary deltas only and skip the full unified diff text", + ) + diff.add_argument( + "--skip-missing", + action="store_true", + help="skip stages where the requested function is missing instead of failing", + ) diff.set_defaults(func=command_diff) return parser From c40e92d3344d7bc770aba0a398c2b44bca07aa33 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 22:29:29 +0100 Subject: [PATCH 61/71] tools: add prodg_dump assembly diffs --- AGENTS.md | 14 ++++++++++++- tools/prodg_dump.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 965134c02..a4d5d80c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -279,6 +279,9 @@ python tools/prodg_dump.py diff /tmp/zattrib_dumps /tmp/zattrib_dumps \ python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_dump /tmp/zattrib_block_dump \ --exact --summary-only --stages rtl,greg,lreg \ -f 'unsigned int VecHashMap::UpdateSearchLength(unsigned int, unsigned int)' +python tools/prodg_dump.py diff /tmp/zattrib_block_dump /tmp/zattrib_trial_dump \ + --exact --stages s \ + -f 'UpdateSearchLength__t10VecHashMap5ZUiZQ26Attrib5ClassZQ36Attrib5Class11TablePolicyb0Ui16UiUi' ``` Use `extract --grep ... -C ` when you only want a few interesting lines inside a @@ -290,7 +293,11 @@ frame-slot counts and compare operand/order signatures, while plain `diff` still the raw stage diff underneath. `diff --skip-missing` is useful when one side is a partial saved dump set that only contains some stages or functions. For `lreg`, `diff` also summarizes register-preference and final hard-register assignment changes so -allocator/regclass shifts stand out immediately. +allocator/regclass shifts stand out immediately. The helper now also understands final +assembly (`--stages s`) by extracting blocks from `.type/.size` symbol ranges; use that +when ProDG's late text dumps (`regmove` / `sched` / `sched2`) are empty in this setup. +Assembly-stage queries are by mangled symbol name, not the demangled `;; Function ...` +header used by RTL-style dumps. When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work @@ -599,6 +606,11 @@ TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / Vec The block-scoped variant `unsigned int newMaxSearch; { unsigned int searchLen = 1; newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++) ... }` is the current best-known text floor. On `zAttribSys` it fixes `Database::RemoveClass`'s **second-inline** `newMaxSearch // r31` placement, raises both remaining wrappers to `98.6%`, and lifts the TU to `99.91307%` (`42B` remaining), but it also collapses the remaining debt in **both** wrappers to the same single mismatch: the shared second-inline `searchLen // r7 -> r6` register-home swap. On that `99.91307%` plateau, a whole batch of "safe" follow-up nudges stayed completely inert: empty `if (Unk2)` / `if (!Unk2)` hooks inside the late loop, `searchLen = searchLen + 0`, `newMaxSearch = newMaxSearch + 0`, `register` hints on `searchLen` / `newMaxSearch`, typed-pointer `IsValid()`, `Move`'s C-style cast, `Key()`'s zero spellings, local `asm("r7")` register variables, and even an empty `asm volatile("" : "+r"(searchLen))` constraint. If you revisit this plateau, skip those low-risk hooks and look for a genuinely different source shape. +### SplitSearchLenInitIsAsmIdenticalNoise + +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength +Splitting the retained block-scoped `searchLen` initializer into declaration plus assignment (`unsigned int searchLen; searchLen = 1;`) is a false signal. It can make `verify` and the per-function `decomp-status` estimates wobble slightly, but `python tools/prodg_dump.py diff --stages s` shows the emitted assembly for the shared `UpdateSearchLength` template and for both wrapper symbols is identical to the retained block floor, so do not treat that variant as real progress. + ### PreincrementLessThanFixesHeaderButMovesGlobalRegs TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength diff --git a/tools/prodg_dump.py b/tools/prodg_dump.py index 52974bee9..9a1145c68 100644 --- a/tools/prodg_dump.py +++ b/tools/prodg_dump.py @@ -50,6 +50,10 @@ re.MULTILINE, ) FRAME_SLOT_DIRECT_RE = re.compile(r"mem/f:[A-Za-z0-9_]+ \(\s*reg:SI (?P\d+)\) 0\)") +ASM_WEAK_RE = re.compile(r"^\s*\.weak\s+(\S+)$") +ASM_TYPE_RE = re.compile(r"^\s*\.type\s+(\S+),@function$") +ASM_LABEL_RE = re.compile(r"^(\S+):$") +ASM_SIZE_RE = re.compile(r"^\s*\.size\s+(\S+),") DEFAULT_STAGES = ("rtl", "greg", "lreg") @@ -254,6 +258,9 @@ def parse_stages(value: str) -> List[str]: def load_function_blocks(path: Path) -> List[FunctionBlock]: + if path.suffix == ".s": + return load_assembly_function_blocks(path) + lines = path.read_text(errors="replace").splitlines() blocks: List[FunctionBlock] = [] current_header: Optional[str] = None @@ -287,6 +294,50 @@ def load_function_blocks(path: Path) -> List[FunctionBlock]: return blocks +def load_assembly_function_blocks(path: Path) -> List[FunctionBlock]: + lines = path.read_text(errors="replace").splitlines() + starts: Dict[str, int] = {} + labels: Dict[str, int] = {} + blocks: List[FunctionBlock] = [] + + for index, line in enumerate(lines, start=1): + weak_match = ASM_WEAK_RE.match(line) + if weak_match: + symbol = weak_match.group(1) + starts[symbol] = min(starts.get(symbol, index), index) + continue + + type_match = ASM_TYPE_RE.match(line) + if type_match: + symbol = type_match.group(1) + starts[symbol] = min(starts.get(symbol, index), index) + continue + + label_match = ASM_LABEL_RE.match(line) + if label_match: + symbol = label_match.group(1) + if symbol in starts and symbol not in labels: + labels[symbol] = index + continue + + size_match = ASM_SIZE_RE.match(line) + if size_match: + symbol = size_match.group(1) + if symbol in starts and symbol in labels: + start_line = starts[symbol] + blocks.append( + FunctionBlock( + header=symbol, + start_line=start_line, + lines=lines[start_line - 1 : index], + ) + ) + del starts[symbol] + del labels[symbol] + + return blocks + + def choose_block(blocks: Sequence[FunctionBlock], query: str, exact: bool) -> FunctionBlock: if exact: matches = [block for block in blocks if block.header == query] From 3715afe7caa4ea02522159e155d05deabf104e6e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 24 Mar 2026 23:51:36 +0100 Subject: [PATCH 62/71] 99.91438%: split late searchLen init by Unk2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 23 +- .../Tools/AttribSys/Runtime/VecHashMap64.h | 7 +- tools/prodg_dump.py | 235 +++++++++++++++++- 3 files changed, 251 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a4d5d80c2..adb0b1222 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -263,8 +263,9 @@ file+line count is stricter and mainly useful as a secondary hint, not as the ma When you need the exact ProDG compiler state for one unit, prefer this helper over reconstructing long `ngccc` / `cc1plus` command lines by hand. It recovers the real `ngccc.exe` invocation from `ninja -t commands`, derives the matching preprocess step, -runs `cc1plus.exe -da`, and can extract, summarize, or diff one function across dump -sets. +passes the real optimization / target / warning flags through to `cc1plus.exe` +(`-fdump-unnumbered` included), and can extract, summarize, or diff one function across +dump sets. ```sh python tools/prodg_dump.py command -u main/Speed/Indep/SourceLists/zAttribSys @@ -282,6 +283,9 @@ python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_dump /tmp/zattrib_block_du python tools/prodg_dump.py diff /tmp/zattrib_block_dump /tmp/zattrib_trial_dump \ --exact --stages s \ -f 'UpdateSearchLength__t10VecHashMap5ZUiZQ26Attrib5ClassZQ36Attrib5Class11TablePolicyb0Ui16UiUi' +build/tools/dtk elf disasm build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o /tmp/zattrib_objdisasm.txt +python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_objdisasm.txt /tmp/zattrib_objdisasm.txt \ + -f 'RemoveCollection__Q26Attrib5ClassPQ26Attrib10Collection' ``` Use `extract --grep ... -C ` when you only want a few interesting lines inside a @@ -297,7 +301,13 @@ allocator/regclass shifts stand out immediately. The helper now also understands assembly (`--stages s`) by extracting blocks from `.type/.size` symbol ranges; use that when ProDG's late text dumps (`regmove` / `sched` / `sched2`) are empty in this setup. Assembly-stage queries are by mangled symbol name, not the demangled `;; Function ...` -header used by RTL-style dumps. +header used by RTL-style dumps. Plain `.s` diffs are normalized automatically now: +`diff` strips `.line` / `.debug_srcinfo` scaffolding and renumbers local `.L*` labels so +you see emitted-code movement instead of debug-section churn. It also understands +`dtk elf disasm` text directly: when you pass a disassembly text file with `.fn ...` / +`.endfn ...` blocks, `extract` and `diff` can operate on real object-level symbols +without another ad hoc extractor, and `diff` also normalizes object-local `.L_*` label +tokens so rebuilt-vs-reference wrapper diffs are easier to read. When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work @@ -598,7 +608,7 @@ Writing the second `UpdateSearchLength` search loop as `for (unsigned int search ### SearchFirstHoistIsFalseFriend TU: zAttribSys | Function: Database::RemoveClass / VecHashMap::UpdateSearchLength -Hoisting the second-loop `searchLen` declaration above `newMaxSearch` (`unsigned int searchLen = 1; unsigned int newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++)`) is a false friend. On the current endgame floor it improves the **second** `UpdateSearchLength` inline in `Database::RemoveClass` and nudges objdiff up to `98.6%`, but it also hoists `searchLen` out of the anonymous loop block in **both** inlined `UpdateSearchLength` bodies and drops normalized DWARF to about `86.6%`, so do not keep or build on that variant. +Hoisting the second-loop `searchLen` declaration above `newMaxSearch` (`unsigned int searchLen = 1; unsigned int newMaxSearch = 0; for (; maxSearch > searchLen; searchLen++)`) is a DWARF-only false friend. On real forced rebuilds, direct `dtk elf disasm` diffs show that both wrapper symbols are text-identical to the retained block-scoped floor, so it does **not** buy real code progress. What it does change is DWARF: it hoists `searchLen` out of the anonymous loop block in both inlined `UpdateSearchLength` bodies and drops normalized DWARF to about `86.6%`, so do not keep or build on that variant. ### BlockScopedSearchLenRaisesTextButSharesR6Debt @@ -616,6 +626,11 @@ Splitting the retained block-scoped `searchLen` initializer into declaration plu TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::UpdateSearchLength Writing the late scan as `for (unsigned int searchLen = 0; ++searchLen < maxSearch; )` is the first condition-only rewrite that makes the second `UpdateSearchLength` loop header itself line up: it fixes the `cmplwi r4, 1` fold, restores the `li r7, 1` / `cmplw r7, r4` shape, and also fixes `Database::RemoveClass`'s `newMaxSearch // r31` placement in that late inline. But it is still a false friend overall: it moves both wrappers onto a broader `r6`/`r7` swap family, drops `Class::RemoveCollection` to `98.6% / 99.6%`, leaves `Database::RemoveClass` at `98.5% / 99.6%`, and changes the remaining mismatches rather than removing them. +### ObjectDisasmResolvesWrapperDrift + +TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass +When wrapper `verify` / `objdiff` movement disagrees with `.s`-level or dump-summary comparisons, disassemble the rebuilt object with `build/tools/dtk elf disasm` and diff the `.fn/.endfn` blocks directly with `python tools/prodg_dump.py diff`. On `zAttribSys`, that direct object diff corrected a stale assumption: the commonly revisited `search-first` late-loop variant is object-identical to the retained block-scoped floor in both wrapper symbols and only changes DWARF. Use the object-level symbol diff when you need the truth about wrapper-emitted code, not just helper `.s` output or status churn. + ### Unk2SelectiveLoopBodyHooksAreRealButUnsafe TU: zAttribSys | Function: Class::RemoveCollection / Database::RemoveClass / VecHashMap::RemoveIndex diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 1e4826961..d6e004661 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -251,8 +251,13 @@ template searchLen; searchLen++) { unsigned int index = Policy::WrapIndex(targetIndex + searchLen, mTableSize, 0); if (Policy::KeyIndex(mTable[index].Key(), mTableSize, 0) == targetIndex) { diff --git a/tools/prodg_dump.py b/tools/prodg_dump.py index 9a1145c68..86bc872b1 100644 --- a/tools/prodg_dump.py +++ b/tools/prodg_dump.py @@ -10,6 +10,9 @@ -f 'VecHashMap::UpdateSearchLength' python tools/prodg_dump.py diff /tmp/zattrib_base /tmp/zattrib_trial \ -f 'VecHashMap::UpdateSearchLength' + build/tools/dtk elf disasm build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o /tmp/zattrib_objdisasm.txt + python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_objdisasm.txt /tmp/zattrib_objdisasm.txt \ + -f 'RemoveCollection__Q26Attrib5ClassPQ26Attrib10Collection' """ import argparse @@ -54,6 +57,19 @@ ASM_TYPE_RE = re.compile(r"^\s*\.type\s+(\S+),@function$") ASM_LABEL_RE = re.compile(r"^(\S+):$") ASM_SIZE_RE = re.compile(r"^\s*\.size\s+(\S+),") +ASM_DEBUG_SECTION_RE = re.compile(r"^\s*\.section\s+\.(line|debug_srcinfo)\b") +ASM_PREVIOUS_RE = re.compile(r"^\s*\.previous\b") +ASM_DEBUG_LABEL_RE = re.compile( + r"^(\.L_(?:LC|LE)\d+|\.L_B\d+(?:_e)?|\.L_b\d+|\.L_f\S*_s):$" +) +DTK_FN_RE = re.compile(r"^\s*\.fn\s+(\S+),") +DTK_ENDFN_RE = re.compile(r"^\s*\.endfn\s+(\S+)$") +DTK_LABEL_RE = re.compile(r"^\s*(\.L_[A-Za-z0-9_]+):$") +DTK_LABEL_TOKEN_RE = re.compile(r"\.L_[A-Za-z0-9_]+") +DTK_INSN_RE = re.compile( + r"^\s*/\*\s*[0-9A-Fa-f]{8}\s+[0-9A-Fa-f]{8}\s+" + r"(?P(?:[0-9A-Fa-f]{2}\s+)+)\*/\s*(?P.*)$" +) DEFAULT_STAGES = ("rtl", "greg", "lreg") @@ -214,6 +230,31 @@ def derive_preprocess_command(info: UnitCompileInfo, ii_path: Path) -> List[str] return [*compile_argv[: info.ngccc_index + 1], *filtered] +def derive_cc1plus_args(info: UnitCompileInfo) -> List[str]: + compiler_args = list(info.compile_argv[info.ngccc_index + 1 :]) + filtered: List[str] = [] + + i = 0 + while i < len(compiler_args): + arg = compiler_args[i] + if arg in ("-c", "-E", "-S", "-M", "-MM", "-MD", "-MMD", "-MG", "-MP"): + i += 1 + continue + if arg in ("-o", "-x", "-I", "-D", "-U", "-MF", "-MT", "-MQ", "-include", "-imacros"): + i += 2 + continue + if arg.startswith("-I") or arg.startswith("-D") or arg.startswith("-U"): + i += 1 + continue + if not arg.startswith("-"): + i += 1 + continue + filtered.append(arg) + i += 1 + + return filtered + + def derive_cc1plus_command( info: UnitCompileInfo, ii_path: Path, dumpbase: Path, asm_path: Path ) -> List[str]: @@ -227,10 +268,13 @@ def derive_cc1plus_command( return [ *prefix, str(cc1plus_path), + *derive_cc1plus_args(info), str(ii_path), "-da", + "-fdump-unnumbered", "-dumpbase", str(dumpbase), + "-quiet", "-o", str(asm_path), ] @@ -258,10 +302,12 @@ def parse_stages(value: str) -> List[str]: def load_function_blocks(path: Path) -> List[FunctionBlock]: + lines = path.read_text(errors="replace").splitlines() if path.suffix == ".s": - return load_assembly_function_blocks(path) + return load_assembly_function_blocks(lines) + if any(DTK_FN_RE.match(line) for line in lines): + return load_dtk_disasm_function_blocks(lines) - lines = path.read_text(errors="replace").splitlines() blocks: List[FunctionBlock] = [] current_header: Optional[str] = None current_start = 0 @@ -294,8 +340,7 @@ def load_function_blocks(path: Path) -> List[FunctionBlock]: return blocks -def load_assembly_function_blocks(path: Path) -> List[FunctionBlock]: - lines = path.read_text(errors="replace").splitlines() +def load_assembly_function_blocks(lines: Sequence[str]) -> List[FunctionBlock]: starts: Dict[str, int] = {} labels: Dict[str, int] = {} blocks: List[FunctionBlock] = [] @@ -338,6 +383,161 @@ def load_assembly_function_blocks(path: Path) -> List[FunctionBlock]: return blocks +def load_dtk_disasm_function_blocks(lines: Sequence[str]) -> List[FunctionBlock]: + blocks: List[FunctionBlock] = [] + current_header: Optional[str] = None + current_start = 0 + current_lines: List[str] = [] + + for index, line in enumerate(lines, start=1): + fn_match = DTK_FN_RE.match(line) + if fn_match: + current_header = fn_match.group(1) + current_start = index + current_lines = [line] + continue + + if current_header is not None: + current_lines.append(line) + end_match = DTK_ENDFN_RE.match(line) + if end_match and end_match.group(1) == current_header: + blocks.append( + FunctionBlock( + header=current_header, + start_line=current_start, + lines=current_lines, + ) + ) + current_header = None + current_start = 0 + current_lines = [] + + return blocks + + +def is_dtk_disasm_block(block: FunctionBlock) -> bool: + return bool(block.lines and DTK_FN_RE.match(block.lines[0])) + + +def replace_dtk_labels(text: str, label_map: Dict[str, str]) -> str: + if not label_map: + return text + result = text + for label, replacement in sorted(label_map.items(), key=lambda item: len(item[0]), reverse=True): + result = result.replace(label, replacement) + return result + + +def normalize_dtk_disasm_lines(block: FunctionBlock) -> List[str]: + label_map: Dict[str, str] = {} + next_index = 1 + + def add_label(label: str) -> None: + nonlocal next_index + if label not in label_map: + label_map[label] = f".L_{next_index}" + next_index += 1 + + for line in block.lines: + label_match = DTK_LABEL_RE.match(line) + if label_match: + add_label(label_match.group(1)) + for label in DTK_LABEL_TOKEN_RE.findall(line): + add_label(label) + + normalized: List[str] = [] + for line in block.lines: + label_match = DTK_LABEL_RE.match(line) + if label_match: + normalized.append(f"{label_map[label_match.group(1)]}:") + continue + + insn_match = DTK_INSN_RE.match(line) + if insn_match: + byte_text = " ".join(insn_match.group("bytes").split()) + asm_text = replace_dtk_labels(insn_match.group("asm"), label_map) + normalized.append(f"/* {byte_text} */\t{asm_text}") + continue + + normalized.append(replace_dtk_labels(line, label_map)) + + return normalized + + +def replace_asm_labels(text: str, label_map: Dict[str, str]) -> str: + if not label_map: + return text + result = text + for label, replacement in sorted(label_map.items(), key=lambda item: len(item[0]), reverse=True): + result = result.replace(label, replacement) + return result + + +def is_debug_asm_label(line: str) -> bool: + return bool(ASM_DEBUG_LABEL_RE.match(line)) + + +def normalize_assembly_lines(block: FunctionBlock) -> List[str]: + label_map: Dict[str, str] = {} + next_index = 1 + + i = 0 + while i < len(block.lines): + line = block.lines[i] + if ASM_DEBUG_SECTION_RE.match(line): + i += 1 + while i < len(block.lines) and not ASM_PREVIOUS_RE.match(block.lines[i]): + i += 1 + if i < len(block.lines): + i += 1 + continue + + if is_debug_asm_label(line): + i += 1 + continue + + label_match = ASM_LABEL_RE.match(line) + if label_match: + label = label_match.group(1) + if label.startswith(".L") and label not in label_map: + label_map[label] = f".L_{next_index}" + next_index += 1 + i += 1 + + normalized: List[str] = [] + i = 0 + while i < len(block.lines): + line = block.lines[i] + if ASM_DEBUG_SECTION_RE.match(line): + i += 1 + while i < len(block.lines) and not ASM_PREVIOUS_RE.match(block.lines[i]): + i += 1 + if i < len(block.lines): + i += 1 + continue + + if is_debug_asm_label(line): + i += 1 + continue + + label_match = ASM_LABEL_RE.match(line) + if label_match: + label = label_match.group(1) + if label.startswith(".L"): + normalized.append(f"{label_map[label]}:") + else: + normalized.append(line) + i += 1 + continue + + normalized_line = replace_asm_labels(line, label_map) + if normalized_line.strip(): + normalized.append(normalized_line) + i += 1 + + return normalized + + def choose_block(blocks: Sequence[FunctionBlock], query: str, exact: bool) -> FunctionBlock: if exact: matches = [block for block in blocks if block.header == query] @@ -845,10 +1045,18 @@ def command_summary(args: argparse.Namespace) -> None: def command_diff(args: argparse.Namespace) -> None: - stages = parse_stages(args.stages) + left_input = Path(args.left) + right_input = Path(args.right) + direct_files = left_input.is_file() and right_input.is_file() + stages = ["file"] if direct_files else parse_stages(args.stages) + for stage in stages: - left_path = resolve_stage_file(args.left, stage, args.left_base_name) - right_path = resolve_stage_file(args.right, stage, args.right_base_name) + if direct_files: + left_path = left_input + right_path = right_input + else: + left_path = resolve_stage_file(args.left, stage, args.left_base_name) + right_path = resolve_stage_file(args.right, stage, args.right_base_name) try: left_block = choose_block( load_function_blocks(left_path), args.function, exact=args.exact @@ -875,10 +1083,19 @@ def command_diff(args: argparse.Namespace) -> None: if args.summary_only: continue + left_lines = left_block.lines + right_lines = right_block.lines + if is_dtk_disasm_block(left_block) and is_dtk_disasm_block(right_block): + left_lines = normalize_dtk_disasm_lines(left_block) + right_lines = normalize_dtk_disasm_lines(right_block) + elif left_path.suffix == ".s" and right_path.suffix == ".s": + left_lines = normalize_assembly_lines(left_block) + right_lines = normalize_assembly_lines(right_block) + diff_lines = list( difflib.unified_diff( - left_block.lines, - right_block.lines, + left_lines, + right_lines, fromfile=str(left_path), tofile=str(right_path), n=args.context, From 8ee77eda7fc20c6c8cfed8b8e08ac9ba5dcd9bfb Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 00:25:24 +0100 Subject: [PATCH 63/71] 99.91438%: add prodg_dump trace helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 41 +++++---- tools/prodg_dump.py | 220 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 240 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index adb0b1222..3adb49b60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -274,6 +274,8 @@ python tools/prodg_dump.py extract /tmp/zattrib_base --stage lreg \ -f 'VecHashMap::UpdateSearchLength' python tools/prodg_dump.py summary /tmp/zattrib_base --stage rtl \ -f 'VecHashMap::UpdateSearchLength' +python tools/prodg_dump.py trace /tmp/zattrib_base --stage greg \ + -f 'void Attrib::Database::RemoveClass(const Attrib::Class *)' --pseudo 318,319 python tools/prodg_dump.py diff /tmp/zattrib_dumps /tmp/zattrib_dumps \ --left-base-name base --right-base-name preinc --stages lreg,greg,rtl \ -f 'VecHashMap::UpdateSearchLength' @@ -291,23 +293,28 @@ python tools/prodg_dump.py diff /tmp/zattrib_oldfloor_objdisasm.txt /tmp/zattrib Use `extract --grep ... -C ` when you only want a few interesting lines inside a function block, such as stack-slot references or one pseudo register family. `summary` prints one function's user pseudos, hard-register refs, frame-slot traffic, and compare -signatures for a given stage. `diff --summary-only` is the quickest way to compare two -variants structurally without drowning in full unified diffs; it highlights changed -frame-slot counts and compare operand/order signatures, while plain `diff` still prints -the raw stage diff underneath. `diff --skip-missing` is useful when one side is a partial -saved dump set that only contains some stages or functions. For `lreg`, `diff` also -summarizes register-preference and final hard-register assignment changes so -allocator/regclass shifts stand out immediately. The helper now also understands final -assembly (`--stages s`) by extracting blocks from `.type/.size` symbol ranges; use that -when ProDG's late text dumps (`regmove` / `sched` / `sched2`) are empty in this setup. -Assembly-stage queries are by mangled symbol name, not the demangled `;; Function ...` -header used by RTL-style dumps. Plain `.s` diffs are normalized automatically now: -`diff` strips `.line` / `.debug_srcinfo` scaffolding and renumbers local `.L*` labels so -you see emitted-code movement instead of debug-section churn. It also understands -`dtk elf disasm` text directly: when you pass a disassembly text file with `.fn ...` / -`.endfn ...` blocks, `extract` and `diff` can operate on real object-level symbols -without another ad hoc extractor, and `diff` also normalizes object-local `.L_*` label -tokens so rebuilt-vs-reference wrapper diffs are easier to read. +signatures for a given stage. `trace` is the quickest way to follow a few pseudo-register +families through a post-allocation dump: it prints each requested pseudo's current home, +conflict/preference summary, and the matching dump entries, including entries where the +pseudo only survives via its assigned hard register. Use it in `greg`/`lreg` when you are +trying to answer "what kept pseudo 318 on r6 instead of r7?" without hand-grepping a full +function dump. `diff --summary-only` is the quickest way to compare two variants +structurally without drowning in full unified diffs; it highlights changed frame-slot +counts and compare operand/order signatures, while plain `diff` still prints the raw +stage diff underneath. `diff --skip-missing` is useful when one side is a partial saved +dump set that only contains some stages or functions. For `lreg`, `diff` also summarizes +register-preference and final hard-register assignment changes so allocator/regclass +shifts stand out immediately. The helper now also understands final assembly (`--stages s`) +by extracting blocks from `.type/.size` symbol ranges; use that when ProDG's late text +dumps (`regmove` / `sched` / `sched2`) are empty in this setup. Assembly-stage queries are +by mangled symbol name, not the demangled `;; Function ...` header used by RTL-style +dumps. Plain `.s` diffs are normalized automatically now: `diff` strips `.line` / +`.debug_srcinfo` scaffolding and renumbers local `.L*` labels so you see emitted-code +movement instead of debug-section churn. It also understands `dtk elf disasm` text +directly: when you pass a disassembly text file with `.fn ...` / `.endfn ...` blocks, +`extract` and `diff` can operate on real object-level symbols without another ad hoc +extractor, and `diff` also normalizes object-local `.L_*` label tokens so +rebuilt-vs-reference wrapper diffs are easier to read. When working with these tools, do not just work around recurring friction silently. If you notice a clear, safe workflow or tooling improvement that would make future decomp work diff --git a/tools/prodg_dump.py b/tools/prodg_dump.py index 86bc872b1..d14595530 100644 --- a/tools/prodg_dump.py +++ b/tools/prodg_dump.py @@ -8,6 +8,8 @@ python tools/prodg_dump.py dump -u main/Speed/Indep/SourceLists/zAttribSys -o /tmp/zattrib_base python tools/prodg_dump.py extract /tmp/zattrib_base --stage lreg \ -f 'VecHashMap::UpdateSearchLength' + python tools/prodg_dump.py trace /tmp/zattrib_base --stage greg \ + -f 'void Attrib::Database::RemoveClass(const Attrib::Class *)' --pseudo 318,319 python tools/prodg_dump.py diff /tmp/zattrib_base /tmp/zattrib_trial \ -f 'VecHashMap::UpdateSearchLength' build/tools/dtk elf disasm build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o /tmp/zattrib_objdisasm.txt @@ -40,6 +42,9 @@ FUNCTION_HEADER_RE = re.compile(r"^;; Function (.+)$") REGISTER_PREF_RE = re.compile(r"^Register (\d+) used .*; pref (.+)$") REGISTER_ASSIGN_RE = re.compile(r"^;; Register (\d+) in ([^.]+)\.$") +REGISTER_CONFLICT_RE = re.compile(r"^;; (\d+) conflicts: (.+)$") +REGISTER_PREFERENCES_RE = re.compile(r"^;; (\d+) preferences: (.+)$") +REGISTER_DISPOSITION_PAIR_RE = re.compile(r"(\d+)\s+in\s+([^\s]+)") USER_PSEUDO_RE = re.compile( r"\(reg/(?P[a-z/]+):(?P[A-Za-z0-9_]+) (?P\d+)(?: r(?P\d+))?\)" ) @@ -621,17 +626,54 @@ def format_block_lines( return output +def parse_int_list(values: Sequence[str]) -> List[int]: + result: List[int] = [] + seen: set[int] = set() + for value in values: + for chunk in value.split(","): + item = chunk.strip() + if not item: + continue + try: + reg_num = int(item, 10) + except ValueError as e: + raise DumpToolError(f"Invalid register number: {item}") from e + if reg_num not in seen: + seen.add(reg_num) + result.append(reg_num) + return result + + def parse_register_preferences(block: FunctionBlock) -> Dict[int, str]: prefs: Dict[int, str] = {} for line in block.lines: - match = REGISTER_PREF_RE.match(line.strip()) + stripped = line.strip() + match = REGISTER_PREF_RE.match(stripped) if not match: - continue - reg = int(match.group(1)) - prefs[reg] = match.group(2).rstrip(".").strip() + match = REGISTER_PREFERENCES_RE.match(stripped) + if match: + reg = int(match.group(1)) + prefs[reg] = match.group(2).rstrip(".").strip() return prefs +def parse_register_dispositions(block: FunctionBlock) -> Dict[int, str]: + assignments: Dict[int, str] = {} + in_section = False + for line in block.lines: + stripped = line.strip() + if stripped == ";; Register dispositions:": + in_section = True + continue + if not in_section: + continue + if stripped.startswith(";; Hard regs used:"): + break + for match in REGISTER_DISPOSITION_PAIR_RE.finditer(line): + assignments[int(match.group(1))] = match.group(2).strip() + return assignments + + def parse_register_assignments(block: FunctionBlock) -> Dict[int, str]: assignments: Dict[int, str] = {} for line in block.lines: @@ -639,9 +681,20 @@ def parse_register_assignments(block: FunctionBlock) -> Dict[int, str]: if not match: continue assignments[int(match.group(1))] = match.group(2).strip() + assignments.update(parse_register_dispositions(block)) return assignments +def parse_register_conflicts(block: FunctionBlock) -> Dict[int, str]: + conflicts: Dict[int, str] = {} + for line in block.lines: + match = REGISTER_CONFLICT_RE.match(line.strip()) + if not match: + continue + conflicts[int(match.group(1))] = match.group(2).strip() + return conflicts + + def iter_block_entries(block: FunctionBlock) -> List[tuple[int, str]]: entries: List[tuple[int, str]] = [] current: List[str] = [] @@ -903,6 +956,112 @@ def changed_counts(left_map: Dict, right_map: Dict) -> List[tuple]: print(flush=True) +def format_entry_text(entry_text: str, start_line: int, line_numbers: bool) -> str: + lines = entry_text.splitlines() + if not line_numbers: + return "\n".join(lines) + return "\n".join(f"{start_line + index}: {line}" for index, line in enumerate(lines)) + + +def collect_entry_trace_tags( + entry_text: str, + pseudos: Sequence[int], + hard_regs: Sequence[int], + pseudo_homes: Dict[int, int], +) -> List[str]: + tags: List[str] = [] + seen_pseudos: set[int] = set() + seen_home_pseudos: set[int] = set() + seen_hard_regs: set[int] = set() + pseudo_targets = set(pseudos) + hard_targets = set(hard_regs) + home_to_pseudos: Dict[int, List[int]] = {} + for pseudo_reg, hard_reg in pseudo_homes.items(): + home_to_pseudos.setdefault(hard_reg, []).append(pseudo_reg) + + for match in USER_PSEUDO_RE.finditer(entry_text): + reg_num = int(match.group("num")) + if reg_num in pseudo_targets and reg_num not in seen_pseudos: + seen_pseudos.add(reg_num) + tags.append(f"pseudo {reg_num}") + for match in HARD_REG_RE.finditer(entry_text): + hard_reg = int(match.group("hard")) + if hard_reg in hard_targets and hard_reg not in seen_hard_regs: + seen_hard_regs.add(hard_reg) + tags.append(f"hard r{hard_reg}") + for pseudo_reg in home_to_pseudos.get(hard_reg, []): + if pseudo_reg not in seen_home_pseudos: + seen_home_pseudos.add(pseudo_reg) + tags.append(f"pseudo {pseudo_reg} via r{hard_reg}") + return tags + + +def command_trace(args: argparse.Namespace) -> None: + dump_path = resolve_stage_file(args.path, args.stage, args.base_name) + blocks = load_function_blocks(dump_path) + block = choose_block(blocks, args.function, exact=args.exact) + pseudos = parse_int_list(args.pseudo) + hard_regs = parse_int_list(args.hard_reg) + if not pseudos and not hard_regs: + raise DumpToolError("Trace requires at least one --pseudo or --hard-reg value") + + assignments = parse_register_assignments(block) + preferences = parse_register_preferences(block) + conflicts = parse_register_conflicts(block) + pseudo_homes: Dict[int, int] = {} + for reg_num in pseudos: + home = assignments.get(reg_num) + if home is None: + continue + try: + pseudo_homes[reg_num] = int(home, 10) + except ValueError: + continue + + print_section(f"{dump_path.name}: {block.header}") + if pseudos: + print("Pseudo summaries:", flush=True) + for reg_num in pseudos: + home = assignments.get(reg_num, "") + print(f" - pseudo {reg_num}: home {home}", flush=True) + if reg_num in preferences: + print(f" preferences: {preferences[reg_num]}", flush=True) + if reg_num in conflicts: + print(f" conflicts: {conflicts[reg_num]}", flush=True) + if hard_regs: + print("Hard-register traces:", flush=True) + for hard_reg in hard_regs: + assigned = sorted( + reg_num for reg_num, home in assignments.items() if home == str(hard_reg) + ) + assigned_text = ", ".join(str(reg_num) for reg_num in assigned) if assigned else "" + print(f" - hard r{hard_reg}: pseudos {assigned_text}", flush=True) + + matched_entries: List[tuple[int, str, List[str]]] = [] + for start_line, entry_text in iter_block_entries(block): + tags = collect_entry_trace_tags(entry_text, pseudos, hard_regs, pseudo_homes) + if tags: + matched_entries.append((start_line, entry_text, tags)) + + print(flush=True) + if not matched_entries: + print("No matching entries.", flush=True) + return + + limit = args.limit if args.limit and args.limit > 0 else len(matched_entries) + print(f"Matching entries ({min(len(matched_entries), limit)}/{len(matched_entries)}):", flush=True) + for start_line, entry_text, tags in matched_entries[:limit]: + line_count = len(entry_text.splitlines()) + end_line = start_line + line_count - 1 + tag_text = ", ".join(tags) + print(flush=True) + print(f"- lines {start_line}-{end_line} [{tag_text}]", flush=True) + print(format_entry_text(entry_text, start_line, line_numbers=args.line_numbers), flush=True) + if limit < len(matched_entries): + print(flush=True) + print(f"... truncated {len(matched_entries) - limit} additional entries", flush=True) + + def print_register_change_summary(left: FunctionBlock, right: FunctionBlock) -> None: left_prefs = parse_register_preferences(left) right_prefs = parse_register_preferences(right) @@ -1204,6 +1363,59 @@ def build_parser() -> argparse.ArgumentParser: ) extract.set_defaults(func=command_extract) + trace = subparsers.add_parser( + "trace", + help="Trace selected pseudos or hard registers through one function block", + ) + trace.add_argument( + "path", + help="dump file path or dump directory produced by this tool", + ) + trace.add_argument( + "--stage", + default="greg", + help="dump stage when PATH is a directory (default: greg)", + ) + trace.add_argument( + "--base-name", + help="base filename used to disambiguate PATH when it contains multiple dump sets", + ) + trace.add_argument( + "-f", + "--function", + required=True, + help="function header query; exact or substring match", + ) + trace.add_argument( + "--exact", + action="store_true", + help="require an exact function-header match", + ) + trace.add_argument( + "--pseudo", + action="append", + default=[], + help="pseudo register numbers to trace (comma-separated, repeatable)", + ) + trace.add_argument( + "--hard-reg", + action="append", + default=[], + help="hard register numbers to trace (comma-separated, repeatable)", + ) + trace.add_argument( + "--limit", + type=int, + default=20, + help="maximum matching entries to print (default: 20, 0 = no limit)", + ) + trace.add_argument( + "--line-numbers", + action="store_true", + help="prefix traced entry lines with their original dump file line numbers", + ) + trace.set_defaults(func=command_trace) + summary = subparsers.add_parser( "summary", help="Summarize one function's pseudos, hard regs, and frame-slot usage in a dump", From 246de78472c75274fde6974d04c17c3f22b211e3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 01:39:24 +0100 Subject: [PATCH 64/71] 99.925865%: improve RemoveClass late-loop state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index d6e004661..0f88f34c3 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -255,6 +255,9 @@ template Date: Wed, 25 Mar 2026 01:42:02 +0100 Subject: [PATCH 65/71] 99.936035%: improve remove-path inline state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 0f88f34c3..5ecb8f6d1 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -255,9 +255,7 @@ template Date: Wed, 25 Mar 2026 01:43:33 +0100 Subject: [PATCH 66/71] 99.94588%: improve remove-path scan loop shape Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h index 5ecb8f6d1..e0314aa65 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/VecHashMap64.h @@ -259,7 +259,7 @@ template searchLen; searchLen++) { + for (; searchLen < maxSearch; searchLen++) { unsigned int index = Policy::WrapIndex(targetIndex + searchLen, mTableSize, 0); if (Policy::KeyIndex(mTable[index].Key(), mTableSize, 0) == targetIndex) { newMaxSearch = searchLen; From fe16393d57a9f3fa349ab68043a83cbb3344885b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 06:13:12 +0100 Subject: [PATCH 67/71] 100.0%: full text match for zAttribSys Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AttribSys/Runtime/Common/AttribPrivate.h | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index 6b1588b58..f817e3c99 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -205,6 +205,91 @@ class ClassPrivate : public Class { ~CollectionHashMap(); + unsigned int UpdateSearchLengthSameIndex(unsigned int index) { + unsigned int targetIndex = index; + unsigned int freeIndex = index; + unsigned int currentIndex; + if (targetIndex == freeIndex && mTable[targetIndex].MaxSearch() == 0) { + goto special_case; + } + currentIndex = targetIndex; + goto after_special_case; + special_case: + currentIndex = Class::TablePolicy::WrapIndex(targetIndex + mTableSize - mWorstCollision, mTableSize, 0); + { + unsigned int distance = mWorstCollision; + while (mTable[currentIndex].MaxSearch() < distance && distance > 0) { + currentIndex = Class::TablePolicy::WrapIndex(currentIndex + 1, mTableSize, 0); + distance--; + } + if (distance == 0) { + return static_cast(-1); + } + } + after_special_case: + unsigned int maxSearch = mTable[currentIndex].MaxSearch(); + unsigned int worstIndex = Class::TablePolicy::WrapIndex(currentIndex + maxSearch, mTableSize, 0); + targetIndex = currentIndex; + if (mTable[worstIndex].IsValid()) { + Class::TablePolicy::KeyIndex(mTable[worstIndex].Key(), mTableSize, 0); + } + + if (mTable[freeIndex].IsValid()) { + } + + if (freeIndex != worstIndex) { + mTable[freeIndex].Move(mTable[worstIndex]); + } + if (mTable[worstIndex].IsValid()) { + } + + unsigned int newMaxSearch; + { + unsigned int searchLen = 1; + newMaxSearch = 0; + asm("" : : "r"(newMaxSearch)); + for (; searchLen < maxSearch; searchLen++) { + unsigned int loopIndex = Class::TablePolicy::WrapIndex(targetIndex + searchLen, mTableSize, 0); + if (Class::TablePolicy::KeyIndex(mTable[loopIndex].Key(), mTableSize, 0) == targetIndex) { + newMaxSearch = searchLen; + } + } + } + mTable[targetIndex].ResetSearchLength(newMaxSearch); + + if (maxSearch == mWorstCollision && mTable[freeIndex].MaxSearch() < maxSearch && newMaxSearch < maxSearch) { + mWorstCollision = 0; + unsigned int prevWorst; + for (unsigned int i = 0; i < mTableSize && mWorstCollision < maxSearch; i++) { + if (mTable[i].MaxSearch() > mWorstCollision) { + prevWorst = mWorstCollision = mTable[i].MaxSearch(); + } + } + } + + return worstIndex; + } + + Collection *RemoveIndex(unsigned int actualIndex) { + if (!ValidIndex(actualIndex)) { + return nullptr; + } + Collection *result = mTable[actualIndex].Get(); + unsigned int key = mTable[actualIndex].Key(); + mTable[actualIndex].Invalidate(); + mNumEntries--; + + unsigned int freedIndex = UpdateSearchLength(Class::TablePolicy::KeyIndex(key, mTableSize, 0), actualIndex); + for (; freedIndex < mTableSize; freedIndex = UpdateSearchLengthSameIndex(freedIndex)) { + } + return result; + } + + Collection *Remove(unsigned int key) { + unsigned int actualIndex = FindIndex(key); + return RemoveIndex(actualIndex); + } + unsigned int GetNextValidIndex(unsigned int startPoint) const { unsigned int index = startPoint + 1; for (; index < mTableSize && !mTable[index].IsValid(); index++) { @@ -253,6 +338,91 @@ class ClassTable : public VecHashMap(capacity) {} ~ClassTable(); + unsigned int UpdateSearchLengthSameIndex(unsigned int index) { + unsigned int targetIndex = index; + unsigned int freeIndex = index; + unsigned int currentIndex; + if (targetIndex == freeIndex && mTable[targetIndex].MaxSearch() == 0) { + goto special_case; + } + currentIndex = targetIndex; + goto after_special_case; + special_case: + currentIndex = Class::TablePolicy::WrapIndex(targetIndex + mTableSize - mWorstCollision, mTableSize, 0); + { + unsigned int distance = mWorstCollision; + while (mTable[currentIndex].MaxSearch() < distance && distance > 0) { + currentIndex = Class::TablePolicy::WrapIndex(currentIndex + 1, mTableSize, 0); + distance--; + } + if (distance == 0) { + return static_cast(-1); + } + } + after_special_case: + unsigned int maxSearch = mTable[currentIndex].MaxSearch(); + unsigned int worstIndex = Class::TablePolicy::WrapIndex(currentIndex + maxSearch, mTableSize, 0); + targetIndex = currentIndex; + if (mTable[worstIndex].IsValid()) { + Class::TablePolicy::KeyIndex(mTable[worstIndex].Key(), mTableSize, 0); + } + + if (mTable[freeIndex].IsValid()) { + } + + if (freeIndex != worstIndex) { + mTable[freeIndex].Move(mTable[worstIndex]); + } + if (mTable[worstIndex].IsValid()) { + } + + unsigned int newMaxSearch; + { + newMaxSearch = 0; + unsigned int searchLen = 1; + asm("" : : "r"(newMaxSearch)); + for (; searchLen < maxSearch; searchLen++) { + unsigned int loopIndex = Class::TablePolicy::WrapIndex(targetIndex + searchLen, mTableSize, 0); + if (Class::TablePolicy::KeyIndex(mTable[loopIndex].Key(), mTableSize, 0) == targetIndex) { + newMaxSearch = searchLen; + } + } + } + mTable[targetIndex].ResetSearchLength(newMaxSearch); + + if (maxSearch == mWorstCollision && mTable[freeIndex].MaxSearch() < maxSearch && newMaxSearch < maxSearch) { + mWorstCollision = 0; + unsigned int prevWorst; + for (unsigned int i = 0; i < mTableSize && mWorstCollision < maxSearch; i++) { + if (mTable[i].MaxSearch() > mWorstCollision) { + prevWorst = mWorstCollision = mTable[i].MaxSearch(); + } + } + } + + return worstIndex; + } + + Class *RemoveIndex(unsigned int actualIndex) { + if (!ValidIndex(actualIndex)) { + return nullptr; + } + Class *result = mTable[actualIndex].Get(); + unsigned int key = mTable[actualIndex].Key(); + mTable[actualIndex].Invalidate(); + mNumEntries--; + + unsigned int freedIndex = UpdateSearchLength(Class::TablePolicy::KeyIndex(key, mTableSize, 0), actualIndex); + for (; freedIndex < mTableSize; freedIndex = UpdateSearchLengthSameIndex(freedIndex)) { + } + return result; + } + + Class *Remove(unsigned int key) { + unsigned int actualIndex = FindIndex(key); + return RemoveIndex(actualIndex); + } + void operator delete(void *ptr, std::size_t bytes) { Free(ptr, bytes, "Attrib::ClassTable"); } From 4597ae6dce837de8d7d7372bc618ede859e409e8 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 07:02:40 +0100 Subject: [PATCH 68/71] tooling: add raw DWARF subroutine tree helper --- AGENTS.md | 20 ++ tools/dwarf-compare.py | 26 +- tools/dwarf1_subroutine_tree.py | 530 ++++++++++++++++++++++++++++++++ 3 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 tools/dwarf1_subroutine_tree.py diff --git a/AGENTS.md b/AGENTS.md index 3adb49b60..ff651ad09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,6 +258,26 @@ It also compares the debug-line ownership of each `// Range:` block. Treat the strong evidence that an inline body came from the wrong header or owner file. The exact file+line count is stricter and mainly useful as a secondary hint, not as the main gate. +### dwarf1_subroutine_tree.py — Raw DWARF subroutine fallback + +When `dtk dwarf dump` prints `// ERROR: Failed to process tag ...` and +`dwarf-compare.py` cannot find the rebuilt wrapper function, inspect the raw +relocated `.debug` tree directly with: + +```sh +python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName +python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Path/To/TU.o --tag 0xTAG +python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --json +``` + +This prints the nested inline-subroutine / lexical-block ownership tree straight +from the raw MWCC DWARF tags, so it is the fastest way to answer: + +- does the rebuilt top-level wrapper DIE still exist at all? +- which inline owner changed (`VecHashMap<...>::Remove` vs `CollectionHashMap::Remove`, etc.)? +- did a wrapper disappear only because the dumper/parser skipped it, or because the + source really changed the raw DWARF tree? + ### prodg_dump.py — ProDG compiler-state dump helper When you need the exact ProDG compiler state for one unit, prefer this helper over diff --git a/tools/dwarf-compare.py b/tools/dwarf-compare.py index 50b2a6351..6c6837b0e 100644 --- a/tools/dwarf-compare.py +++ b/tools/dwarf-compare.py @@ -18,6 +18,7 @@ import json import os import re +import shlex import shutil import sqlite3 import sys @@ -153,6 +154,22 @@ def load_function_blocks( return split_functions(text) +def dump_has_processing_errors(path: str) -> bool: + try: + return "// ERROR: Failed to process tag " in read_text(path) + except OSError: + return False + + +def append_raw_tree_hint(message: str, unit_name: str, function_name: str) -> str: + hint = ( + "\nRaw .debug inspection may help:\n" + f" python tools/dwarf1_subroutine_tree.py -u {shlex.quote(unit_name)} " + f"-f {shlex.quote(function_name)}" + ) + return message + hint + + def find_function_blocks(funcs: Iterable[FunctionBlock], query: str) -> List[FunctionBlock]: candidates = _candidate_func_names(query) matches: List[FunctionBlock] = [] @@ -954,7 +971,14 @@ def main() -> None: ) original_block = choose_function_block(original_funcs, args.function, "original DWARF") - rebuilt_block = choose_function_block(rebuilt_funcs, args.function, "rebuilt DWARF") + try: + rebuilt_block = choose_function_block(rebuilt_funcs, args.function, "rebuilt DWARF") + except DwarfCompareError as exc: + if rebuilt_dwarf_path and dump_has_processing_errors(rebuilt_dwarf_path): + raise DwarfCompareError( + append_raw_tree_hint(str(exc), args.unit, args.function) + ) from exc + raise report = build_report( args.unit, diff --git a/tools/dwarf1_subroutine_tree.py b/tools/dwarf1_subroutine_tree.py new file mode 100644 index 000000000..f8072249c --- /dev/null +++ b/tools/dwarf1_subroutine_tree.py @@ -0,0 +1,530 @@ +#!/usr/bin/env python3 + +""" +Inspect raw MWCC DWARF 1.1 subroutine trees directly from .debug. + +This is useful when `dtk dwarf dump` fails to emit a top-level function block, +but the raw DIE tree is still present in the rebuilt object. + +Examples: + python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' + python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o --tag 0x2A2C8 + python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Database::RemoveClass(Attrib::Class const *)' --json +""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from enum import IntEnum +from io import BytesIO +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional + +from elftools.elf.elffile import ELFFile +from elftools.elf.relocation import RelocationSection + +from _common import find_objdiff_unit, load_objdiff_config, make_abs + + +class TreeError(RuntimeError): + pass + + +class Endian: + def __init__(self, little: bool): + self.little = little + + def u16(self, data: bytes) -> int: + return int.from_bytes(data, "little" if self.little else "big") + + def u32(self, data: bytes) -> int: + return int.from_bytes(data, "little" if self.little else "big") + + def u64(self, data: bytes) -> int: + return int.from_bytes(data, "little" if self.little else "big") + + +class TagKind(IntEnum): + PADDING = 0x0000 + FORMAL_PARAMETER = 0x0005 + GLOBAL_SUBROUTINE = 0x0006 + LABEL = 0x000A + LEXICAL_BLOCK = 0x000B + LOCAL_VARIABLE = 0x000C + COMPILE_UNIT = 0x0011 + SUBROUTINE = 0x0014 + INLINED_SUBROUTINE = 0x001D + + +class AttributeKind(IntEnum): + SIBLING = 0x0010 | 0x2 + LOCATION = 0x0020 | 0x3 + NAME = 0x0030 | 0x8 + FUND_TYPE = 0x0050 | 0x5 + MOD_FUND_TYPE = 0x0060 | 0x3 + USER_DEF_TYPE = 0x0070 | 0x2 + MOD_UD_TYPE = 0x0080 | 0x3 + LOW_PC = 0x0110 | 0x1 + HIGH_PC = 0x0120 | 0x1 + MEMBER = 0x0140 | 0x2 + MW_MANGLED = 0x2000 | 0x8 + SPECIFICATION = 0x02B0 | 0x2 + + +class FormKind(IntEnum): + ADDR = 0x1 + REF = 0x2 + BLOCK2 = 0x3 + BLOCK4 = 0x4 + DATA2 = 0x5 + DATA4 = 0x6 + DATA8 = 0x7 + STRING = 0x8 + + +FORM_MASK = 0xF + + +@dataclass +class Attribute: + kind: int + value: object + + +@dataclass +class Tag: + key: int + kind: int + attributes: List[Attribute] + + +def unit_output_path(unit_name: str) -> str: + config = load_objdiff_config() + unit = find_objdiff_unit(config, unit_name) + if unit is None: + raise TreeError(f"Unit not found in objdiff.json: {unit_name}") + + target = unit.get("base_path") or unit.get("target_path") + if not target: + raise TreeError(f"Unit has no build target in objdiff.json: {unit_name}") + return make_abs(str(target)) or str(target) + + +def normalize_query(name: str) -> str: + bare = name.strip() + paren = bare.find("(") + if paren != -1: + bare = bare[:paren] + return bare.strip() + + +def candidate_names(name: str) -> List[str]: + bare = normalize_query(name) + if not bare: + return [] + parts = bare.split("::") + out: List[str] = [] + for index in range(len(parts)): + candidate = "::".join(parts[index:]).strip() + if candidate and candidate not in out: + out.append(candidate) + return out + + +def read_string(f: BytesIO) -> str: + data = bytearray() + while True: + b = f.read(1) + if not b or b == b"\0": + break + data.extend(b) + return data.decode("latin1") + + +def apply_debug_relocations(elf: ELFFile, section_name: str = ".debug") -> bytes: + debug_section = elf.get_section_by_name(section_name) + if debug_section is None: + raise TreeError(f"Object is missing {section_name}") + data = bytearray(debug_section.data()) + + symtabs = [ + section + for section in elf.iter_sections() + if section.header["sh_type"] in ("SHT_SYMTAB", "SHT_DYNSYM") + ] + + def get_symbol(index: int): + for symtab in symtabs: + if index < symtab.num_symbols(): + return symtab.get_symbol(index) + return None + + for section in elf.iter_sections(): + if not isinstance(section, RelocationSection): + continue + target = elf.get_section(section["sh_info"]) + if target.name != section_name: + continue + + for reloc in section.iter_relocations(): + reloc_type = reloc.entry["r_info_type"] + if reloc_type == 0: + continue + if reloc_type != 1: + raise TreeError( + f"Unhandled {section_name} relocation type {reloc_type} in {section.name}" + ) + symbol = get_symbol(reloc.entry["r_info_sym"]) + addend = reloc.entry.get("r_addend", 0) + value = (symbol["st_value"] if symbol is not None else 0) + addend + offset = reloc["r_offset"] + data[offset : offset + 4] = int(value).to_bytes(4, "big") + + return bytes(data) + + +def read_attribute(f: BytesIO, endian: Endian) -> Attribute: + kind = endian.u16(f.read(2)) + form = FormKind(kind & FORM_MASK) + if form in (FormKind.ADDR, FormKind.REF, FormKind.DATA4): + value = endian.u32(f.read(4)) + elif form == FormKind.DATA2: + value = endian.u16(f.read(2)) + elif form == FormKind.DATA8: + value = endian.u64(f.read(8)) + elif form == FormKind.BLOCK2: + size = endian.u16(f.read(2)) + value = f.read(size) + elif form == FormKind.BLOCK4: + size = endian.u32(f.read(4)) + value = f.read(size) + elif form == FormKind.STRING: + value = read_string(f) + else: + raise TreeError(f"Unhandled attribute form {form}") + return Attribute(kind=kind, value=value) + + +def read_tags(data: bytes, endian: Endian) -> Dict[int, Tag]: + f = BytesIO(data) + tags: Dict[int, Tag] = {} + while f.tell() < len(data): + position = f.tell() + size = endian.u32(f.read(4)) + if size < 8: + if size > 4: + f.seek(size - 4, 1) + tags[position] = Tag(key=position, kind=TagKind.PADDING, attributes=[]) + continue + + kind = endian.u16(f.read(2)) + end = position + size + attributes: List[Attribute] = [] + while f.tell() < end: + attributes.append(read_attribute(f, endian)) + tags[position] = Tag(key=position, kind=kind, attributes=attributes) + return tags + + +def attribute_value(tag: Tag, kind: AttributeKind) -> Optional[object]: + for attr in tag.attributes: + if attr.kind == kind: + return attr.value + return None + + +def tag_name(kind: int) -> str: + try: + return TagKind(kind).name + except ValueError: + return f"0x{kind:X}" + + +def sorted_keys(tags: Dict[int, Tag]) -> List[int]: + return sorted(tags.keys()) + + +def next_sibling_key(tags: Dict[int, Tag], keys: List[int], key: int) -> Optional[int]: + sibling = attribute_value(tags[key], AttributeKind.SIBLING) + if isinstance(sibling, int): + return sibling + index = keys.index(key) + if index + 1 < len(keys): + return keys[index + 1] + return None + + +def child_keys(tags: Dict[int, Tag], keys: List[int], key: int) -> List[int]: + sibling = next_sibling_key(tags, keys, key) + index = keys.index(key) + current = keys[index + 1] if index + 1 < len(keys) else None + out: List[int] = [] + while current is not None and current != sibling: + if tags[current].kind != TagKind.PADDING: + out.append(current) + current = next_sibling_key(tags, keys, current) + return out + + +def member_owner_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: + member_ref = attribute_value(tag, AttributeKind.MEMBER) + if not isinstance(member_ref, int): + return None + owner_tag = tags.get(member_ref) + if owner_tag is None: + return None + owner_name = attribute_value(owner_tag, AttributeKind.NAME) + return owner_name if isinstance(owner_name, str) else None + + +def resolved_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: + name = attribute_value(tag, AttributeKind.NAME) + if isinstance(name, str): + return name + spec_ref = attribute_value(tag, AttributeKind.SPECIFICATION) + if isinstance(spec_ref, int) and spec_ref in tags: + spec_name = attribute_value(tags[spec_ref], AttributeKind.NAME) + if isinstance(spec_name, str): + return spec_name + return None + + +def resolved_owner_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: + owner = member_owner_name(tags, tag) + if owner: + return owner + spec_ref = attribute_value(tag, AttributeKind.SPECIFICATION) + if isinstance(spec_ref, int) and spec_ref in tags: + return member_owner_name(tags, tags[spec_ref]) + return None + + +def qualified_name(tags: Dict[int, Tag], tag: Tag) -> str: + name = resolved_name(tags, tag) or "" + owner = resolved_owner_name(tags, tag) + return f"{owner}::{name}" if owner else name + + +def tag_range(tag: Tag) -> tuple[Optional[int], Optional[int]]: + low = attribute_value(tag, AttributeKind.LOW_PC) + high = attribute_value(tag, AttributeKind.HIGH_PC) + return ( + low if isinstance(low, int) else None, + high if isinstance(high, int) else None, + ) + + +def top_level_subroutines(tags: Dict[int, Tag]) -> List[Tag]: + return [ + tag + for tag in tags.values() + if tag.kind in (TagKind.GLOBAL_SUBROUTINE, TagKind.SUBROUTINE) + and isinstance(attribute_value(tag, AttributeKind.LOW_PC), int) + and isinstance(attribute_value(tag, AttributeKind.HIGH_PC), int) + ] + + +def choose_subroutine(tags: Dict[int, Tag], query: Optional[str], tag_offset: Optional[int]) -> Tag: + if tag_offset is not None: + tag = tags.get(tag_offset) + if tag is None: + raise TreeError(f"No tag at offset 0x{tag_offset:X}") + if tag.kind not in (TagKind.GLOBAL_SUBROUTINE, TagKind.SUBROUTINE): + raise TreeError( + f"Tag 0x{tag_offset:X} is {tag_name(tag.kind)}, not a top-level subroutine" + ) + return tag + + if not query: + raise TreeError("Either --function or --tag is required") + + matches: List[Tag] = [] + exact: List[Tag] = [] + for tag in top_level_subroutines(tags): + name = qualified_name(tags, tag) + if query in name: + exact.append(tag) + if any(candidate in name for candidate in candidate_names(query)): + matches.append(tag) + + selected = exact or matches + if not selected: + raise TreeError(f"Top-level raw subroutine '{query}' not found") + if len(selected) > 1: + names = "\n".join(f" - {qualified_name(tags, tag)}" for tag in selected[:12]) + raise TreeError( + f"Raw subroutine query '{query}' matched multiple functions.\n{names}" + ) + return selected[0] + + +def block_label(tags: Dict[int, Tag], tag: Tag) -> str: + if tag.kind == TagKind.LEXICAL_BLOCK: + return "" + return qualified_name(tags, tag) + + +def render_tree( + tags: Dict[int, Tag], + tag: Tag, + *, + max_depth: Optional[int] = None, + include_non_subroutine: bool = False, +) -> List[Dict[str, Any]]: + keys = sorted_keys(tags) + + def walk(current: Tag, depth: int, out: List[Dict[str, Any]]) -> None: + if max_depth is not None and depth > max_depth: + return + low, high = tag_range(current) + out.append( + { + "tag_offset": current.key, + "tag_kind": tag_name(current.kind), + "depth": depth, + "label": block_label(tags, current), + "owner": resolved_owner_name(tags, current), + "name": resolved_name(tags, current), + "low_pc": low, + "high_pc": high, + "specification": attribute_value(current, AttributeKind.SPECIFICATION), + } + ) + + for child_key in child_keys(tags, keys, current.key): + child = tags[child_key] + if child.kind in (TagKind.INLINED_SUBROUTINE, TagKind.LEXICAL_BLOCK): + walk(child, depth + 1, out) + elif include_non_subroutine and child.kind in ( + TagKind.FORMAL_PARAMETER, + TagKind.LOCAL_VARIABLE, + TagKind.LABEL, + ): + low_pc, high_pc = tag_range(child) + out.append( + { + "tag_offset": child.key, + "tag_kind": tag_name(child.kind), + "depth": depth + 1, + "label": block_label(tags, child), + "owner": resolved_owner_name(tags, child), + "name": resolved_name(tags, child), + "low_pc": low_pc, + "high_pc": high_pc, + "specification": attribute_value(child, AttributeKind.SPECIFICATION), + } + ) + + rows: List[Dict[str, Any]] = [] + walk(tag, 0, rows) + return rows + + +def format_range(low_pc: Optional[int], high_pc: Optional[int]) -> str: + if low_pc is None or high_pc is None: + return "-------- --------" + return f"{low_pc:04X}-{high_pc:04X}" + + +def print_tree(rows: Iterable[Dict[str, Any]]) -> None: + for row in rows: + indent = " " * int(row["depth"]) + spec = row["specification"] + spec_suffix = f" [spec=0x{spec:X}]" if isinstance(spec, int) else "" + print( + f"{indent}{format_range(row['low_pc'], row['high_pc'])} " + f"{row['label']}{spec_suffix}" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Print a raw MWCC DWARF 1.1 inline-subroutine tree directly from " + "an object file's .debug section." + ) + ) + parser.add_argument( + "object", + nargs="?", + help="Path to the object file to inspect. Optional when -u/--unit is used.", + ) + parser.add_argument("-u", "--unit", help="Objdiff unit name to inspect") + parser.add_argument("-f", "--function", help="Top-level function name to inspect") + parser.add_argument( + "--tag", + help="Inspect a specific raw tag offset (hex like 0x2A2C8 or decimal)", + ) + parser.add_argument( + "--max-depth", + type=int, + default=None, + help="Limit the printed tree depth", + ) + parser.add_argument( + "--show-non-subroutine", + action="store_true", + help="Also include parameters, locals, and labels in the tree output", + ) + parser.add_argument( + "--json", + action="store_true", + help="Emit JSON instead of a text tree", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if args.object: + obj_path = Path(args.object) + elif args.unit: + obj_path = Path(unit_output_path(args.unit)) + else: + raise TreeError("Either an object path or --unit is required") + + if not obj_path.is_file(): + raise TreeError(f"Object file not found: {obj_path}") + + tag_offset = None + if args.tag: + tag_offset = int(args.tag, 0) + + with obj_path.open("rb") as f: + elf = ELFFile(f) + data = apply_debug_relocations(elf) + tags = read_tags(data, Endian(elf.little_endian)) + + tag = choose_subroutine(tags, args.function, tag_offset) + rows = render_tree( + tags, + tag, + max_depth=args.max_depth, + include_non_subroutine=args.show_non_subroutine, + ) + + if args.json: + payload = { + "object": str(obj_path), + "selected_tag": tag.key, + "selected_name": qualified_name(tags, tag), + "rows": rows, + } + print(json.dumps(payload, indent=2)) + else: + print(f"Object: {obj_path}") + print(f"Tag: 0x{tag.key:X}") + print(f"Function: {qualified_name(tags, tag)}") + low_pc, high_pc = tag_range(tag) + print(f"Range: {format_range(low_pc, high_pc)}") + print() + print_tree(rows) + + +if __name__ == "__main__": + try: + main() + except TreeError as exc: + raise SystemExit(str(exc)) From dcef5aaa1883ab657799c04f4697a13752e25e6c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 07:09:26 +0100 Subject: [PATCH 69/71] tooling: compare raw DWARF trees against original --- AGENTS.md | 3 + tools/dwarf1_subroutine_tree.py | 224 ++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ff651ad09..0add01d35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -268,6 +268,7 @@ relocated `.debug` tree directly with: python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Path/To/TU.o --tag 0xTAG python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --json +python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --compare-original ``` This prints the nested inline-subroutine / lexical-block ownership tree straight @@ -277,6 +278,8 @@ from the raw MWCC DWARF tags, so it is the fastest way to answer: - which inline owner changed (`VecHashMap<...>::Remove` vs `CollectionHashMap::Remove`, etc.)? - did a wrapper disappear only because the dumper/parser skipped it, or because the source really changed the raw DWARF tree? +- with `--compare-original`, which owner/name rows are actually inserted, replaced, or + missing relative to the original `symbols/Dwarf/functions.nothpp` tree? ### prodg_dump.py — ProDG compiler-state dump helper diff --git a/tools/dwarf1_subroutine_tree.py b/tools/dwarf1_subroutine_tree.py index f8072249c..d428c3d47 100644 --- a/tools/dwarf1_subroutine_tree.py +++ b/tools/dwarf1_subroutine_tree.py @@ -10,12 +10,15 @@ python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o --tag 0x2A2C8 python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Database::RemoveClass(Attrib::Class const *)' --json + python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' --compare-original """ from __future__ import annotations import argparse +import difflib import json +import re from dataclasses import dataclass from enum import IntEnum from io import BytesIO @@ -26,6 +29,7 @@ from elftools.elf.relocation import RelocationSection from _common import find_objdiff_unit, load_objdiff_config, make_abs +from lookup import split_functions class TreeError(RuntimeError): @@ -439,6 +443,203 @@ def print_tree(rows: Iterable[Dict[str, Any]]) -> None: ) +def find_original_function_block(query: str) -> tuple[str, str, str, str]: + dwarf_path = Path(make_abs("symbols/Dwarf/functions.nothpp") or "symbols/Dwarf/functions.nothpp") + try: + text = dwarf_path.read_text(encoding="utf-8", errors="replace") + except OSError as exc: + raise TreeError(f"Failed to read original DWARF dump: {dwarf_path}") from exc + + matches: List[tuple[str, str, str, str]] = [] + exact: List[tuple[str, str, str, str]] = [] + for func in split_functions(text): + sig_line = func[2] + if query in sig_line: + exact.append(func) + if any(candidate in sig_line for candidate in candidate_names(query)): + matches.append(func) + + selected = exact or matches + if not selected: + raise TreeError(f"Original DWARF function '{query}' not found in {dwarf_path}") + if len(selected) > 1: + preview = "\n".join(f" - {func[2]}" for func in selected[:12]) + raise TreeError( + f"Original DWARF query '{query}' matched multiple functions.\n{preview}" + ) + return selected[0] + + +def normalize_original_label(sig_line: str) -> str: + line = sig_line.strip() + if not line: + return "" + + if line.endswith("{}"): + line = line[:-2].rstrip() + if line.endswith("{"): + line = line[:-1].rstrip() + if line.endswith(" const"): + line = line[:-6].rstrip() + + while line.startswith("inline "): + line = line[len("inline ") :].lstrip() + while line.startswith("static "): + line = line[len("static ") :].lstrip() + while line.startswith("inline "): + line = line[len("inline ") :].lstrip() + + paren = line.find("(") + if paren == -1: + return "" + prefix = line[:paren].rstrip() + if not prefix: + return "" + + depth = 0 + for index in range(len(prefix) - 1, -1, -1): + char = prefix[index] + if char == ">": + depth += 1 + elif char == "<": + depth = max(0, depth - 1) + elif depth == 0 and char.isspace(): + candidate = prefix[index + 1 :].strip() + return candidate or "" + return prefix + + +def original_rows_from_block( + block: tuple[str, str, str, str], + function_label: str, + *, + max_depth: Optional[int] = None, +) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [ + { + "tag_kind": "ORIGINAL_FUNCTION", + "depth": 0, + "label": function_label, + "low_pc": int(block[0], 16), + "high_pc": int(block[1], 16), + } + ] + + lines = block[3].splitlines() + range_re = re.compile( + r"^(\s*)// Range:\s+0x([0-9A-Fa-f]+)\s*->\s*0x([0-9A-Fa-f]+)\s*$" + ) + for index, line in enumerate(lines): + match = range_re.match(line) + if not match: + continue + + sig_line = "" + lookahead = index + 1 + while lookahead < len(lines): + candidate = lines[lookahead].strip() + if candidate: + if candidate.startswith("//"): + lookahead += 1 + continue + sig_line = candidate + break + lookahead += 1 + + depth = len(match.group(1)) // 4 + if depth == 0: + continue + if max_depth is not None and depth > max_depth: + continue + rows.append( + { + "tag_kind": "ORIGINAL_RANGE", + "depth": depth, + "label": normalize_original_label(sig_line), + "low_pc": int(match.group(2), 16) & 0xFFFF, + "high_pc": int(match.group(3), 16) & 0xFFFF, + } + ) + return rows + + +def compare_rows( + original_rows: List[Dict[str, Any]], rebuilt_rows: List[Dict[str, Any]] +) -> Dict[str, Any]: + original_keys = [(row["depth"], row["label"]) for row in original_rows] + rebuilt_keys = [(row["depth"], row["label"]) for row in rebuilt_rows] + + mismatches: List[Dict[str, Any]] = [] + matcher = difflib.SequenceMatcher(a=original_keys, b=rebuilt_keys, autojunk=False) + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + continue + mismatches.append( + { + "tag": tag, + "original_range": [i1, i2], + "rebuilt_range": [j1, j2], + "original": [ + { + "depth": row["depth"], + "label": row["label"], + "low_pc": row["low_pc"], + "high_pc": row["high_pc"], + } + for row in original_rows[i1:i2] + ], + "rebuilt": [ + { + "depth": row["depth"], + "label": row["label"], + "low_pc": row["low_pc"], + "high_pc": row["high_pc"], + } + for row in rebuilt_rows[j1:j2] + ], + } + ) + + return { + "original_count": len(original_rows), + "rebuilt_count": len(rebuilt_rows), + "mismatch_count": len(mismatches), + "mismatches": mismatches, + } + + +def print_comparison(compare: Dict[str, Any]) -> None: + print() + print("Original-vs-rebuilt range-tree comparison:") + print( + f" original rows: {compare['original_count']}, rebuilt rows: {compare['rebuilt_count']}, " + f"mismatches: {compare['mismatch_count']}" + ) + if not compare["mismatches"]: + if compare["original_count"] == compare["rebuilt_count"]: + print(" range trees match by depth/label") + else: + print(" shared prefix matches by depth/label, but row counts differ") + return + + for mismatch in compare["mismatches"][:12]: + print( + f" {mismatch['tag']} " + f"original[{mismatch['original_range'][0]}:{mismatch['original_range'][1]}] " + f"rebuilt[{mismatch['rebuilt_range'][0]}:{mismatch['rebuilt_range'][1]}]" + ) + for row in mismatch["original"][:4]: + print(f" - original {row['depth']} {row['label']}") + if len(mismatch["original"]) > 4: + print(f" - ... {len(mismatch['original']) - 4} more original rows") + for row in mismatch["rebuilt"][:4]: + print(f" + rebuilt {row['depth']} {row['label']}") + if len(mismatch["rebuilt"]) > 4: + print(f" + ... {len(mismatch['rebuilt']) - 4} more rebuilt rows") + if compare["mismatch_count"] > 12: + print(f" ... {compare['mismatch_count'] - 12} more mismatches") + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( @@ -473,6 +674,14 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Emit JSON instead of a text tree", ) + parser.add_argument( + "--compare-original", + action="store_true", + help=( + "Also compare the rebuilt raw tree against the original symbols/Dwarf/functions.nothpp " + "range tree for the same function" + ), + ) return parser.parse_args() @@ -504,6 +713,17 @@ def main() -> None: max_depth=args.max_depth, include_non_subroutine=args.show_non_subroutine, ) + compare: Optional[Dict[str, Any]] = None + if args.compare_original: + if not args.function: + raise TreeError("--compare-original requires --function") + original_block = find_original_function_block(args.function) + original_rows = original_rows_from_block( + original_block, + qualified_name(tags, tag), + max_depth=args.max_depth, + ) + compare = compare_rows(original_rows, rows) if args.json: payload = { @@ -512,6 +732,8 @@ def main() -> None: "selected_name": qualified_name(tags, tag), "rows": rows, } + if compare is not None: + payload["compare_original"] = compare print(json.dumps(payload, indent=2)) else: print(f"Object: {obj_path}") @@ -521,6 +743,8 @@ def main() -> None: print(f"Range: {format_range(low_pc, high_pc)}") print() print_tree(rows) + if compare is not None: + print_comparison(compare) if __name__ == "__main__": From 26a46d162118801223452a1e47db043ced8448ae Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 11:16:03 +0100 Subject: [PATCH 70/71] tools: parse original non-subroutine DWARF rows Teach dwarf1_subroutine_tree to parse original params and locals from functions.nothpp when --show-non-subroutine is enabled so deep original-vs-rebuilt comparisons are meaningful. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dwarf1_subroutine_tree.py | 187 +++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 3 deletions(-) diff --git a/tools/dwarf1_subroutine_tree.py b/tools/dwarf1_subroutine_tree.py index d428c3d47..099468b28 100644 --- a/tools/dwarf1_subroutine_tree.py +++ b/tools/dwarf1_subroutine_tree.py @@ -11,6 +11,14 @@ python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Speed/Indep/SourceLists/zAttribSys.o --tag 0x2A2C8 python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Database::RemoveClass(Attrib::Class const *)' --json python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' --compare-original + python tools/dwarf1_subroutine_tree.py -u main/Speed/Indep/SourceLists/zAttribSys -f 'Attrib::Class::RemoveCollection(Attrib::Collection *)' --show-non-subroutine + +The default tree intentionally compares by owner + bare name, which is great for fast +owner-drift checks but can hide overload-only mismatches. When an exact-text candidate +adds an overloaded inline helper or otherwise reuses the same base name with different +parameters/locals, use `--show-non-subroutine` (and `--show-mangled` when available) to +inspect the rebuilt formal-parameter/local rows directly before trusting a +`--compare-original` pass. """ from __future__ import annotations @@ -295,6 +303,18 @@ def resolved_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: return None +def resolved_mangled_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: + mangled = attribute_value(tag, AttributeKind.MW_MANGLED) + if isinstance(mangled, str): + return mangled + spec_ref = attribute_value(tag, AttributeKind.SPECIFICATION) + if isinstance(spec_ref, int) and spec_ref in tags: + spec_mangled = attribute_value(tags[spec_ref], AttributeKind.MW_MANGLED) + if isinstance(spec_mangled, str): + return spec_mangled + return None + + def resolved_owner_name(tags: Dict[int, Tag], tag: Tag) -> Optional[str]: owner = member_owner_name(tags, tag) if owner: @@ -391,6 +411,7 @@ def walk(current: Tag, depth: int, out: List[Dict[str, Any]]) -> None: "label": block_label(tags, current), "owner": resolved_owner_name(tags, current), "name": resolved_name(tags, current), + "mangled": resolved_mangled_name(tags, current), "low_pc": low, "high_pc": high, "specification": attribute_value(current, AttributeKind.SPECIFICATION), @@ -415,6 +436,7 @@ def walk(current: Tag, depth: int, out: List[Dict[str, Any]]) -> None: "label": block_label(tags, child), "owner": resolved_owner_name(tags, child), "name": resolved_name(tags, child), + "mangled": resolved_mangled_name(tags, child), "low_pc": low_pc, "high_pc": high_pc, "specification": attribute_value(child, AttributeKind.SPECIFICATION), @@ -432,14 +454,20 @@ def format_range(low_pc: Optional[int], high_pc: Optional[int]) -> str: return f"{low_pc:04X}-{high_pc:04X}" -def print_tree(rows: Iterable[Dict[str, Any]]) -> None: +def print_tree(rows: Iterable[Dict[str, Any]], *, show_mangled: bool = False) -> None: for row in rows: indent = " " * int(row["depth"]) spec = row["specification"] spec_suffix = f" [spec=0x{spec:X}]" if isinstance(spec, int) else "" + mangled = row.get("mangled") + mangled_suffix = ( + f" [mangled={mangled}]" + if show_mangled and isinstance(mangled, str) and mangled + else "" + ) print( f"{indent}{format_range(row['low_pc'], row['high_pc'])} " - f"{row['label']}{spec_suffix}" + f"{row['label']}{spec_suffix}{mangled_suffix}" ) @@ -509,11 +537,95 @@ def normalize_original_label(sig_line: str) -> str: return prefix +def split_top_level_commas(text: str) -> List[str]: + parts: List[str] = [] + depth_angle = 0 + depth_paren = 0 + start = 0 + for index, char in enumerate(text): + if char == "<": + depth_angle += 1 + elif char == ">": + depth_angle = max(0, depth_angle - 1) + elif char == "(": + depth_paren += 1 + elif char == ")": + depth_paren = max(0, depth_paren - 1) + elif char == "," and depth_angle == 0 and depth_paren == 0: + parts.append(text[start:index].strip()) + start = index + 1 + tail = text[start:].strip() + if tail: + parts.append(tail) + return parts + + +def extract_decl_name(line: str) -> Optional[str]: + candidate = re.sub(r"/\*.*?\*/", "", line).split("//", 1)[0].strip() + if not candidate or candidate.startswith("/*"): + return None + if candidate.endswith("{}"): + return None + if candidate.endswith("{"): + candidate = candidate[:-1].rstrip() + if "=" in candidate: + candidate = candidate.split("=", 1)[0].rstrip() + if candidate.endswith(";"): + candidate = candidate[:-1].rstrip() + match = re.search(r"([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?$", candidate) + if not match: + return None + name = match.group(1) + if name in {"const", "volatile", "unsigned", "signed", "struct", "class"}: + return None + return name + + +def original_param_names(sig_line: str) -> List[str]: + line = re.sub(r"/\*.*?\*/", "", sig_line).strip() + if line.endswith("{}"): + line = line[:-2].rstrip() + if line.endswith("{"): + line = line[:-1].rstrip() + + is_static = False + changed = True + while changed: + changed = False + if line.startswith("static "): + line = line[len("static ") :].lstrip() + is_static = True + changed = True + if line.startswith("inline "): + line = line[len("inline ") :].lstrip() + changed = True + + paren = line.find("(") + close = line.rfind(")") + if paren == -1 or close == -1 or close < paren: + return [] + + names: List[str] = [] + if not is_static and "::" in line[:paren]: + names.append("this") + + params_text = line[paren + 1 : close].strip() + if not params_text or params_text == "void": + return names + + for part in split_top_level_commas(params_text): + name = extract_decl_name(part) + if name: + names.append(name) + return names + + def original_rows_from_block( block: tuple[str, str, str, str], function_label: str, *, max_depth: Optional[int] = None, + include_non_subroutine: bool = False, ) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [ { @@ -524,12 +636,47 @@ def original_rows_from_block( "high_pc": int(block[1], 16), } ] + if include_non_subroutine: + for name in original_param_names(block[2]): + rows.append( + { + "tag_kind": "ORIGINAL_PARAM", + "depth": 1, + "label": name, + "low_pc": int(block[0], 16), + "high_pc": int(block[1], 16), + } + ) lines = block[3].splitlines() range_re = re.compile( r"^(\s*)// Range:\s+0x([0-9A-Fa-f]+)\s*->\s*0x([0-9A-Fa-f]+)\s*$" ) for index, line in enumerate(lines): + if include_non_subroutine and line.strip() == "// Local variables": + depth = len(line) - len(line.lstrip(" ")) + local_depth = depth // 4 + lookahead = index + 1 + while lookahead < len(lines): + candidate = lines[lookahead].strip() + if not candidate or candidate.startswith("// Range:"): + break + if candidate.startswith("//"): + lookahead += 1 + continue + name = extract_decl_name(candidate) + if name: + rows.append( + { + "tag_kind": "ORIGINAL_LOCAL", + "depth": local_depth, + "label": name, + "low_pc": 0, + "high_pc": 0, + } + ) + lookahead += 1 + match = range_re.match(line) if not match: continue @@ -560,6 +707,30 @@ def original_rows_from_block( "high_pc": int(match.group(3), 16) & 0xFFFF, } ) + if include_non_subroutine: + for name in original_param_names(sig_line): + rows.append( + { + "tag_kind": "ORIGINAL_PARAM", + "depth": depth + 1, + "label": name, + "low_pc": int(match.group(2), 16) & 0xFFFF, + "high_pc": int(match.group(3), 16) & 0xFFFF, + } + ) + if normalize_original_label(sig_line) == "": + name = extract_decl_name(sig_line) + if name: + rows.append( + { + "tag_kind": "ORIGINAL_LOCAL", + "depth": depth + 1, + "label": name, + "low_pc": int(match.group(2), 16) & 0xFFFF, + "high_pc": int(match.group(3), 16) & 0xFFFF, + } + ) + return rows @@ -669,6 +840,11 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Also include parameters, locals, and labels in the tree output", ) + parser.add_argument( + "--show-mangled", + action="store_true", + help="Also print MWCC mangled names when present", + ) parser.add_argument( "--json", action="store_true", @@ -722,6 +898,7 @@ def main() -> None: original_block, qualified_name(tags, tag), max_depth=args.max_depth, + include_non_subroutine=args.show_non_subroutine, ) compare = compare_rows(original_rows, rows) @@ -739,10 +916,14 @@ def main() -> None: print(f"Object: {obj_path}") print(f"Tag: 0x{tag.key:X}") print(f"Function: {qualified_name(tags, tag)}") + if args.show_mangled: + mangled = resolved_mangled_name(tags, tag) + if mangled: + print(f"Mangled: {mangled}") low_pc, high_pc = tag_range(tag) print(f"Range: {format_range(low_pc, high_pc)}") print() - print_tree(rows) + print_tree(rows, show_mangled=args.show_mangled) if compare is not None: print_comparison(compare) From 69a1c0731370e3613a308cf74305a8f845a7d3fa Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 25 Mar 2026 11:17:56 +0100 Subject: [PATCH 71/71] docs: note raw-tree compare caveats Document the need to use --show-non-subroutine for overload-heavy DWARF checks and note that concrete VecHashMap member specializations are a valid ProDG lane when dependent names are expanded. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 0add01d35..798e39bec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -269,6 +269,7 @@ python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName python tools/dwarf1_subroutine_tree.py build/GOWE69/src/Path/To/TU.o --tag 0xTAG python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --json python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --compare-original +python tools/dwarf1_subroutine_tree.py -u main/Path/To/TU -f FunctionName --show-non-subroutine ``` This prints the nested inline-subroutine / lexical-block ownership tree straight @@ -281,6 +282,12 @@ from the raw MWCC DWARF tags, so it is the fastest way to answer: - with `--compare-original`, which owner/name rows are actually inserted, replaced, or missing relative to the original `symbols/Dwarf/functions.nothpp` tree? +Treat `--compare-original` primarily as an owner/name drift check. By default it compares +owner + bare function names, so overload-only mismatches can still hide behind a clean +depth/label pass. When a candidate adds an overloaded inline helper or reuses the same +base name with a different signature, run `--show-non-subroutine` too and inspect the +rebuilt parameter/local rows directly before trusting the raw-tree result. + ### prodg_dump.py — ProDG compiler-state dump helper When you need the exact ProDG compiler state for one unit, prefer this helper over @@ -588,6 +595,19 @@ register assignments but does NOT affect integer register assignments (and vice - If an inline appears in the DWARF but does not exist in `src/`, deduce its body and add it to the correct header (use `line-lookup` skill to find the header file). +### Concrete template specializations + +- Do not assume ProDG rejects explicit member-function specializations of concrete template + instantiations. Forms like + `template <> inline ReturnType VecHashMap<...>::RemoveIndex(...)` + do compile here. +- When you use that lane, expand dependent names inside the specialized body: + replace `Policy::` with the concrete owner, replace non-type template params like `Unk2` + with literal `true` / `false`, and replace aliases like `T *` / `KeyType` with the + concrete instantiation types. +- Prefer this over owner-specific derived helper classes when you need per-instantiation + source bodies but want to preserve the original `VecHashMap<...>` owner names in DWARF. + --- ## Discovered Matching Patterns