From 47dd0e1c620bfede6ab6e1914d9983d23d7ae5e5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 15:45:14 +0100 Subject: [PATCH 001/172] 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 4ff6a3f6fcfc3adc73abcc7497e984fb122eae0e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 11 Mar 2026 19:54:50 +0100 Subject: [PATCH 002/172] 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 003/172] 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 8c384ccfadecb180350214cc125841cfd6363e64 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 12 Mar 2026 21:06:33 +0100 Subject: [PATCH 004/172] 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 005/172] 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 4453407039a188cf62d4ee3677b0641f340eab85 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 13 Mar 2026 13:30:30 +0100 Subject: [PATCH 006/172] 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 007/172] 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 008/172] 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 009/172] 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 010/172] - 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 011/172] 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 012/172] 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 013/172] 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 014/172] 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 015/172] 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 016/172] 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 017/172] 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 018/172] 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 019/172] 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 020/172] 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 021/172] 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 022/172] 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 a982a7fdfaf694ba458ee2ab940d8299181e8c46 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 22:29:00 +0100 Subject: [PATCH 023/172] 4.0%: reconstruct zPlatform source list and match initial helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/DemoDisc_G.cpp | 9 + .../GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 134 ++++++++++++++ .../GameCube/Src/Ecstasy/TextureInfoPlat.hpp | 6 + src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 165 ++++++++++++++++++ src/Speed/GameCube/Src/Logitech/LGWheels.hpp | 66 +++++++ src/Speed/GameCube/Src/Logitech/Wheels.cpp | 16 ++ src/Speed/GameCube/Src/Platform_G.cpp | 111 ++++++++++++ src/Speed/Indep/SourceLists/zPlatform.cpp | 33 ++++ src/Speed/Indep/Src/Ecstasy/Texture.hpp | 23 +-- src/Speed/Indep/Src/Ecstasy/TextureTypes.hpp | 14 ++ .../Src/Frontend/MemoryCard/MemoryCard.hpp | 5 + src/Speed/Indep/Src/Misc/BuildRegion.hpp | 3 + 12 files changed, 571 insertions(+), 14 deletions(-) create mode 100644 src/Speed/GameCube/Src/Logitech/LGWheels.hpp create mode 100644 src/Speed/Indep/Src/Ecstasy/TextureTypes.hpp diff --git a/src/Speed/GameCube/Src/DemoDisc_G.cpp b/src/Speed/GameCube/Src/DemoDisc_G.cpp index e69de29bb..d9d1622ef 100644 --- a/src/Speed/GameCube/Src/DemoDisc_G.cpp +++ b/src/Speed/GameCube/Src/DemoDisc_G.cpp @@ -0,0 +1,9 @@ +#include "Speed/Indep/Src/Misc/DemoDisc.hpp" + +DemoDiscManager TheDemoDiscManager; + +DemoDiscManager::DemoDiscManager() {} + +void DemoDiscManager::Init(int argc, char **argv) {} + +void DemoDiscManager::SetEndReason(enum DemoDiscEndReason end_reason) {} diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index e69de29bb..d8f60b57a 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -0,0 +1,134 @@ +#include "Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp" +#include "Speed/Indep/Src/Ecstasy/Texture.hpp" +#include "Speed/Indep/bWare/Inc/bMemory.hpp" +#include "Speed/Indep/bWare/Inc/bWare.hpp" + +extern SlotPool *eAnimTextureSlotPool; + +static inline unsigned int Convert16To32(unsigned short entry) { + if (entry & 0x8000) { + unsigned int r = (entry >> 10) & 0x1F; + unsigned int g = (entry >> 5) & 0x1F; + unsigned int b = entry & 0x1F; + return 0xFF000000 | (r << 19) | (g << 11) | (b << 3); + } + + unsigned int a = (entry >> 12) & 0x0F; + unsigned int r = (entry >> 8) & 0x0F; + unsigned int g = (entry >> 4) & 0x0F; + unsigned int b = entry & 0x0F; + return (a << 28) | (r << 20) | (g << 12) | (b << 4); +} + +static inline unsigned short Convert32To16(unsigned int entry) { + unsigned int a = entry >> 24; + unsigned int r = (entry >> 16) & 0xFF; + unsigned int g = (entry >> 8) & 0xFF; + unsigned int b = entry & 0xFF; + + if (a > 0xEF) { + return 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3); + } + + return ((a >> 4) << 12) | ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4); +} + +void TextureInfoPlatInterface::SetPlatInfo(TextureInfoPlatInfo *info) { + this->PlatInfo = info; +} + +void TextureInfoPlatInterface::Init() { + TextureInfo *texture_info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + + bMemSet(&plat_info->ImageInfos, 0, 0x2C); + GXInvalidateTexAll(); + DCFlushRange(texture_info->ImageData, texture_info->BaseImageSize); + DCFlushRange(texture_info->PaletteData, texture_info->PaletteSize); + plat_info->SetImage(texture_info); +} + +void TextureInfoPlatInterface::Close() {} + +void *TextureInfoPlatInterface::LockImage(TextureLockType lock) { + TextureInfo *texture_info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + return texture_info->ImageData; +} + +void TextureInfoPlatInterface::UnlockImage(void *image_lock) {} + +void *TextureInfoPlatInterface::LockPalette(TextureLockType lock) { + TextureInfo *texture_info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + unsigned short *gcPal = static_cast(texture_info->PaletteData); + unsigned int *Pal32 = nullptr; + + if (gcPal) { + Pal32 = new unsigned int[256]; + if (Pal32) { + for (int j = 0; j <= 0xFF; j++) { + Pal32[j] = Convert16To32(gcPal[j]); + } + } + } + + return Pal32; +} + +void TextureInfoPlatInterface::UnlockPalette(void *palette_lock) { + TextureInfo *texture_info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + + if (palette_lock) { + unsigned short *gcPal = static_cast(texture_info->PaletteData); + unsigned int *Pal32 = reinterpret_cast(palette_lock); + + for (int j = 0; j <= 0xFF; j++) { + gcPal[j] = Convert32To16(Pal32[j]); + } + + delete[] Pal32; + } +} + +void *TextureInfoPlatInterface::CreateAnimData() { + TextureInfo *info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + unsigned int *val = reinterpret_cast(bOMalloc(eAnimTextureSlotPool)); + + val[0] = reinterpret_cast(info->ImageData); + val[1] = reinterpret_cast(info->PaletteData); + return val; +} + +void TextureInfoPlatInterface::ReleaseAnimData(void *anim_data) { + bFree(eAnimTextureSlotPool, anim_data); +} + +void TextureInfoPlatInterface::SetAnimData(void *anim_data) { + TextureInfo *info = static_cast(this); + TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + unsigned int *val = reinterpret_cast(anim_data); + + info->ImageData = reinterpret_cast(val[0]); + info->PaletteData = reinterpret_cast(val[1]); + plat_info->SetImage(info); +} + +unsigned char TextureInfoPlatInfo::HasClut() { + unsigned int texture_format = this->Format & 0x7FFFFFFF; + return texture_format >= 8 && texture_format <= 10; +} + +unsigned char TextureInfoPlatInfo::SetImage(TextureInfo *texture_info) { + TextureInfoPlatInfo *plat_info = texture_info->GetPlatInfo(); + + if (!plat_info) { + return 0; + } + + plat_info->SetImage(texture_info->Width, texture_info->Height, texture_info->NumMipMapLevels, plat_info->Format, texture_info->ImageData, + texture_info->PaletteData, texture_info->AlphaUsageType, texture_info->TilableUV); + return 1; +} diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp index 7447e13ff..577dca5cf 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp @@ -6,6 +6,7 @@ #endif #include "Speed/Indep/bWare/Inc/bList.hpp" +#include "Speed/Indep/Src/Ecstasy/TextureTypes.hpp" #include struct TextureInfoPlatInfoOBJ { @@ -22,6 +23,7 @@ struct TextureInfoPlatInfo : public bTNode { unsigned char SetImage(int width, int height, int mip, int format, void *imageData, void *imagePal, int alphaUsageType, int clamp); unsigned char SetImage(struct TextureInfo *texture_info); + unsigned char HasClut(); }; class TextureInfoPlatInterface { @@ -35,6 +37,10 @@ class TextureInfoPlatInterface { void SetPlatInfo(TextureInfoPlatInfo *info); void Init(); void Close(); + void *LockImage(TextureLockType lock); + void UnlockImage(void *image_lock); + void *LockPalette(TextureLockType lock); + void UnlockPalette(void *palette_lock); TextureInfoPlatInfo *GetPlatInfo() { return this->PlatInfo; diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index e69de29bb..e12469c09 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -0,0 +1,165 @@ +#include "LGWheels.hpp" + +struct SpringForceParams { + char offset; + unsigned char saturation; + short coefficient; +}; + +struct ConstantForceParams { + short magnitude; + unsigned short direction; +}; + +struct DamperForceParams { + short coefficient; +}; + +struct RoadEffectParams { + short magnitude; +}; + +struct SurfaceEffectParams { + unsigned char type; + unsigned char magnitude; + unsigned short period; +}; + +static inline Wheels *GetWheels(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x828); +} + +static inline const Wheels *GetWheels(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x828); +} + +static inline SpringForceParams *GetSpringForceParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x167C); +} + +static inline const SpringForceParams *GetSpringForceParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x167C); +} + +static inline ConstantForceParams *GetConstantForceParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x168C); +} + +static inline const ConstantForceParams *GetConstantForceParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x168C); +} + +static inline DamperForceParams *GetDamperForceParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x169C); +} + +static inline const DamperForceParams *GetDamperForceParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x169C); +} + +static inline RoadEffectParams *GetDirtRoadParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16BC); +} + +static inline const RoadEffectParams *GetDirtRoadParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16BC); +} + +static inline RoadEffectParams *GetBumpyRoadParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16C4); +} + +static inline const RoadEffectParams *GetBumpyRoadParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16C4); +} + +static inline RoadEffectParams *GetSlipperyRoadParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16CC); +} + +static inline const RoadEffectParams *GetSlipperyRoadParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16CC); +} + +static inline SurfaceEffectParams *GetSurfaceEffectParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16D4); +} + +static inline const SurfaceEffectParams *GetSurfaceEffectParams(const LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16D4); +} + +bool LGWheels::IsConnected(int channel) { + return GetWheels(this)->IsConnected(channel); +} + +bool LGWheels::ButtonIsPressed(int channel, unsigned long buttonMask) { + return GetWheels(this)->ButtonIsPressed(channel, buttonMask); +} + +bool LGWheels::PedalsConnected(int channel) { + return GetWheels(this)->PedalsConnected(channel); +} + +void LGWheels::StopSpringForce(int channel) { + this->StopForce(channel, 0); +} + +bool LGWheels::SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient) { + const SpringForceParams ¶ms = GetSpringForceParams(this)[channel]; + return params.offset == offset && params.saturation == saturation && params.coefficient == coefficient; +} + +void LGWheels::StopConstantForce(int channel) { + this->StopForce(channel, 1); +} + +bool LGWheels::SameConstantForceParams(int channel, short magnitude, unsigned short direction) { + const ConstantForceParams ¶ms = GetConstantForceParams(this)[channel]; + return params.magnitude == magnitude && params.direction == direction; +} + +void LGWheels::StopDamperForce(int channel) { + this->StopForce(channel, 2); +} + +bool LGWheels::SameDamperForceParams(int channel, short coefficient) { + return GetDamperForceParams(this)[channel].coefficient == coefficient; +} + +void LGWheels::StopDirtRoadEffect(int channel) { + this->StopForce(channel, 5); +} + +bool LGWheels::SameDirtRoadEffectParams(int channel, short magnitude) { + return GetDirtRoadParams(this)[channel].magnitude == magnitude; +} + +void LGWheels::StopBumpyRoadEffect(int channel) { + this->StopForce(channel, 6); +} + +bool LGWheels::SameBumpyRoadEffectParams(int channel, short magnitude) { + return GetBumpyRoadParams(this)[channel].magnitude == magnitude; +} + +void LGWheels::StopSlipperyRoadEffect(int channel) { + this->StopForce(channel, 7); +} + +bool LGWheels::SameSlipperyRoadEffectParams(int channel, short magnitude) { + return GetSlipperyRoadParams(this)[channel].magnitude == magnitude; +} + +void LGWheels::StopSurfaceEffect(int channel) { + this->StopForce(channel, 8); +} + +bool LGWheels::SameSurfaceEffectParams(int channel, unsigned char type, unsigned char magnitude, unsigned short period) { + const SurfaceEffectParams ¶ms = GetSurfaceEffectParams(this)[channel]; + return params.type == type && params.magnitude == magnitude && params.period == period; +} + +void LGWheels::StopCarAirborne(int channel) { + this->StopForce(channel, 9); +} diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp new file mode 100644 index 000000000..fe14fa638 --- /dev/null +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp @@ -0,0 +1,66 @@ +#ifndef GAMECUBE_LOGITECH_LGWHEELS_H +#define GAMECUBE_LOGITECH_LGWHEELS_H + +#ifdef EA_PRAGMA_ONCE_SUPPORTED +#pragma once +#endif + +struct LGPosition { + unsigned short button; + unsigned char misc; + char wheel; + unsigned char accelerator; + unsigned char brake; + char combined; + unsigned char triggerLeft; + unsigned char triggerRight; + char err; +}; + +struct Wheels { + Wheels(); + ~Wheels(); + + bool ButtonIsPressed(int channel, unsigned long buttonMask); + bool IsConnected(int channel); + bool PedalsConnected(int channel); +}; + +struct Force { + bool Playing[8][4]; + unsigned long EffectID[8][4]; +}; + +struct Condition : public Force {}; +struct Constant : public Force {}; +struct Periodic : public Force {}; +struct Ramp : public Force {}; + +struct LGWheels { + LGWheels(); + ~LGWheels(); + + void StopForce(int channel, int forceType); + bool IsConnected(int channel); + bool ButtonIsPressed(int channel, unsigned long buttonMask); + bool PedalsConnected(int channel); + void StopSpringForce(int channel); + bool SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient); + void StopConstantForce(int channel); + bool SameConstantForceParams(int channel, short magnitude, unsigned short direction); + void StopDamperForce(int channel); + bool SameDamperForceParams(int channel, short coefficient); + void StopDirtRoadEffect(int channel); + bool SameDirtRoadEffectParams(int channel, short magnitude); + void StopBumpyRoadEffect(int channel); + bool SameBumpyRoadEffectParams(int channel, short magnitude); + void StopSlipperyRoadEffect(int channel); + bool SameSlipperyRoadEffectParams(int channel, short magnitude); + void StopSurfaceEffect(int channel); + bool SameSurfaceEffectParams(int channel, unsigned char type, unsigned char magnitude, unsigned short period); + void StopCarAirborne(int channel); +}; + +extern LGWheels *plat_lgwheels; + +#endif diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index e69de29bb..658c4138a 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -0,0 +1,16 @@ +#include "LGWheels.hpp" + +bool Wheels::ButtonIsPressed(int channel, unsigned long buttonMask) { + const LGPosition *position = reinterpret_cast(this); + return (position[channel].button & buttonMask) != 0; +} + +bool Wheels::IsConnected(int channel) { + const LGPosition *position = reinterpret_cast(this); + return position[channel].err != 0; +} + +bool Wheels::PedalsConnected(int channel) { + const LGPosition *position = reinterpret_cast(this); + return (position[channel].misc >> 3) & 1; +} diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index e69de29bb..1e34afc62 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -0,0 +1,111 @@ +#include "Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp" +#include "Speed/Indep/Src/Misc/BuildRegion.hpp" +#include "Speed/Indep/Src/Misc/Platform.h" +#include "dolphin.h" + +enum VIDEO_MODE { + MODE_PAL = 0, + MODE_PAL60 = 1, + MODE_NTSC = 2, + NUM_VIDEO_MODES = 3, +}; + +enum eLanguages { + eLANGUAGE_NONE = -1, + eLANGUAGE_FIRST = 0, + eLANGUAGE_ENGLISH = 0, + eLANGUAGE_FRENCH = 1, + eLANGUAGE_GERMAN = 2, + eLANGUAGE_ITALIAN = 3, + eLANGUAGE_SPANISH = 4, + eLANGUAGE_DUTCH = 5, + eLANGUAGE_SWEDISH = 6, + eLANGUAGE_DANISH = 7, + eLANGUAGE_KOREAN = 8, + eLANGUAGE_CHINESE = 9, + eLANGUAGE_JAPANESE = 10, + eLANGUAGE_THAI = 11, + eLANGUAGE_POLISH = 12, + eLANGUAGE_FINNISH = 13, + eLANGUAGE_LARGEST = 14, + eLANGUAGE_LABELS = 15, + eLANGUAGE_MAX = 16, +}; + +extern bool bEURGB60; +extern "C" void OSResetSystem(BOOL reset, u32 resetCode, BOOL forceMenu); + +void FlushCaches() { + PPCSync(); +} + +void EnableInterrupts() { + OSEnableInterrupts(); +} + +VIDEO_MODE GetVideoMode(); +void SetVideoMode(VIDEO_MODE mode); +VIDEO_MODE GetBuildRegionVideoMode(); +int eSetDisplaySystem(int video_mode); + +void InitDisplaySystem() { + if (bEURGB60) { + SetVideoMode(MODE_PAL60); + eSetDisplaySystem(GetVideoMode()); + } else { + SetVideoMode(GetBuildRegionVideoMode()); + eSetDisplaySystem(GetVideoMode()); + } +} + +void FinishedRenderingFEngLayer() {} + +int bDoWithStack(void *function, void *stack_pointer, int arg1, int arg2) { + return 0; +} + +enum eLanguages GC_GetOSLanguage() { + if (BuildRegion::IsEuropeFr()) { + return eLANGUAGE_FRENCH; + } + if (BuildRegion::IsEuropeGer()) { + return eLANGUAGE_GERMAN; + } + if (BuildRegion::IsJapan()) { + return eLANGUAGE_JAPANESE; + } + return eLANGUAGE_ENGLISH; +} + +void CheckReset(int resetMode) { + if (!MemoryCard::IsCardBusy()) { + VISetBlack(1); + VIFlush(); + VIWaitForRetrace(); + VISetBlack(1); + VIFlush(); + VIWaitForRetrace(); + OSResetSystem(resetMode, 1, 0); + } +} + +int DVDValidErrorState(int error) { + switch (error) { + case 5: + case 4: + case 6: + case 11: + case -1: + return error; + default: + return 0; + } +} + +void ServicePlatform() {} + +void eInitTexture() {} + +void eUnSwizzle8bitPalette(unsigned int *palette) {} + +void eSwizzle8bitPalette(unsigned int *palette) {} diff --git a/src/Speed/Indep/SourceLists/zPlatform.cpp b/src/Speed/Indep/SourceLists/zPlatform.cpp index e69de29bb..fe94e4c08 100644 --- a/src/Speed/Indep/SourceLists/zPlatform.cpp +++ b/src/Speed/Indep/SourceLists/zPlatform.cpp @@ -0,0 +1,33 @@ +#include "Speed/GameCube/Src/Platform_G.cpp" + +#include "Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp" + +#include "Speed/GameCube/Src/JoyE.cpp" + +#include "Speed/GameCube/Src/Render/SunE.cpp" + +#include "Speed/GameCube/Src/Render/AcidFX_G.cpp" + +#include "Speed/GameCube/Src/DemoDisc_G.cpp" + +#include "Speed/GameCube/Src/Movie_GC.cpp" + +#include "Speed/GameCube/Src/Logitech/Wheels.cpp" + +#include "Speed/GameCube/Src/Logitech/Ramp.cpp" + +#include "Speed/GameCube/Src/Logitech/Periodic.cpp" + +#include "Speed/GameCube/Src/Logitech/LGWheels.cpp" + +#include "Speed/GameCube/Src/Logitech/Force.cpp" + +#include "Speed/GameCube/Src/Logitech/Constant.cpp" + +#include "Speed/GameCube/Src/Logitech/Condition.cpp" + +#include "Speed/GameCube/Src/Ecstasy/xSprites.cpp" + +#include "Speed/GameCube/Src/xSparks.cpp" + +#include "Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp" diff --git a/src/Speed/Indep/Src/Ecstasy/Texture.hpp b/src/Speed/Indep/Src/Ecstasy/Texture.hpp index dbfb90ba5..c795c5b2f 100644 --- a/src/Speed/Indep/Src/Ecstasy/Texture.hpp +++ b/src/Speed/Indep/Src/Ecstasy/Texture.hpp @@ -5,17 +5,10 @@ #pragma once #endif -#ifdef EA_PLATFORM_GAMECUBE -#include "Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp" -#elif defined(EA_PLATFORM_XENON) -#include "Speed/Xenon/Src/Ecstasy/TextureInfoPlat.hpp" -#elif defined(EA_PLATFORM_PLAYSTATION2) -#include "Speed/PSX2/Src/Ecstasy/TextureInfoPlat.hpp" -#endif - #include "Speed/Indep/bWare/Inc/bChunk.hpp" #include "Speed/Indep/bWare/Inc/bSlotPool.hpp" #include "Speed/Indep/bWare/Inc/bWare.hpp" +#include "TextureTypes.hpp" extern SlotPool *TexturePackSlotPool; @@ -26,12 +19,6 @@ enum TextureScrollType { TEXSCROLL_NONE = 0, }; -enum TextureLockType { - TEXLOCK_READWRITE = 2, - TEXLOCK_WRITE = 1, - TEXLOCK_READ = 0, -}; - enum TextureCompressionType { TEXCOMP_8BIT_64 = 129, TEXCOMP_8BIT_16 = 128, @@ -69,6 +56,14 @@ enum TextureAlphaBlendType { TEXBLEND_SRCCOPY = 0, }; +#ifdef EA_PLATFORM_GAMECUBE +#include "Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp" +#elif defined(EA_PLATFORM_XENON) +#include "Speed/Xenon/Src/Ecstasy/TextureInfoPlat.hpp" +#elif defined(EA_PLATFORM_PLAYSTATION2) +#include "Speed/PSX2/Src/Ecstasy/TextureInfoPlat.hpp" +#endif + struct TextureIndexEntry { // total size: 0x8 unsigned int NameHash; // offset 0x0, size 0x4 diff --git a/src/Speed/Indep/Src/Ecstasy/TextureTypes.hpp b/src/Speed/Indep/Src/Ecstasy/TextureTypes.hpp new file mode 100644 index 000000000..a098bc7d8 --- /dev/null +++ b/src/Speed/Indep/Src/Ecstasy/TextureTypes.hpp @@ -0,0 +1,14 @@ +#ifndef ECSTASY_TEXTURE_TYPES_H +#define ECSTASY_TEXTURE_TYPES_H + +#ifdef EA_PRAGMA_ONCE_SUPPORTED +#pragma once +#endif + +enum TextureLockType { + TEXLOCK_READWRITE = 2, + TEXLOCK_WRITE = 1, + TEXLOCK_READ = 0, +}; + +#endif diff --git a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp index 377c828e1..d0aef81f4 100644 --- a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp +++ b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp @@ -5,6 +5,11 @@ #pragma once #endif +class MemoryCard { + public: + static int IsCardBusy(); +}; + void InitMemoryCard(); #endif diff --git a/src/Speed/Indep/Src/Misc/BuildRegion.hpp b/src/Speed/Indep/Src/Misc/BuildRegion.hpp index 612220bf6..57ab0dd40 100644 --- a/src/Speed/Indep/Src/Misc/BuildRegion.hpp +++ b/src/Speed/Indep/Src/Misc/BuildRegion.hpp @@ -8,6 +8,9 @@ namespace BuildRegion { bool IsAmerica(); +bool IsEuropeFr(); +bool IsEuropeGer(); +bool IsJapan(); }; // namespace BuildRegion From 57405bf7bb686e58de0104dc688eef169af000e6 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 22:43:20 +0100 Subject: [PATCH 024/172] 7.4%: match Logitech force and wheel helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Condition.cpp | 78 ++++++++ src/Speed/GameCube/Src/Logitech/Constant.cpp | 76 ++++++++ src/Speed/GameCube/Src/Logitech/Force.cpp | 95 ++++++++++ src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 173 +++++++++++++++--- src/Speed/GameCube/Src/Logitech/LGWheels.hpp | 88 ++++++++- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 82 +++++++++ src/Speed/GameCube/Src/Logitech/Ramp.cpp | 3 + src/Speed/GameCube/Src/Logitech/Wheels.cpp | 61 ++++++ 8 files changed, 626 insertions(+), 30 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Condition.cpp b/src/Speed/GameCube/Src/Logitech/Condition.cpp index e69de29bb..61da3e9cf 100644 --- a/src/Speed/GameCube/Src/Logitech/Condition.cpp +++ b/src/Speed/GameCube/Src/Logitech/Condition.cpp @@ -0,0 +1,78 @@ +#include "LGWheels.hpp" + +#include + +extern "C" { +int LGDownloadForceEffect(unsigned long handle, unsigned long *effectId, LGForceEffect *force); +int LGUpdateForceEffect(unsigned long effectId, LGForceEffect *force); +void OSReport(const char *fmt, ...); +} + +static const char kDownloadConditionForceError[] = "ERROR: DownloadForce(condition force) on channel %d returned %d\n"; +static const char kDownloadConditionForceInvalidWheel[] = "ERROR: Trying to download a condition force to channel %d but wheel has not been opened.\n"; +static const char kUpdateConditionForceError[] = "ERROR: UpdateForce(condition force) on channel %d returned %d\n"; + +static inline unsigned long &ConditionGetEffectID(Force *self, int channel, int forceNumber) { + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; +} + +Condition::Condition() : Force() {} + +int Condition::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { + LGForceEffect force; + int ret; + + ret = 0; + if (ConditionGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + Destroy(channel, forceNumber); + } + + if (handle != static_cast(-1)) { + memset(&force, 0, sizeof(force)); + force.type = type; + force.duration = duration; + force.startDelay = startDelay; + force.p.condition[0].offset = offset; + force.p.condition[0].deadband = deadband; + force.p.condition[0].saturationNeg = satNeg; + force.p.condition[0].saturationPos = satPos; + force.p.condition[0].coefficientNeg = coeffNeg; + force.p.condition[0].coefficientPos = coeffPos; + force.p.condition[1] = force.p.condition[0]; + + ret = LGDownloadForceEffect(handle, &ConditionGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kDownloadConditionForceError, channel, ret); + ConditionGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + } else { + OSReport(kDownloadConditionForceInvalidWheel, channel); + } + + return ret; +} + +int Condition::UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { + LGForceEffect force; + int ret; + + memset(&force, 0, sizeof(force)); + force.type = type; + force.duration = duration; + force.startDelay = startDelay; + force.p.condition[0].offset = offset; + force.p.condition[0].deadband = deadband; + force.p.condition[0].saturationNeg = satNeg; + force.p.condition[0].saturationPos = satPos; + force.p.condition[0].coefficientNeg = coeffNeg; + force.p.condition[0].coefficientPos = coeffPos; + force.p.condition[1] = force.p.condition[0]; + + ret = LGUpdateForceEffect(ConditionGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kUpdateConditionForceError, channel, ret); + ConditionGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + + return ret; +} diff --git a/src/Speed/GameCube/Src/Logitech/Constant.cpp b/src/Speed/GameCube/Src/Logitech/Constant.cpp index e69de29bb..53e887614 100644 --- a/src/Speed/GameCube/Src/Logitech/Constant.cpp +++ b/src/Speed/GameCube/Src/Logitech/Constant.cpp @@ -0,0 +1,76 @@ +#include "LGWheels.hpp" + +#include + +extern "C" { +int LGDownloadForceEffect(unsigned long handle, unsigned long *effectId, LGForceEffect *force); +int LGUpdateForceEffect(unsigned long effectId, LGForceEffect *force); +void OSReport(const char *fmt, ...); +} + +static const char kDownloadConstantForceError[] = "ERROR: DownloadForce(constant force) on channel %d returned %d\n"; +static const char kDownloadConstantForceInvalidWheel[] = "ERROR: Trying to download a constant force to channel %d but wheel has not been opened.\n"; +static const char kUpdateConstantForceError[] = "ERROR: UpdateForce(constant force) on channel %d returned %d\n"; + +static inline unsigned long &ConstantGetEffectID(Force *self, int channel, int forceNumber) { + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; +} + +Constant::Constant() : Force() {} + +int Constant::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { + LGForceEffect force; + int ret; + + ret = 0; + if (ConstantGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + Destroy(channel, forceNumber); + } + + if (handle != static_cast(-1)) { + memset(&force, 0, sizeof(force)); + force.type = 0; + force.duration = duration; + force.startDelay = startDelay; + force.p.constant.magnitude = magnitude; + force.p.constant.direction = direction; + force.p.constant.envelope.attackTime = attackTime; + force.p.constant.envelope.fadeTime = fadeTime; + force.p.constant.envelope.attackLevel = attackLevel; + force.p.constant.envelope.fadeLevel = fadeLevel; + + ret = LGDownloadForceEffect(handle, &ConstantGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kDownloadConstantForceError, channel, ret); + ConstantGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + } else { + OSReport(kDownloadConstantForceInvalidWheel, channel); + } + + return ret; +} + +int Constant::UpdateForce(int channel, int forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { + LGForceEffect force; + int ret; + + memset(&force, 0, sizeof(force)); + force.type = 0; + force.duration = duration; + force.startDelay = startDelay; + force.p.constant.magnitude = magnitude; + force.p.constant.direction = direction; + force.p.constant.envelope.attackTime = attackTime; + force.p.constant.envelope.fadeTime = fadeTime; + force.p.constant.envelope.attackLevel = attackLevel; + force.p.constant.envelope.fadeLevel = fadeLevel; + + ret = LGUpdateForceEffect(ConstantGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kUpdateConstantForceError, channel, ret); + ConstantGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + + return ret; +} diff --git a/src/Speed/GameCube/Src/Logitech/Force.cpp b/src/Speed/GameCube/Src/Logitech/Force.cpp index e69de29bb..b544753b4 100644 --- a/src/Speed/GameCube/Src/Logitech/Force.cpp +++ b/src/Speed/GameCube/Src/Logitech/Force.cpp @@ -0,0 +1,95 @@ +#include "LGWheels.hpp" + +extern "C" { +int LGStartForceEffect(unsigned long effectId); +int LGStopForceEffect(unsigned long effectId); +int LGDestroyForceEffect(unsigned long effectId); +void OSReport(const char *fmt, ...); +} + +static const char kStartForceError[] = "ERROR: Failed to start force effect on channel %d\n"; +static const char kStartForceInvalidEffectId[] = "ERROR: Trying to start force effect on channel %d but we have an invalid effectid\n"; +static const char kStopForceError[] = "ERROR: Failed to stop force effect on channel %d\n"; +static const char kStopForceInvalidEffectId[] = "ERROR: Trying to stop force effect on channel %d but we have an invalid effectid\n"; +static const char kDestroyForceError[] = "ERROR: Failed to destroy force effect on channel %d\n"; +static const char kDestroyForceInvalidEffectId[] = "ERROR: Trying to destroy force effect on channel %d but we have an invalid effectid\n"; + +static inline int &ForceGetPlaying(Force *self, int channel, int forceNumber) { + return reinterpret_cast(self)[channel * 8 + forceNumber]; +} + +static inline unsigned long &ForceGetEffectID(Force *self, int channel, int forceNumber) { + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; +} + +Force::Force() { + InitVars(); +} + +void Force::InitVars() { + int channel; + + for (channel = 0; channel < 4; channel++) { + int forceNumber; + + for (forceNumber = 0; forceNumber < 8; forceNumber++) { + ForceGetPlaying(this, channel, forceNumber) = 0; + ForceGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + } +} + +int Force::Start(int channel, int forceNumber) { + int ret; + + ret = 0; + if (ForceGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + ret = LGStartForceEffect(ForceGetEffectID(this, channel, forceNumber)); + if (ret < 0) { + OSReport(kStartForceError, channel); + } else { + ForceGetPlaying(this, channel, forceNumber) = 1; + } + } else { + OSReport(kStartForceInvalidEffectId, channel); + } + + return ret; +} + +int Force::Stop(int channel, int forceNumber) { + int ret; + + ret = 0; + if (ForceGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + ret = LGStopForceEffect(ForceGetEffectID(this, channel, forceNumber)); + if (ret < 0) { + OSReport(kStopForceError, channel); + } else { + ForceGetPlaying(this, channel, forceNumber) = 0; + } + } else { + OSReport(kStopForceInvalidEffectId, channel); + } + + return ret; +} + +int Force::Destroy(int channel, int forceNumber) { + int ret; + + ret = 0; + if (ForceGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + ret = LGDestroyForceEffect(ForceGetEffectID(this, channel, forceNumber)); + if (ret < 0) { + OSReport(kDestroyForceError, channel); + } else { + ForceGetPlaying(this, channel, forceNumber) = 0; + ForceGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + } else { + OSReport(kDestroyForceInvalidEffectId, channel); + } + + return ret; +} diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index e12469c09..7106a4f17 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -1,5 +1,11 @@ #include "LGWheels.hpp" +#include + +extern "C" { +void LGInit(); +} + struct SpringForceParams { char offset; unsigned char saturation; @@ -25,80 +31,195 @@ struct SurfaceEffectParams { unsigned short period; }; -static inline Wheels *GetWheels(LGWheels *self) { +extern void Wheels_Ctor(Wheels *self) asm("__6Wheels"); +extern void Force_Ctor(Force *self) asm("__5Force"); +extern void Condition_Ctor(Condition *self) asm("__9Condition"); +extern void Constant_Ctor(Constant *self) asm("__8Constant"); +extern void Periodic_Ctor(Periodic *self) asm("__8Periodic"); +extern void Ramp_Ctor(Ramp *self) asm("__4Ramp"); + +static inline Wheels *LGWheelsGetWheels(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x828); } -static inline const Wheels *GetWheels(const LGWheels *self) { +static inline const Wheels *LGWheelsGetWheels(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x828); } -static inline SpringForceParams *GetSpringForceParams(LGWheels *self) { +static inline Force *LGWheelsGetForce(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x10A8); +} + +static inline Condition *LGWheelsGetCondition(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x11A8); +} + +static inline Constant *LGWheelsGetConstant(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x12A8); +} + +static inline Periodic *LGWheelsGetPeriodic(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x13A8); +} + +static inline Ramp *LGWheelsGetRamp(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x14A8); +} + +static inline unsigned long &LGWheelsGetWheelHandle(LGWheels *self, int channel) { + return reinterpret_cast(reinterpret_cast(self) + 0x1050)[channel]; +} + +static inline int &LGWheelsGetDamperWasPlaying(LGWheels *self, int channel) { + return reinterpret_cast(reinterpret_cast(self) + 0x15AC)[channel]; +} + +static inline int &LGWheelsGetSpringWasPlaying(LGWheels *self, int channel) { + return reinterpret_cast(reinterpret_cast(self) + 0x15BC)[channel]; +} + +static inline int &LGWheelsGetIsAirborne(LGWheels *self, int channel) { + return reinterpret_cast(reinterpret_cast(self) + 0x166C)[channel]; +} + +static inline unsigned char &LGWheelsGetOverallGain(LGWheels *self) { + return *reinterpret_cast(reinterpret_cast(self) + 0x15A8); +} + +static inline int &LGWheelsGetPlaying(Force *self, int channel, int forceNumber) { + return reinterpret_cast(self)[channel * 8 + forceNumber]; +} + +static inline unsigned long &LGWheelsGetEffectID(Force *self, int channel, int forceNumber) { + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; +} + +static inline SpringForceParams *LGWheelsGetSpringForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x167C); } -static inline const SpringForceParams *GetSpringForceParams(const LGWheels *self) { +static inline const SpringForceParams *LGWheelsGetSpringForceParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x167C); } -static inline ConstantForceParams *GetConstantForceParams(LGWheels *self) { +static inline ConstantForceParams *LGWheelsGetConstantForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x168C); } -static inline const ConstantForceParams *GetConstantForceParams(const LGWheels *self) { +static inline const ConstantForceParams *LGWheelsGetConstantForceParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x168C); } -static inline DamperForceParams *GetDamperForceParams(LGWheels *self) { +static inline DamperForceParams *LGWheelsGetDamperForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x169C); } -static inline const DamperForceParams *GetDamperForceParams(const LGWheels *self) { +static inline const DamperForceParams *LGWheelsGetDamperForceParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x169C); } -static inline RoadEffectParams *GetDirtRoadParams(LGWheels *self) { +static inline RoadEffectParams *LGWheelsGetDirtRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16BC); } -static inline const RoadEffectParams *GetDirtRoadParams(const LGWheels *self) { +static inline const RoadEffectParams *LGWheelsGetDirtRoadParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16BC); } -static inline RoadEffectParams *GetBumpyRoadParams(LGWheels *self) { +static inline RoadEffectParams *LGWheelsGetBumpyRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16C4); } -static inline const RoadEffectParams *GetBumpyRoadParams(const LGWheels *self) { +static inline const RoadEffectParams *LGWheelsGetBumpyRoadParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16C4); } -static inline RoadEffectParams *GetSlipperyRoadParams(LGWheels *self) { +static inline RoadEffectParams *LGWheelsGetSlipperyRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16CC); } -static inline const RoadEffectParams *GetSlipperyRoadParams(const LGWheels *self) { +static inline const RoadEffectParams *LGWheelsGetSlipperyRoadParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16CC); } -static inline SurfaceEffectParams *GetSurfaceEffectParams(LGWheels *self) { +static inline SurfaceEffectParams *LGWheelsGetSurfaceEffectParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16D4); } -static inline const SurfaceEffectParams *GetSurfaceEffectParams(const LGWheels *self) { +static inline const SurfaceEffectParams *LGWheelsGetSurfaceEffectParams(const LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16D4); } +LGWheels::LGWheels() { + int ii; + + Wheels_Ctor(LGWheelsGetWheels(this)); + Force_Ctor(LGWheelsGetForce(this)); + Condition_Ctor(LGWheelsGetCondition(this)); + Constant_Ctor(LGWheelsGetConstant(this)); + Periodic_Ctor(LGWheelsGetPeriodic(this)); + Ramp_Ctor(LGWheelsGetRamp(this)); + LGInit(); + LGWheelsGetOverallGain(this) = 0xFF; + + for (ii = 0; ii < 4; ii++) { + InitVars(ii); + } +} + +void LGWheels::InitVars(int channel) { + int ii; + + LGWheelsGetIsAirborne(this, channel) = 0; + LGWheelsGetDamperWasPlaying(this, channel) = 0; + LGWheelsGetSpringWasPlaying(this, channel) = 0; + + for (ii = 0; ii < 8; ii++) { + LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, ii) = static_cast(-1); + LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, ii) = 0; + LGWheelsGetEffectID(LGWheelsGetConstant(this), channel, ii) = static_cast(-1); + LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, ii) = 0; + LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, ii) = static_cast(-1); + LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, ii) = 0; + LGWheelsGetEffectID(LGWheelsGetRamp(this), channel, ii) = static_cast(-1); + LGWheelsGetPlaying(LGWheelsGetRamp(this), channel, ii) = 0; + } +} + +void LGWheels::ReadAll() { + short wheelUnplugged; + + wheelUnplugged = LGWheelsGetWheels(this)->ReadAll(); + memcpy(this, LGWheelsGetWheels(this), sizeof(LGPosition) * 4); + if (wheelUnplugged != -1) { + InitVars(wheelUnplugged); + } +} + bool LGWheels::IsConnected(int channel) { - return GetWheels(this)->IsConnected(channel); + return LGWheelsGetWheels(this)->IsConnected(channel); } bool LGWheels::ButtonIsPressed(int channel, unsigned long buttonMask) { - return GetWheels(this)->ButtonIsPressed(channel, buttonMask); + return LGWheelsGetWheels(this)->ButtonIsPressed(channel, buttonMask); } bool LGWheels::PedalsConnected(int channel) { - return GetWheels(this)->PedalsConnected(channel); + return LGWheelsGetWheels(this)->PedalsConnected(channel); +} + +void LGWheels::PlayAutoCalibAndSpringForce(int channel) { + if (LGWheelsGetWheels(this)->IsConnected(channel) && !LGWheelsGetIsAirborne(this, channel)) { + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 4) == static_cast(-1)) { + LGWheelsGetPeriodic(this)->DownloadForce(channel, 4, LGWheelsGetWheelHandle(this, channel), 3, 2200, 0, 180, 90, 2200, 0, 0, 0, 0, 0, 0); + LGWheelsGetPeriodic(this)->Start(channel, 4); + } + + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { + LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 2200, 0, 0, 180, 180, 180, 180); + LGWheelsGetCondition(this)->Start(channel, 0); + } + } } void LGWheels::StopSpringForce(int channel) { @@ -106,7 +227,7 @@ void LGWheels::StopSpringForce(int channel) { } bool LGWheels::SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient) { - const SpringForceParams ¶ms = GetSpringForceParams(this)[channel]; + const SpringForceParams ¶ms = LGWheelsGetSpringForceParams(this)[channel]; return params.offset == offset && params.saturation == saturation && params.coefficient == coefficient; } @@ -115,7 +236,7 @@ void LGWheels::StopConstantForce(int channel) { } bool LGWheels::SameConstantForceParams(int channel, short magnitude, unsigned short direction) { - const ConstantForceParams ¶ms = GetConstantForceParams(this)[channel]; + const ConstantForceParams ¶ms = LGWheelsGetConstantForceParams(this)[channel]; return params.magnitude == magnitude && params.direction == direction; } @@ -124,7 +245,7 @@ void LGWheels::StopDamperForce(int channel) { } bool LGWheels::SameDamperForceParams(int channel, short coefficient) { - return GetDamperForceParams(this)[channel].coefficient == coefficient; + return LGWheelsGetDamperForceParams(this)[channel].coefficient == coefficient; } void LGWheels::StopDirtRoadEffect(int channel) { @@ -132,7 +253,7 @@ void LGWheels::StopDirtRoadEffect(int channel) { } bool LGWheels::SameDirtRoadEffectParams(int channel, short magnitude) { - return GetDirtRoadParams(this)[channel].magnitude == magnitude; + return LGWheelsGetDirtRoadParams(this)[channel].magnitude == magnitude; } void LGWheels::StopBumpyRoadEffect(int channel) { @@ -140,7 +261,7 @@ void LGWheels::StopBumpyRoadEffect(int channel) { } bool LGWheels::SameBumpyRoadEffectParams(int channel, short magnitude) { - return GetBumpyRoadParams(this)[channel].magnitude == magnitude; + return LGWheelsGetBumpyRoadParams(this)[channel].magnitude == magnitude; } void LGWheels::StopSlipperyRoadEffect(int channel) { @@ -148,7 +269,7 @@ void LGWheels::StopSlipperyRoadEffect(int channel) { } bool LGWheels::SameSlipperyRoadEffectParams(int channel, short magnitude) { - return GetSlipperyRoadParams(this)[channel].magnitude == magnitude; + return LGWheelsGetSlipperyRoadParams(this)[channel].magnitude == magnitude; } void LGWheels::StopSurfaceEffect(int channel) { @@ -156,7 +277,7 @@ void LGWheels::StopSurfaceEffect(int channel) { } bool LGWheels::SameSurfaceEffectParams(int channel, unsigned char type, unsigned char magnitude, unsigned short period) { - const SurfaceEffectParams ¶ms = GetSurfaceEffectParams(this)[channel]; + const SurfaceEffectParams ¶ms = LGWheelsGetSurfaceEffectParams(this)[channel]; return params.type == type && params.magnitude == magnitude && params.period == period; } diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp index fe14fa638..971ef1685 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp @@ -17,33 +17,113 @@ struct LGPosition { char err; }; +struct LGEnvelopeParams { + unsigned long attackTime; + unsigned long fadeTime; + unsigned char attackLevel; + unsigned char fadeLevel; + unsigned char pad[2]; +}; + +struct LGConstantForceParams { + short magnitude; + unsigned short direction; + LGEnvelopeParams envelope; +}; + +struct LGRampForceParams { + short magnitudeStart; + short magnitudeEnd; + unsigned short direction; +}; + +struct LGPeriodicForceParams { + unsigned char magnitude; + unsigned char pad; + unsigned short direction; + unsigned short period; + unsigned short phase; + short offset; + unsigned char pad2[2]; + LGEnvelopeParams envelope; +}; + +struct LGConditionForceParams { + char offset; + unsigned char deadband; + unsigned char saturationNeg; + unsigned char saturationPos; + short coefficientNeg; + short coefficientPos; +}; + +struct LGForceEffect { + unsigned char type; + unsigned char pad[3]; + unsigned long duration; + unsigned long startDelay; + union { + LGConstantForceParams constant; + LGRampForceParams ramp; + LGPeriodicForceParams periodic; + LGConditionForceParams condition[2]; + } p; +}; + struct Wheels { Wheels(); ~Wheels(); + short ReadAll(); bool ButtonIsPressed(int channel, unsigned long buttonMask); bool IsConnected(int channel); bool PedalsConnected(int channel); }; struct Force { + Force(); + void InitVars(); + int Start(int channel, int forceNumber); + int Stop(int channel, int forceNumber); + int Destroy(int channel, int forceNumber); + bool Playing[8][4]; unsigned long EffectID[8][4]; }; -struct Condition : public Force {}; -struct Constant : public Force {}; -struct Periodic : public Force {}; -struct Ramp : public Force {}; +struct Condition : public Force { + Condition(); + int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); + int UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); +}; + +struct Constant : public Force { + Constant(); + int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int UpdateForce(int channel, int forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); +}; + +struct Periodic : public Force { + Periodic(); + int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); +}; + +struct Ramp : public Force { + Ramp(); +}; struct LGWheels { LGWheels(); ~LGWheels(); + void InitVars(int channel); + void ReadAll(); void StopForce(int channel, int forceType); bool IsConnected(int channel); bool ButtonIsPressed(int channel, unsigned long buttonMask); bool PedalsConnected(int channel); + void PlayAutoCalibAndSpringForce(int channel); void StopSpringForce(int channel); bool SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient); void StopConstantForce(int channel); diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index e69de29bb..e93f1d0b3 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -0,0 +1,82 @@ +#include "LGWheels.hpp" + +#include + +extern "C" { +int LGDownloadForceEffect(unsigned long handle, unsigned long *effectId, LGForceEffect *force); +int LGUpdateForceEffect(unsigned long effectId, LGForceEffect *force); +void OSReport(const char *fmt, ...); +} + +static const char kDownloadPeriodicForceError[] = "ERROR: DownloadForce(periodic force) on channel %d returned %d\n"; +static const char kDownloadPeriodicForceInvalidWheel[] = "ERROR: Trying to download a periodic force to channel %d but wheel has not been opened.\n"; +static const char kUpdatePeriodicForceError[] = "ERROR: UpdateForce(periodic force) on channel %d returned %d\n"; + +static inline unsigned long &PeriodicGetEffectID(Force *self, int channel, int forceNumber) { + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; +} + +Periodic::Periodic() : Force() {} + +int Periodic::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { + LGForceEffect force; + int ret; + + ret = 0; + if (PeriodicGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + Destroy(channel, forceNumber); + } + + if (handle != static_cast(-1)) { + memset(&force, 0, sizeof(force)); + force.type = type; + force.duration = duration; + force.startDelay = startDelay; + force.p.periodic.magnitude = magnitude; + force.p.periodic.direction = direction; + force.p.periodic.period = period; + force.p.periodic.phase = phase; + force.p.periodic.offset = offset; + force.p.periodic.envelope.attackTime = attackTime; + force.p.periodic.envelope.fadeTime = fadeTime; + force.p.periodic.envelope.attackLevel = attackLevel; + force.p.periodic.envelope.fadeLevel = fadeLevel; + + ret = LGDownloadForceEffect(handle, &PeriodicGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kDownloadPeriodicForceError, channel, ret); + PeriodicGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + } else { + OSReport(kDownloadPeriodicForceInvalidWheel, channel); + } + + return ret; +} + +int Periodic::UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { + LGForceEffect force; + int ret; + + memset(&force, 0, sizeof(force)); + force.type = type; + force.duration = duration; + force.startDelay = startDelay; + force.p.periodic.magnitude = magnitude; + force.p.periodic.direction = direction; + force.p.periodic.period = period; + force.p.periodic.phase = phase; + force.p.periodic.offset = offset; + force.p.periodic.envelope.attackTime = attackTime; + force.p.periodic.envelope.fadeTime = fadeTime; + force.p.periodic.envelope.attackLevel = attackLevel; + force.p.periodic.envelope.fadeLevel = fadeLevel; + + ret = LGUpdateForceEffect(PeriodicGetEffectID(this, channel, forceNumber), &force); + if (ret < 0) { + OSReport(kUpdatePeriodicForceError, channel, ret); + PeriodicGetEffectID(this, channel, forceNumber) = static_cast(-1); + } + + return ret; +} diff --git a/src/Speed/GameCube/Src/Logitech/Ramp.cpp b/src/Speed/GameCube/Src/Logitech/Ramp.cpp index e69de29bb..791678f88 100644 --- a/src/Speed/GameCube/Src/Logitech/Ramp.cpp +++ b/src/Speed/GameCube/Src/Logitech/Ramp.cpp @@ -0,0 +1,3 @@ +#include "LGWheels.hpp" + +Ramp::Ramp() : Force() {} diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index 658c4138a..c0ff4d9e8 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -1,5 +1,66 @@ #include "LGWheels.hpp" +#include + +extern "C" { +int LGOpen(int channel, unsigned long *wheelHandle); +void LGRead(Wheels *wheels); +void OSReport(const char *fmt, ...); +} + +static const char kOpenWheelError[] = "ERROR: Could not open wheel on channel %d\n"; + +static inline unsigned long *WheelsGetWheelHandles(Wheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x828); +} + +static inline LGPosition *WheelsGetPositionLast(Wheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x858); +} + +Wheels::Wheels() { + int channel; + + for (channel = 0; channel < 4; channel++) { + WheelsGetWheelHandles(this)[channel] = static_cast(-1); + reinterpret_cast(this)[channel].err = -1; + } + + memset(WheelsGetPositionLast(this), 0, sizeof(LGPosition) * 4); +} + +short Wheels::ReadAll() { + int channel; + short wheelUnplugged; + int ret; + + for (channel = 0; channel < 4; channel++) { + if (SIProbe(channel) == 0x08000000 && WheelsGetWheelHandles(this)[channel] == static_cast(-1)) { + ret = LGOpen(channel, &WheelsGetWheelHandles(this)[channel]); + if (ret < 0) { + OSReport(kOpenWheelError, channel); + break; + } + + reinterpret_cast(this)[channel].err = 0; + } + } + + memcpy(WheelsGetPositionLast(this), reinterpret_cast(this), sizeof(LGPosition) * 4); + LGRead(this); + + for (channel = 0; channel < 4; channel++) { + if (reinterpret_cast(this)[channel].err == -1 && WheelsGetWheelHandles(this)[channel] != static_cast(-1)) { + WheelsGetWheelHandles(this)[channel] = static_cast(-1); + wheelUnplugged = static_cast(channel); + return wheelUnplugged; + } + } + + wheelUnplugged = -1; + return wheelUnplugged; +} + bool Wheels::ButtonIsPressed(int channel, unsigned long buttonMask) { const LGPosition *position = reinterpret_cast(this); return (position[channel].button & buttonMask) != 0; From 73875e285c8fd0eebe8b3242db84ffe1e53a2e23 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 22:53:44 +0100 Subject: [PATCH 025/172] 19.2%: match LGWheels force wrapper batch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Condition.cpp | 4 +- src/Speed/GameCube/Src/Logitech/Constant.cpp | 4 +- src/Speed/GameCube/Src/Logitech/Force.cpp | 6 +- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 615 ++++++++++++++++-- src/Speed/GameCube/Src/Logitech/LGWheels.hpp | 77 ++- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 4 +- src/Speed/GameCube/Src/Logitech/Wheels.cpp | 6 +- 7 files changed, 621 insertions(+), 95 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Condition.cpp b/src/Speed/GameCube/Src/Logitech/Condition.cpp index 61da3e9cf..5069675a4 100644 --- a/src/Speed/GameCube/Src/Logitech/Condition.cpp +++ b/src/Speed/GameCube/Src/Logitech/Condition.cpp @@ -18,7 +18,7 @@ static inline unsigned long &ConditionGetEffectID(Force *self, int channel, int Condition::Condition() : Force() {} -int Condition::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { +int Condition::DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, signed char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { LGForceEffect force; int ret; @@ -52,7 +52,7 @@ int Condition::DownloadForce(int channel, int forceNumber, unsigned long & handl return ret; } -int Condition::UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { +int Condition::UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, signed char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos) { LGForceEffect force; int ret; diff --git a/src/Speed/GameCube/Src/Logitech/Constant.cpp b/src/Speed/GameCube/Src/Logitech/Constant.cpp index 53e887614..685b7e1d5 100644 --- a/src/Speed/GameCube/Src/Logitech/Constant.cpp +++ b/src/Speed/GameCube/Src/Logitech/Constant.cpp @@ -18,7 +18,7 @@ static inline unsigned long &ConstantGetEffectID(Force *self, int channel, int f Constant::Constant() : Force() {} -int Constant::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { +int Constant::DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; @@ -51,7 +51,7 @@ int Constant::DownloadForce(int channel, int forceNumber, unsigned long & handle return ret; } -int Constant::UpdateForce(int channel, int forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { +int Constant::UpdateForce(long channel, long forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; diff --git a/src/Speed/GameCube/Src/Logitech/Force.cpp b/src/Speed/GameCube/Src/Logitech/Force.cpp index b544753b4..30d43a373 100644 --- a/src/Speed/GameCube/Src/Logitech/Force.cpp +++ b/src/Speed/GameCube/Src/Logitech/Force.cpp @@ -39,7 +39,7 @@ void Force::InitVars() { } } -int Force::Start(int channel, int forceNumber) { +int Force::Start(long channel, long forceNumber) { int ret; ret = 0; @@ -57,7 +57,7 @@ int Force::Start(int channel, int forceNumber) { return ret; } -int Force::Stop(int channel, int forceNumber) { +int Force::Stop(long channel, long forceNumber) { int ret; ret = 0; @@ -75,7 +75,7 @@ int Force::Stop(int channel, int forceNumber) { return ret; } -int Force::Destroy(int channel, int forceNumber) { +int Force::Destroy(long channel, long forceNumber) { int ret; ret = 0; diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 7106a4f17..80739ead7 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -38,14 +38,13 @@ extern void Constant_Ctor(Constant *self) asm("__8Constant"); extern void Periodic_Ctor(Periodic *self) asm("__8Periodic"); extern void Ramp_Ctor(Ramp *self) asm("__4Ramp"); +static const char kPlayForceError[] = "ERROR: trying to play a force on channel %d but no wheel opened. +"; + static inline Wheels *LGWheelsGetWheels(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x828); } -static inline const Wheels *LGWheelsGetWheels(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x828); -} - static inline Force *LGWheelsGetForce(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x10A8); } @@ -78,6 +77,10 @@ static inline int &LGWheelsGetSpringWasPlaying(LGWheels *self, int channel) { return reinterpret_cast(reinterpret_cast(self) + 0x15BC)[channel]; } +static inline int &LGWheelsGetWasPlayingBeforeAirborne(LGWheels *self, int channel, int forceType) { + return reinterpret_cast(reinterpret_cast(self) + 0x15CC + channel * 0x28)[forceType]; +} + static inline int &LGWheelsGetIsAirborne(LGWheels *self, int channel) { return reinterpret_cast(reinterpret_cast(self) + 0x166C)[channel]; } @@ -98,58 +101,34 @@ static inline SpringForceParams *LGWheelsGetSpringForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x167C); } -static inline const SpringForceParams *LGWheelsGetSpringForceParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x167C); -} - static inline ConstantForceParams *LGWheelsGetConstantForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x168C); } -static inline const ConstantForceParams *LGWheelsGetConstantForceParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x168C); -} - static inline DamperForceParams *LGWheelsGetDamperForceParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x169C); } -static inline const DamperForceParams *LGWheelsGetDamperForceParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x169C); +static inline RoadEffectParams *LGWheelsGetFrontalCollisionParams(LGWheels *self) { + return reinterpret_cast(reinterpret_cast(self) + 0x16B4); } static inline RoadEffectParams *LGWheelsGetDirtRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16BC); } -static inline const RoadEffectParams *LGWheelsGetDirtRoadParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x16BC); -} - static inline RoadEffectParams *LGWheelsGetBumpyRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16C4); } -static inline const RoadEffectParams *LGWheelsGetBumpyRoadParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x16C4); -} - static inline RoadEffectParams *LGWheelsGetSlipperyRoadParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16CC); } -static inline const RoadEffectParams *LGWheelsGetSlipperyRoadParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x16CC); -} - static inline SurfaceEffectParams *LGWheelsGetSurfaceEffectParams(LGWheels *self) { return reinterpret_cast(reinterpret_cast(self) + 0x16D4); } -static inline const SurfaceEffectParams *LGWheelsGetSurfaceEffectParams(const LGWheels *self) { - return reinterpret_cast(reinterpret_cast(self) + 0x16D4); -} - LGWheels::LGWheels() { int ii; @@ -167,7 +146,7 @@ LGWheels::LGWheels() { } } -void LGWheels::InitVars(int channel) { +void LGWheels::InitVars(long channel) { int ii; LGWheelsGetIsAirborne(this, channel) = 0; @@ -196,19 +175,137 @@ void LGWheels::ReadAll() { } } -bool LGWheels::IsConnected(int channel) { +void LGWheels::StopForce(long channel, long forceType) { + switch (forceType) { + case 0: + if (IsPlaying(channel, 0)) { + LGWheelsGetCondition(this)->Stop(channel, 0); + } + break; + case 1: + if (IsPlaying(channel, 1)) { + LGWheelsGetConstant(this)->Stop(channel, 0); + } + break; + case 2: + if (IsPlaying(channel, 2)) { + LGWheelsGetCondition(this)->Stop(channel, 1); + } + break; + case 3: + if (IsPlaying(channel, 3)) { + LGWheelsGetConstant(this)->Stop(channel, 1); + } + break; + case 4: + if (IsPlaying(channel, 4)) { + LGWheelsGetPeriodic(this)->Stop(channel, 0); + } + break; + case 5: + if (IsPlaying(channel, 5)) { + LGWheelsGetPeriodic(this)->Stop(channel, 1); + } + break; + case 6: + if (IsPlaying(channel, 6)) { + LGWheelsGetPeriodic(this)->Stop(channel, 2); + } + break; + case 7: + if (IsPlaying(channel, 7)) { + LGWheelsGetCondition(this)->Stop(channel, 2); + } + if (LGWheelsGetDamperWasPlaying(this, channel)) { + PlayDamperForce(channel, LGWheelsGetDamperForceParams(this)[channel].coefficient); + LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) = 1; + LGWheelsGetDamperWasPlaying(this, channel) = 0; + } + if (LGWheelsGetSpringWasPlaying(this, channel)) { + PlaySpringForce(channel, LGWheelsGetSpringForceParams(this)[channel].offset, LGWheelsGetSpringForceParams(this)[channel].saturation, LGWheelsGetSpringForceParams(this)[channel].coefficient); + LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) = 1; + LGWheelsGetSpringWasPlaying(this, channel) = 0; + } + break; + case 8: + if (IsPlaying(channel, 8)) { + LGWheelsGetPeriodic(this)->Stop(channel, 3); + } + break; + case 9: + LGWheelsGetIsAirborne(this, channel) = 0; + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) == 1) { + PlaySpringForce(channel, LGWheelsGetSpringForceParams(this)[channel].offset, LGWheelsGetSpringForceParams(this)[channel].saturation, LGWheelsGetSpringForceParams(this)[channel].coefficient); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) == 1) { + PlayConstantForce(channel, LGWheelsGetConstantForceParams(this)[channel].magnitude, LGWheelsGetConstantForceParams(this)[channel].direction); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) == 1) { + PlayDamperForce(channel, LGWheelsGetDamperForceParams(this)[channel].coefficient); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) == 1) { + PlayDirtRoadEffect(channel, static_cast(LGWheelsGetDirtRoadParams(this)[channel].magnitude)); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) == 1) { + PlayBumpyRoadEffect(channel, static_cast(LGWheelsGetBumpyRoadParams(this)[channel].magnitude)); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) == 1) { + PlaySlipperyRoadEffect(channel, LGWheelsGetSlipperyRoadParams(this)[channel].magnitude); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) == 1) { + PlaySurfaceEffect(channel, LGWheelsGetSurfaceEffectParams(this)[channel].type, LGWheelsGetSurfaceEffectParams(this)[channel].magnitude, LGWheelsGetSurfaceEffectParams(this)[channel].period); + } + { + int jj; + + for (jj = 0; jj < 10; jj++) { + LGWheelsGetWasPlayingBeforeAirborne(this, channel, jj) = 0; + } + } + break; + } +} + +bool LGWheels::IsConnected(long channel) { return LGWheelsGetWheels(this)->IsConnected(channel); } -bool LGWheels::ButtonIsPressed(int channel, unsigned long buttonMask) { +bool LGWheels::IsPlaying(long channel, long forceType) { + switch (forceType) { + case 0: + return LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) != 0; + case 1: + return LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, 0) != 0; + case 2: + return LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) != 0; + case 3: + return LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, 1) != 0; + case 4: + return LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 0) != 0; + case 5: + return LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 1) != 0; + case 6: + return LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 2) != 0; + case 7: + return LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0; + case 8: + return LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 3) != 0; + case 9: + return LGWheelsGetIsAirborne(this, channel) == 1; + } + + return false; +} + +bool LGWheels::ButtonIsPressed(long channel, unsigned long buttonMask) { return LGWheelsGetWheels(this)->ButtonIsPressed(channel, buttonMask); } -bool LGWheels::PedalsConnected(int channel) { +bool LGWheels::PedalsConnected(long channel) { return LGWheelsGetWheels(this)->PedalsConnected(channel); } -void LGWheels::PlayAutoCalibAndSpringForce(int channel) { +void LGWheels::PlayAutoCalibAndSpringForce(long channel) { if (LGWheelsGetWheels(this)->IsConnected(channel) && !LGWheelsGetIsAirborne(this, channel)) { if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 4) == static_cast(-1)) { LGWheelsGetPeriodic(this)->DownloadForce(channel, 4, LGWheelsGetWheelHandle(this, channel), 3, 2200, 0, 180, 90, 2200, 0, 0, 0, 0, 0, 0); @@ -222,65 +319,483 @@ void LGWheels::PlayAutoCalibAndSpringForce(int channel) { } } -void LGWheels::StopSpringForce(int channel) { +void LGWheels::PlaySpringForce(long channel, signed char offset, unsigned char saturation, short coefficient) { + int ret; + + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + return; + } + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) != 0) { + if (SameSpringForceParams(channel, offset, saturation, coefficient)) { + return; + } + + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + if (ret < 0) { + return; + } + + LGWheelsGetSpringForceParams(this)[channel].offset = offset; + LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; + LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + } else if (SameSpringForceParams(channel, offset, saturation, coefficient)) { + LGWheelsGetCondition(this)->Start(channel, 0); + return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + } + + if (ret >= 0) { + LGWheelsGetSpringForceParams(this)[channel].offset = offset; + LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; + LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + } + + LGWheelsGetCondition(this)->Start(channel, 0); +} + +void LGWheels::StopSpringForce(long channel) { this->StopForce(channel, 0); } -bool LGWheels::SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient) { +bool LGWheels::SameSpringForceParams(long channel, signed char offset, unsigned char saturation, short coefficient) { const SpringForceParams ¶ms = LGWheelsGetSpringForceParams(this)[channel]; return params.offset == offset && params.saturation == saturation && params.coefficient == coefficient; } -void LGWheels::StopConstantForce(int channel) { +void LGWheels::PlayConstantForce(long channel, short magnitude, unsigned short direction) { + int ret; + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, 0) != 0) { + if (SameConstantForceParams(channel, magnitude, direction)) { + return; + } + + ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); + if (ret < 0) { + return; + } + + LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; + LGWheelsGetConstantForceParams(this)[channel].direction = direction; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetConstant(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetConstant(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); + } else if (SameConstantForceParams(channel, magnitude, direction)) { + LGWheelsGetConstant(this)->Start(channel, 0); + return; + } else { + ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; + LGWheelsGetConstantForceParams(this)[channel].direction = direction; + } + + LGWheelsGetConstant(this)->Start(channel, 0); +} + +void LGWheels::StopConstantForce(long channel) { this->StopForce(channel, 1); } -bool LGWheels::SameConstantForceParams(int channel, short magnitude, unsigned short direction) { +bool LGWheels::SameConstantForceParams(long channel, short magnitude, unsigned short direction) { const ConstantForceParams ¶ms = LGWheelsGetConstantForceParams(this)[channel]; return params.magnitude == magnitude && params.direction == direction; } -void LGWheels::StopDamperForce(int channel) { +void LGWheels::PlayDamperForce(long channel, short coefficient) { + int ret; + + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + return; + } + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) != 0) { + if (SameDamperForceParams(channel, coefficient)) { + return; + } + + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + if (ret < 0) { + return; + } + + LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 1) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + } else if (SameDamperForceParams(channel, coefficient)) { + LGWheelsGetCondition(this)->Start(channel, 1); + return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + } + + if (ret >= 0) { + LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + } + + LGWheelsGetCondition(this)->Start(channel, 1); +} + +void LGWheels::StopDamperForce(long channel) { this->StopForce(channel, 2); } -bool LGWheels::SameDamperForceParams(int channel, short coefficient) { +bool LGWheels::SameDamperForceParams(long channel, short coefficient) { return LGWheelsGetDamperForceParams(this)[channel].coefficient == coefficient; } -void LGWheels::StopDirtRoadEffect(int channel) { +void LGWheels::PlayFrontalCollisionForce(long channel, unsigned char magnitude) { + int ret; + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 0) != 0) { + if (!SameFrontalCollisionForceParams(channel, magnitude)) { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 0, 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); + if (ret >= 0) { + LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; + } + } + LGWheelsGetPeriodic(this)->Start(channel, 0); + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); + } else if (SameFrontalCollisionForceParams(channel, magnitude)) { + ret = 0; + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 0, 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; + } + + LGWheelsGetPeriodic(this)->Start(channel, 0); +} + +bool LGWheels::SameFrontalCollisionForceParams(long channel, short magnitude) { + return LGWheelsGetFrontalCollisionParams(this)[channel].magnitude == magnitude; +} + +void LGWheels::PlayDirtRoadEffect(long channel, unsigned char magnitude) { + int ret; + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 1) != 0) { + if (SameDirtRoadEffectParams(channel, magnitude)) { + return; + } + + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + if (ret < 0) { + return; + } + + LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 1) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + } else if (SameDirtRoadEffectParams(channel, magnitude)) { + LGWheelsGetPeriodic(this)->Start(channel, 1); + return; + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; + } + + LGWheelsGetPeriodic(this)->Start(channel, 1); +} + +void LGWheels::StopDirtRoadEffect(long channel) { this->StopForce(channel, 5); } -bool LGWheels::SameDirtRoadEffectParams(int channel, short magnitude) { +bool LGWheels::SameDirtRoadEffectParams(long channel, short magnitude) { return LGWheelsGetDirtRoadParams(this)[channel].magnitude == magnitude; } -void LGWheels::StopBumpyRoadEffect(int channel) { +void LGWheels::PlayBumpyRoadEffect(long channel, unsigned char magnitude) { + int ret; + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 2) != 0) { + if (SameBumpyRoadEffectParams(channel, magnitude)) { + return; + } + + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + if (ret < 0) { + return; + } + + LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 2) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + } else if (SameBumpyRoadEffectParams(channel, magnitude)) { + LGWheelsGetPeriodic(this)->Start(channel, 2); + return; + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; + } + + LGWheelsGetPeriodic(this)->Start(channel, 2); +} + +void LGWheels::StopBumpyRoadEffect(long channel) { this->StopForce(channel, 6); } -bool LGWheels::SameBumpyRoadEffectParams(int channel, short magnitude) { +bool LGWheels::SameBumpyRoadEffectParams(long channel, short magnitude) { return LGWheelsGetBumpyRoadParams(this)[channel].magnitude == magnitude; } -void LGWheels::StopSlipperyRoadEffect(int channel) { +void LGWheels::PlaySlipperyRoadEffect(long channel, short magnitude) { + int ret; + + if (IsPlaying(channel, 2)) { + StopDamperForce(channel); + LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) = 0; + LGWheelsGetDamperWasPlaying(this, channel) = 1; + } + + if (IsPlaying(channel, 0)) { + StopSpringForce(channel); + LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) = 0; + LGWheelsGetSpringWasPlaying(this, channel) = 1; + } + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + if (SameSlipperyRoadEffectParams(channel, magnitude)) { + return; + } + + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + if (ret < 0) { + return; + } + + LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 2) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + } else if (SameSlipperyRoadEffectParams(channel, magnitude)) { + LGWheelsGetCondition(this)->Start(channel, 2); + return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + } + + if (ret >= 0) { + LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + } + + LGWheelsGetCondition(this)->Start(channel, 2); +} + +void LGWheels::StopSlipperyRoadEffect(long channel) { this->StopForce(channel, 7); } -bool LGWheels::SameSlipperyRoadEffectParams(int channel, short magnitude) { +bool LGWheels::SameSlipperyRoadEffectParams(long channel, short magnitude) { return LGWheelsGetSlipperyRoadParams(this)[channel].magnitude == magnitude; } -void LGWheels::StopSurfaceEffect(int channel) { +void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { + int ret; + + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } + + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 3) != 0) { + if (SameSurfaceEffectParams(channel, type, magnitude, period)) { + return; + } + + if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { + LGWheelsGetPeriodic(this)->Destroy(channel, 3); + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + LGWheelsGetPeriodic(this)->Start(channel, 3); + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetSurfaceEffectParams(this)[channel].type = type; + LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; + LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + } + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 3) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + if (ret >= 0) { + LGWheelsGetSurfaceEffectParams(this)[channel].type = type; + LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; + LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + } + LGWheelsGetPeriodic(this)->Start(channel, 3); + return; + } + + if (SameSurfaceEffectParams(channel, type, magnitude, period)) { + LGWheelsGetPeriodic(this)->Start(channel, 3); + return; + } + + if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { + LGWheelsGetPeriodic(this)->Destroy(channel, 3); + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetSurfaceEffectParams(this)[channel].type = type; + LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; + LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + } + + LGWheelsGetPeriodic(this)->Start(channel, 3); +} + +void LGWheels::StopSurfaceEffect(long channel) { this->StopForce(channel, 8); } -bool LGWheels::SameSurfaceEffectParams(int channel, unsigned char type, unsigned char magnitude, unsigned short period) { +bool LGWheels::SameSurfaceEffectParams(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { const SurfaceEffectParams ¶ms = LGWheelsGetSurfaceEffectParams(this)[channel]; return params.type == type && params.magnitude == magnitude && params.period == period; } -void LGWheels::StopCarAirborne(int channel) { +void LGWheels::PlayCarAirborne(long channel) { + if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + OSReport(kPlayForceError, channel); + return; + } + + LGWheelsGetIsAirborne(this, channel) = 1; + if (IsPlaying(channel, 0)) { + StopSpringForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) = 1; + } + if (IsPlaying(channel, 1)) { + StopConstantForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) = 1; + } + if (IsPlaying(channel, 2)) { + StopDamperForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) = 1; + } + if (IsPlaying(channel, 5)) { + StopDirtRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) = 1; + } + if (IsPlaying(channel, 6)) { + StopBumpyRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) = 1; + } + if (IsPlaying(channel, 7)) { + StopSlipperyRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) = 1; + } + if (IsPlaying(channel, 8)) { + StopSurfaceEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) = 1; + } +} + +void LGWheels::StopCarAirborne(long channel) { this->StopForce(channel, 9); } diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp index 971ef1685..3a68866f8 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp @@ -75,17 +75,17 @@ struct Wheels { ~Wheels(); short ReadAll(); - bool ButtonIsPressed(int channel, unsigned long buttonMask); - bool IsConnected(int channel); - bool PedalsConnected(int channel); + bool ButtonIsPressed(long channel, unsigned long buttonMask); + bool IsConnected(long channel); + bool PedalsConnected(long channel); }; struct Force { Force(); void InitVars(); - int Start(int channel, int forceNumber); - int Stop(int channel, int forceNumber); - int Destroy(int channel, int forceNumber); + int Start(long channel, long forceNumber); + int Stop(long channel, long forceNumber); + int Destroy(long channel, long forceNumber); bool Playing[8][4]; unsigned long EffectID[8][4]; @@ -93,20 +93,20 @@ struct Force { struct Condition : public Force { Condition(); - int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); - int UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); + int DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, signed char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); + int UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, signed char offset, unsigned char deadband, unsigned char satNeg, unsigned char satPos, short coeffNeg, short coeffPos); }; struct Constant : public Force { Constant(); - int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); - int UpdateForce(int channel, int forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int UpdateForce(long channel, long forceNumber, unsigned long duration, unsigned long startDelay, short magnitude, unsigned short direction, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); }; struct Periodic : public Force { Periodic(); - int DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); - int UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); + int UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel); }; struct Ramp : public Force { @@ -117,28 +117,39 @@ struct LGWheels { LGWheels(); ~LGWheels(); - void InitVars(int channel); + void InitVars(long channel); void ReadAll(); - void StopForce(int channel, int forceType); - bool IsConnected(int channel); - bool ButtonIsPressed(int channel, unsigned long buttonMask); - bool PedalsConnected(int channel); - void PlayAutoCalibAndSpringForce(int channel); - void StopSpringForce(int channel); - bool SameSpringForceParams(int channel, char offset, unsigned char saturation, short coefficient); - void StopConstantForce(int channel); - bool SameConstantForceParams(int channel, short magnitude, unsigned short direction); - void StopDamperForce(int channel); - bool SameDamperForceParams(int channel, short coefficient); - void StopDirtRoadEffect(int channel); - bool SameDirtRoadEffectParams(int channel, short magnitude); - void StopBumpyRoadEffect(int channel); - bool SameBumpyRoadEffectParams(int channel, short magnitude); - void StopSlipperyRoadEffect(int channel); - bool SameSlipperyRoadEffectParams(int channel, short magnitude); - void StopSurfaceEffect(int channel); - bool SameSurfaceEffectParams(int channel, unsigned char type, unsigned char magnitude, unsigned short period); - void StopCarAirborne(int channel); + void StopForce(long channel, long forceType); + bool IsConnected(long channel); + bool IsPlaying(long channel, long forceType); + bool ButtonIsPressed(long channel, unsigned long buttonMask); + bool PedalsConnected(long channel); + void PlayAutoCalibAndSpringForce(long channel); + void PlaySpringForce(long channel, signed char offset, unsigned char saturation, short coefficient); + void StopSpringForce(long channel); + bool SameSpringForceParams(long channel, signed char offset, unsigned char saturation, short coefficient); + void PlayConstantForce(long channel, short magnitude, unsigned short direction); + void StopConstantForce(long channel); + bool SameConstantForceParams(long channel, short magnitude, unsigned short direction); + void PlayDamperForce(long channel, short coefficient); + void StopDamperForce(long channel); + bool SameDamperForceParams(long channel, short coefficient); + void PlayFrontalCollisionForce(long channel, unsigned char magnitude); + bool SameFrontalCollisionForceParams(long channel, short magnitude); + void PlayDirtRoadEffect(long channel, unsigned char magnitude); + void StopDirtRoadEffect(long channel); + bool SameDirtRoadEffectParams(long channel, short magnitude); + void PlayBumpyRoadEffect(long channel, unsigned char magnitude); + void StopBumpyRoadEffect(long channel); + bool SameBumpyRoadEffectParams(long channel, short magnitude); + void PlaySlipperyRoadEffect(long channel, short magnitude); + void StopSlipperyRoadEffect(long channel); + bool SameSlipperyRoadEffectParams(long channel, short magnitude); + void PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period); + void StopSurfaceEffect(long channel); + bool SameSurfaceEffectParams(long channel, unsigned char type, unsigned char magnitude, unsigned short period); + void PlayCarAirborne(long channel); + void StopCarAirborne(long channel); }; extern LGWheels *plat_lgwheels; diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index e93f1d0b3..28c1d1063 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -18,7 +18,7 @@ static inline unsigned long &PeriodicGetEffectID(Force *self, int channel, int f Periodic::Periodic() : Force() {} -int Periodic::DownloadForce(int channel, int forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { +int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; @@ -54,7 +54,7 @@ int Periodic::DownloadForce(int channel, int forceNumber, unsigned long & handle return ret; } -int Periodic::UpdateForce(int channel, int forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { +int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index c0ff4d9e8..066ce78ad 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -61,17 +61,17 @@ short Wheels::ReadAll() { return wheelUnplugged; } -bool Wheels::ButtonIsPressed(int channel, unsigned long buttonMask) { +bool Wheels::ButtonIsPressed(long channel, unsigned long buttonMask) { const LGPosition *position = reinterpret_cast(this); return (position[channel].button & buttonMask) != 0; } -bool Wheels::IsConnected(int channel) { +bool Wheels::IsConnected(long channel) { const LGPosition *position = reinterpret_cast(this); return position[channel].err != 0; } -bool Wheels::PedalsConnected(int channel) { +bool Wheels::PedalsConnected(long channel) { const LGPosition *position = reinterpret_cast(this); return (position[channel].misc >> 3) & 1; } From 5bb4059dafca61d56689c55e85b1f3b9627f2f0b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:00:13 +0100 Subject: [PATCH 026/172] 21.4%: add JoyE and movie wrapper functions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 56 +++++++++++++++++++++++++++++ src/Speed/GameCube/Src/Movie_GC.cpp | 32 +++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index e69de29bb..910201acc 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -0,0 +1,56 @@ +#include "Logitech/LGWheels.hpp" + +#include + +unsigned char notYetCalibrating[4]; +unsigned char wasWheelConnected[4]; +PADStatus HardwarePadStatus[4]; +int JoystickInitialized; + +void PlatformInitJoystick() { + int i; + + plat_lgwheels = new LGWheels(); + for (i = 0; i < 4; i++) { + notYetCalibrating[i] = 1; + wasWheelConnected[i] = 0; + } + PADRead(HardwarePadStatus); + JoystickInitialized = 1; +} + +void AutoCalibrateWheel(int channel) { + plat_lgwheels->StopConstantForce(channel); + plat_lgwheels->StopSurfaceEffect(channel); + plat_lgwheels->StopDamperForce(channel); + plat_lgwheels->StopCarAirborne(channel); + plat_lgwheels->StopSlipperyRoadEffect(channel); + plat_lgwheels->StopSpringForce(channel); + plat_lgwheels->PlayAutoCalibAndSpringForce(channel); +} + +void ReadLGWheelDataForProgressiveMenu() { + if (plat_lgwheels) { + plat_lgwheels->ReadAll(); + } +} + +unsigned int ReadLGWheelButtonsForProgressiveMenu(int ix) { + unsigned int wheel_buttons; + + wheel_buttons = 0; + if (plat_lgwheels && plat_lgwheels->IsConnected(ix)) { + wheel_buttons = reinterpret_cast(plat_lgwheels)[ix].button; + } + return wheel_buttons; +} + +unsigned int IsWheelActiveForProgressiveMenu(int ix) { + unsigned int wheel_connected; + + wheel_connected = 0; + if (plat_lgwheels) { + wheel_connected = plat_lgwheels->IsConnected(ix); + } + return wheel_connected; +} diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index e69de29bb..653cc63b8 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -0,0 +1,32 @@ +#include "Speed/Indep/Src/Ecstasy/Texture.hpp" + +namespace RealShape { +struct Shape; +} + +struct GCHW_VD { + GCHW_VD(RealShape::Shape *yuvshp, bool isVP6Movie); + ~GCHW_VD(); + void iDraw(); +}; + +GCHW_VD *gGCVD; + +void GCDrawMovie() { + if (gGCVD) { + gGCVD->iDraw(); + } +} + +void PlatSetFirstMovieFrame(TextureInfo *texture_info, RealShape::Shape *yuv_shape, bool isVP6Movie) { + if (!gGCVD) { + gGCVD = new GCHW_VD(yuv_shape, isVP6Movie); + } +} + +void PlatFinishMovie() { + if (gGCVD) { + delete gGCVD; + gGCVD = 0; + } +} From 67f98b04a05aaa1a6837adb97b409b735de4ce02 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:16:32 +0100 Subject: [PATCH 027/172] 25.4%: add platform memorycard and movie owners Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 20 +++ .../GameCube/Src/MemoryCard/MemoryCardImp.cpp | 153 ++++++++++++++++++ src/Speed/GameCube/Src/Movie_GC.cpp | 73 ++++++++- src/Speed/GameCube/Src/Platform_G.cpp | 90 +++++++++++ .../Src/Frontend/MemoryCard/MemoryCard.hpp | 8 + 5 files changed, 343 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index d8f60b57a..cf63fccbd 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -4,6 +4,7 @@ #include "Speed/Indep/bWare/Inc/bWare.hpp" extern SlotPool *eAnimTextureSlotPool; +extern TextureInfo *pTexPrev; static inline unsigned int Convert16To32(unsigned short entry) { if (entry & 0x8000) { @@ -132,3 +133,22 @@ unsigned char TextureInfoPlatInfo::SetImage(TextureInfo *texture_info) { texture_info->PaletteData, texture_info->AlphaUsageType, texture_info->TilableUV); return 1; } + +int eSetTexture(TextureInfo *texture_info, int stage) { + static int stagePrev; + TextureInfoPlatInfo *plat_info = texture_info->GetPlatInfo(); + + if (texture_info == pTexPrev && stage == stagePrev) { + return 0; + } + + if (plat_info->HasClut()) { + GXLoadTlut(&plat_info->ImageInfos.objClut, 0); + } + + GXLoadTexObj(&plat_info->ImageInfos.obj, static_cast(stage)); + + pTexPrev = texture_info; + stagePrev = stage; + return 1; +} diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index e69de29bb..da932204c 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -0,0 +1,153 @@ +#include "Speed/Indep/Src/FEng/FEObject.h" +#include "Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp" + +namespace RealmcIface { +enum CardStatus { + STATUS_OK = 0, + STATUS_NO_CARD = 1, + STATUS_CARD_CHANGED = 2, + STATUS_CARD_UNFORMATTED = 3, + STATUS_CARD_DAMAGED = 4, + STATUS_WRONG_DEVICE = 5, + STATUS_CARD_FULL = 6, + STATUS_ACCESS_DENIED = 7, + STATUS_INSUFFICIENT_SPACE = 8, + STATUS_FILE_NOT_FOUND = 9, + STATUS_ENTRY_NOT_FOUND = 10, + STATUS_ENTRY_ALREADY_EXISTS = 11, + STATUS_FILE_NOT_OPENED = 12, + STATUS_FILE_CORRUPTED = 13, + STATUS_DIRECTORY_NOT_FOUND = 14, + STATUS_DIRECTORY_NOT_EMPTY = 15, + STATUS_TOO_MANY_OPENED_FILES = 16, + STATUS_CANNOTMOUNT = 17, + STATUS_FILE_DELETED = 18, + STATUS_RANGE_ERROR = 19, + STATUS_CARD_REMOVED = 20, + STATUS_INACCESSIBLE_CARD = 21, + STATUS_EXIT_TO_CARD_MANAGER = 22, + STATUS_FAILED = 23, + STATUS_UNKNOWN = -1, +}; + +struct BootupCheckResults; +struct GCIconDataInfo; +struct GCBannerDataInfo; + +struct Ps2SaveInfo { + const char *mIconSysData; + unsigned int mIconSysDataSize; + const char *mStaticIconData; + unsigned int mStaticIconDataSize; + const char *mStaticIconFilename; + const char *mCopyIconData; + unsigned int mCopyIconDataSize; + const char *mCopyIconFilename; + const char *mDeleteIconData; + unsigned int mDeleteIconDataSize; + const char *mDeleteIconFilename; +}; + +struct XboxSaveInfo { + const char *mImageData; + unsigned int mImageDataSize; +}; + +struct GcSaveInfo { + const char *mComment1; + unsigned int mComment1Size; + const char *mComment2; + unsigned int mComment2Size; + GCIconDataInfo *mIconDataInfo; + GCBannerDataInfo *mBannerDataInfo; +}; + +struct SaveInfo { + SaveInfo(); + + Ps2SaveInfo mPs2Info; + XboxSaveInfo mXboxInfo; + GcSaveInfo mGcInfo; + unsigned int mHeaderSize; + unsigned int mBodySize; + const unsigned short *mTypeName; + const unsigned short *mContentName; +}; + +struct SaveReq { + unsigned int mNumSaves; + SaveInfo *mSaveInfo; +}; +} // namespace RealmcIface + +class cFEng { + public: + static cFEng *mInstance; + + void QueuePackageMessage(unsigned int pMessage, const char *pPackageName, FEObject *obj); +}; + +struct MemoryCardImp { + static unsigned short *gEntryType[MemoryCard::ST_MAX]; + static unsigned short gContentName[]; + + RealmcIface::SaveReq *m_pSaveReq; + RealmcIface::SaveReq m_SaveReq; + + RealmcIface::SaveInfo *ConstructSaveInfo(MemoryCard::SaveType type, const char *DisplayName, int aSize); + void DestructSaveInfo(); + void BootupCheckDone(RealmcIface::CardStatus status, RealmcIface::BootupCheckResults *pParam); +}; + +char *bStrCpy(char *dst, const char *src); +int bStrLen(const char *str); +MemoryCard *GetMemcard(); + +extern const char *gComment1; + +RealmcIface::SaveInfo *MemoryCardImp::ConstructSaveInfo(MemoryCard::SaveType type, const char *DisplayName, int aSize) { + static char sDisplayName[32]; + RealmcIface::SaveInfo *save_info = new RealmcIface::SaveInfo; + + if (type == MemoryCard::ST_PROFILE) { + bStrCpy(sDisplayName, DisplayName); + } + + save_info->mGcInfo.mComment1 = gComment1; + save_info->mGcInfo.mComment1Size = bStrLen(gComment1); + save_info->mGcInfo.mComment2 = sDisplayName; + save_info->mGcInfo.mComment2Size = bStrLen(sDisplayName); + save_info->mGcInfo.mIconDataInfo = + *reinterpret_cast(reinterpret_cast(MemoryCard::s_pThis) + 0x10); + save_info->mGcInfo.mBannerDataInfo = + *reinterpret_cast(reinterpret_cast(MemoryCard::s_pThis) + 0x14); + this->m_SaveReq.mSaveInfo = save_info; + save_info->mContentName = MemoryCardImp::gContentName; + save_info->mTypeName = MemoryCardImp::gEntryType[type]; + save_info->mHeaderSize = 8; + save_info->mBodySize = aSize; + return save_info; +} + +void MemoryCardImp::DestructSaveInfo() { + if (this->m_SaveReq.mSaveInfo) { + delete this->m_SaveReq.mSaveInfo; + this->m_SaveReq.mSaveInfo = 0; + } +} + +void MemoryCardImp::BootupCheckDone(RealmcIface::CardStatus status, RealmcIface::BootupCheckResults *pParam) { + MemoryCard *memcard = GetMemcard(); + + if (*reinterpret_cast(reinterpret_cast(memcard) + 0x30)) { + void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); + const char *package_name = *reinterpret_cast(reinterpret_cast(screen) + 0xC); + + if (status == RealmcIface::STATUS_NO_CARD || status == RealmcIface::STATUS_CARD_DAMAGED || + status == RealmcIface::STATUS_WRONG_DEVICE || status == RealmcIface::STATUS_CARD_FULL) { + cFEng::mInstance->QueuePackageMessage(0x8867412D, package_name, 0); + } else { + cFEng::mInstance->QueuePackageMessage(0x3A2BE557, package_name, 0); + } + } +} diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 653cc63b8..2915ff486 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -1,10 +1,52 @@ #include "Speed/Indep/Src/Ecstasy/Texture.hpp" +#include "dolphin.h" + +struct ElementInfo { + unsigned int mType : 8; + unsigned int mFlags : 24; +}; + +struct ShapeElement { + ElementInfo mElementInfo; + int mNextOffset; + int mDataOffset; + int mDataSize; +}; + +struct TextureElement : public ShapeElement { + int mShapeX; + int mShapeY; + int mWidth; + int mHeight; + + int GetWidth() const { + return this->mWidth; + } + + int GetHeight() const { + return this->mHeight; + } +}; namespace RealShape { -struct Shape; +struct Shape { + TextureElement *GetTexture() const; +}; } +struct tBigYUVSwizzler; +struct tBigYUVSwizzler *NEW_tBigYUVSwizzlerTexture(GXTexObj *tYTexp, GXTexObj *tUTexp, GXTexObj *tVTexp) + __asm__("NEW_tBigYUVSwizzlerTexture__FP9_GXTexObjN20"); +void DELETE_tBigYUVSwizzler(struct tBigYUVSwizzler *This); + struct GCHW_VD { + tBigYUVSwizzler *m_pYUVSwizzler; + GXTexObj YTexObj; + GXTexObj CbTexObj; + GXTexObj CrTexObj; + RealShape::Shape *mCurrentFrame; + bool mIsVP6; + GCHW_VD(RealShape::Shape *yuvshp, bool isVP6Movie); ~GCHW_VD(); void iDraw(); @@ -12,6 +54,35 @@ struct GCHW_VD { GCHW_VD *gGCVD; +GCHW_VD::GCHW_VD(RealShape::Shape *yuvshp, bool isVP6Movie) : mIsVP6(isVP6Movie) { + TextureElement *texture = yuvshp->GetTexture(); + int w = texture->GetWidth(); + int h = texture->GetHeight(); + + if (mIsVP6) { + w += 0x60; + h += 0x60; + } + + GXInitTexObjCI(&YTexObj, 0, static_cast(w), static_cast(h), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 2); + GXInitTexObjLOD(&YTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + + w /= 2; + h /= 2; + + GXInitTexObjCI(&CrTexObj, 0, static_cast(w), static_cast(h), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 1); + GXInitTexObjLOD(&CrTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + + GXInitTexObjCI(&CbTexObj, 0, static_cast(w), static_cast(h), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 0); + GXInitTexObjLOD(&CbTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + + m_pYUVSwizzler = NEW_tBigYUVSwizzlerTexture(&YTexObj, &CrTexObj, &CbTexObj); +} + +GCHW_VD::~GCHW_VD() { + DELETE_tBigYUVSwizzler(m_pYUVSwizzler); +} + void GCDrawMovie() { if (gGCVD) { gGCVD->iDraw(); diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 1e34afc62..6cb6f1f74 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -1,6 +1,8 @@ #include "Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp" #include "Speed/Indep/Src/Misc/BuildRegion.hpp" #include "Speed/Indep/Src/Misc/Platform.h" +#include "Speed/Indep/bWare/Inc/bMemory.hpp" +#include #include "dolphin.h" enum VIDEO_MODE { @@ -35,6 +37,94 @@ enum eLanguages { extern bool bEURGB60; extern "C" void OSResetSystem(BOOL reset, u32 resetCode, BOOL forceMenu); +struct FILESYSOPTS { + int size; + EA::Allocator::IAllocator *allocator; + int MaxOpenFiles; + int MaxFileOps; + int nSearchLocs; + int nSearchPathLength; + int MaxDevices; + int ThreadStackSize; + int (*decompresssize)(const void *); + int (*decompress)(const void *, void *); + unsigned int LargeReadSliceSize; + unsigned int AllocAlignBoundary; + int DiscType; + int mErrorRetryCount; +}; + +void bWareInit(); +void bMemoryInit(); +void THREAD_init(); +void TIMER_init(int slice); +void FILE_getopts(FILESYSOPTS *opts); +void FILE_setopts(FILESYSOPTS *opts); +void FILE_init(void *allocator, int allowAsync); +void ASYNCFILE_init(int numFiles, int unk); +void SYNCTASK_add(void (*task)(void *, int), int priority, int unk, void *user); +void fn_80311870(int, void *, int); + +extern EA::Allocator::IAllocator &gMemoryAllocator; + +void *arenaLo; +unsigned int g_GC_Disk_GameName; +int snProfilerEnable; + +void InitPlatform() { + static char profdata[0x2000]; + FILESYSOPTS opts; + + bWareInit(); + OSInit(); + DVDInit(); + VIInit(); + PADInit(); + arenaLo = OSGetArenaLo(); + bMemoryInit(); + THREAD_init(); + TIMER_init(0x64); + g_GC_Disk_GameName = *reinterpret_cast(DVDGetCurrentDiskID()); + + opts.size = 0x38; + FILE_getopts(&opts); + opts.allocator = &gMemoryAllocator; + opts.MaxOpenFiles = 0x20; + opts.MaxFileOps = 0x40; + FILE_setopts(&opts); + + FILE_init(0, 0); + ASYNCFILE_init(0x10, 0); + SYNCTASK_add(DVDErrorTask, 2, 0, 0); + + asm volatile( + "li 3, 4\n\t" + "oris 3, 3, 4\n\t" + "mtspr 914, 3\n\t" + "li 3, 5\n\t" + "oris 3, 3, 5\n\t" + "mtspr 915, 3\n\t" + "li 3, 6\n\t" + "oris 3, 3, 6\n\t" + "mtspr 916, 3\n\t" + "li 3, 7\n\t" + "oris 3, 3, 7\n\t" + "mtspr 917, 3\n\t" + "lis 9, 0x0B07\n\t" + "ori 9, 9, 0x0B07\n\t" + "mtspr 917, 9\n\t" + "lis 11, 0x0704\n\t" + "ori 11, 11, 0x0704\n\t" + "mtspr 918, 11\n\t" + "lis 9, 0x0606\n\t" + "ori 9, 9, 0x0606\n\t" + "mtspr 919, 9"); + + if (snProfilerEnable) { + fn_80311870(0x278D, profdata, 0x2000); + } +} + void FlushCaches() { PPCSync(); } diff --git a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp index d0aef81f4..3ee8e1c38 100644 --- a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp +++ b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp @@ -7,6 +7,14 @@ class MemoryCard { public: + enum SaveType { + ST_PROFILE = 0, + ST_THUMBNAIL = 1, + ST_IMAGE = 2, + ST_MAX = 3, + }; + + static MemoryCard *s_pThis; static int IsCardBusy(); }; From 6210752e8a93831b07ef597f60db4ac6d9933065 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:26:01 +0100 Subject: [PATCH 028/172] 28.4%: add ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 174 ++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 910201acc..3173f7ab9 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -1,12 +1,94 @@ #include "Logitech/LGWheels.hpp" +#include "Speed/Indep/Src/Misc/Timer.hpp" +#include "Speed/Indep/bWare/Inc/bMemory.hpp" #include +struct PadData { + unsigned short DigitalButtons; + unsigned char LTrigger; + unsigned char RTrigger; + unsigned char AnalogLeftX; + unsigned char AnalogLeftY; + unsigned char AnalogRightX; + unsigned char AnalogRightY; + unsigned char Error; + unsigned char Type; +}; + +struct JoyData { + PadData ThePadData[4]; + unsigned char Bytes[4]; + int RegularControllerType; + PADStatus padSTATUS; +}; + +static const unsigned long PADMASKS[4] = { + PAD_CHAN0_BIT, + PAD_CHAN1_BIT, + PAD_CHAN2_BIT, + PAD_CHAN3_BIT, +}; + +extern Timer RealTimer; + +JoyData PadRingData[4][32]; +int JoystickRingBufferTop; +int JoystickRingBufferBottom; unsigned char notYetCalibrating[4]; unsigned char wasWheelConnected[4]; PADStatus HardwarePadStatus[4]; +float calibrationTimer[4]; +float lastCalibTime[4]; int JoystickInitialized; +static inline unsigned short ConvertPadButtons(unsigned short buttons) { + unsigned short converted = 0xFFFF; + + if (buttons & PAD_BUTTON_A) { + converted &= ~0x0001; + } + if (buttons & PAD_BUTTON_B) { + converted &= ~0x0002; + } + if (buttons & PAD_BUTTON_X) { + converted &= ~0x0004; + } + if (buttons & PAD_BUTTON_Y) { + converted &= ~0x0008; + } + if (buttons & PAD_TRIGGER_Z) { + converted &= ~0x0010; + } + if (buttons & PAD_BUTTON_START) { + converted &= ~0x0020; + } + if (buttons & PAD_BUTTON_UP) { + converted &= ~0x0100; + } + if (buttons & PAD_BUTTON_DOWN) { + converted &= ~0x0200; + } + if (buttons & PAD_BUTTON_LEFT) { + converted &= ~0x0400; + } + if (buttons & PAD_BUTTON_RIGHT) { + converted &= ~0x0800; + } + + return converted; +} + +static inline unsigned char ClampAnalogValue(int value) { + if (value < 0) { + return 0; + } + if (value > 0xFF) { + return 0xFF; + } + return value; +} + void PlatformInitJoystick() { int i; @@ -54,3 +136,95 @@ unsigned int IsWheelActiveForProgressiveMenu(int ix) { } return wheel_connected; } + +int ActualReadJoystickData() { + int nNewTop; + int port; + + if (!JoystickInitialized) { + return 0; + } + + nNewTop = (JoystickRingBufferTop + 1) & 0x1F; + if (nNewTop == JoystickRingBufferBottom) { + return 0; + } + + PADRead(HardwarePadStatus); + PADClamp(HardwarePadStatus); + plat_lgwheels->ReadAll(); + + for (port = 0; port <= 3; port++) { + JoyData *joy_data = &PadRingData[port][JoystickRingBufferTop]; + PadData *pad_data = &joy_data->ThePadData[0]; + + bMemSet(joy_data, 0xFF, sizeof(JoyData)); + + if (HardwarePadStatus[port].err == PAD_ERR_NONE) { + joy_data->padSTATUS = HardwarePadStatus[port]; + pad_data->Type = 0x41; + pad_data->Error = 0; + pad_data->DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); + pad_data->AnalogRightX = ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); + pad_data->AnalogRightY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); + pad_data->AnalogLeftX = ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); + pad_data->AnalogLeftY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); + pad_data->LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); + pad_data->RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); + } else if (plat_lgwheels->IsConnected(port)) { + const LGPosition *wheel_position; + int wheel_connected; + int pedals_connected; + + wheel_connected = wasWheelConnected[port]; + if (!wheel_connected) { + wasWheelConnected[port] = 1; + calibrationTimer[port] = 7.0f; + lastCalibTime[port] = RealTimer.GetSeconds(); + } + + if (calibrationTimer[port] < 5.0f && notYetCalibrating[port]) { + AutoCalibrateWheel(port); + notYetCalibrating[port] = 0; + } + + if (calibrationTimer[port] > 0.0f) { + float now = RealTimer.GetSeconds(); + float elapsed = now - lastCalibTime[port]; + lastCalibTime[port] = now; + calibrationTimer[port] -= elapsed; + } + + wheel_position = &reinterpret_cast(plat_lgwheels)[port]; + joy_data->padSTATUS.button = wheel_position->button; + HardwarePadStatus[port].button = wheel_position->button; + pad_data->Error = 0; + pedals_connected = plat_lgwheels->PedalsConnected(port); + pad_data->Type = pedals_connected ? 0x51 : 0x50; + pad_data->DigitalButtons = ConvertPadButtons(wheel_position->button); + pad_data->AnalogRightX = 0; + pad_data->AnalogLeftX = wheel_position->wheel + 0x80; + + if (pedals_connected) { + pad_data->AnalogRightY = wheel_position->accelerator; + pad_data->AnalogLeftY = wheel_position->brake; + } else { + pad_data->AnalogLeftY = 0; + pad_data->AnalogRightY = 0; + } + + pad_data->LTrigger = wheel_position->triggerLeft; + pad_data->RTrigger = wheel_position->triggerRight; + } else { + pad_data->Type = 0xFF; + wasWheelConnected[port] = 0; + notYetCalibrating[port] = 1; + pad_data->Error = 1; + PADReset(PADMASKS[port]); + HardwarePadStatus[port].button = 0; + } + } + + JoystickRingBufferTop = nNewTop; + return 1; +} From 5f054479e877907375a6773bbbb1d225b4863657 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:30:50 +0100 Subject: [PATCH 029/172] 29.3%: add zPlatform helper owners Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GameCube/Src/MemoryCard/MemoryCardImp.cpp | 9 +++ src/Speed/GameCube/Src/Movie_GC.cpp | 10 +++ src/Speed/GameCube/Src/Platform_G.cpp | 61 +++++++++++++++++++ src/Speed/GameCube/Src/Render/SunE.cpp | 18 ++++++ 4 files changed, 98 insertions(+) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index da932204c..4dbb86deb 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -94,6 +94,7 @@ struct MemoryCardImp { RealmcIface::SaveReq *m_pSaveReq; RealmcIface::SaveReq m_SaveReq; + const char *GetPrefix(); RealmcIface::SaveInfo *ConstructSaveInfo(MemoryCard::SaveType type, const char *DisplayName, int aSize); void DestructSaveInfo(); void BootupCheckDone(RealmcIface::CardStatus status, RealmcIface::BootupCheckResults *pParam); @@ -105,6 +106,14 @@ MemoryCard *GetMemcard(); extern const char *gComment1; +MemoryCard *GetMemcard() { + return MemoryCard::s_pThis; +} + +const char *MemoryCardImp::GetPrefix() { + return "NFSMW"; +} + RealmcIface::SaveInfo *MemoryCardImp::ConstructSaveInfo(MemoryCard::SaveType type, const char *DisplayName, int aSize) { static char sDisplayName[32]; RealmcIface::SaveInfo *save_info = new RealmcIface::SaveInfo; diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 2915ff486..5ec7f7c63 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -52,6 +52,10 @@ struct GCHW_VD { void iDraw(); }; +struct MoviePlayer { + void FillInTextureInfo(unsigned int *buffer, TextureInfo *texture_info, RealShape::Shape *yuv_shape); +}; + GCHW_VD *gGCVD; GCHW_VD::GCHW_VD(RealShape::Shape *yuvshp, bool isVP6Movie) : mIsVP6(isVP6Movie) { @@ -83,6 +87,12 @@ GCHW_VD::~GCHW_VD() { DELETE_tBigYUVSwizzler(m_pYUVSwizzler); } +void MoviePlayer::FillInTextureInfo(unsigned int *buffer, TextureInfo *texture_info, RealShape::Shape *yuv_shape) { + if (gGCVD) { + gGCVD->mCurrentFrame = yuv_shape; + } +} + void GCDrawMovie() { if (gGCVD) { gGCVD->iDraw(); diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 6cb6f1f74..6eea73d70 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -199,3 +199,64 @@ void eInitTexture() {} void eUnSwizzle8bitPalette(unsigned int *palette) {} void eSwizzle8bitPalette(unsigned int *palette) {} + +struct VMStats { + unsigned int mNumPageFaults; + unsigned int mNumWritebacks; + float mElapsedTime; + unsigned int mServiceTimeMicroSecs; + unsigned int mServiceTimeMin; + unsigned int mServiceTimeMax; + float mServiceTimeAvg; + + void Init(); +}; + +struct VMStatsManager { + bool mInitialized; + unsigned long long mFrameCounter; + VMStats mFrameStats; + float mElapsedTime; + unsigned int mAccumService_us; + unsigned int mAccumNumFaults; + float mMinServicePercentPerFrame; + float mMaxServicePercentPerFrame; + unsigned int mMinNumServicesPerFrame; + unsigned int mMaxNumServicesPerFrame; + float mMinFrameTime; + float mMaxFrameTime; + const char *DebugName; + + void Init(const char *name); +}; + +void VMStats::Init() { + mServiceTimeMax = 0; + mServiceTimeAvg = 0.0f; + mServiceTimeMin = static_cast(-1); + mNumPageFaults = 0; + mNumWritebacks = 0; + mElapsedTime = 0.0f; + mServiceTimeMicroSecs = 0; +} + +void VMStatsManager::Init(const char *name) { + mFrameStats.mNumPageFaults = 0; + mFrameStats.mNumWritebacks = 0; + DebugName = name; + mFrameStats.mServiceTimeMin = static_cast(-1); + mFrameCounter = 0; + mElapsedTime = 0.0f; + mMinNumServicesPerFrame = 9999999; + mMaxNumServicesPerFrame = 0; + mMinFrameTime = 9999999.0f; + mMaxFrameTime = -9999999.0f; + mFrameStats.mElapsedTime = 0.0f; + mFrameStats.mServiceTimeMicroSecs = 0; + mFrameStats.mServiceTimeMax = 0; + mFrameStats.mServiceTimeAvg = 0.0f; + mAccumService_us = 0; + mAccumNumFaults = 0; + mMinServicePercentPerFrame = 9999999.0f; + mMaxServicePercentPerFrame = -9999999.0f; +} diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index e69de29bb..1054edf2f 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -0,0 +1,18 @@ +#include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/World/Sun.hpp" + +SunLayer vis_layer_fix; +ePoly sun_vis_poly_fix; + +void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float size, float x, float y); + +void eInitSunPat() { + vis_layer_fix.Texture = SUNTEX_CENTER; + vis_layer_fix.IntensityScale = 32.0f; + vis_layer_fix.Size = 1.0f; + vis_layer_fix.Angle = 0; + vis_layer_fix.OffsetX = 0.0f; + vis_layer_fix.OffsetY = 0.0f; + vis_layer_fix.SweepAngleAmount = 0.0f; + eBuildSunPolyFix(&sun_vis_poly_fix, &vis_layer_fix, 1.0f, 0.0f, 0.0f); +} From 14dcaa933b6b8157da4ff6c50db10e2776aeed40 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:35:45 +0100 Subject: [PATCH 030/172] 30.6%: add TextureInfoPlatInfo::SetImage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index cf63fccbd..65ce39b07 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -134,6 +134,55 @@ unsigned char TextureInfoPlatInfo::SetImage(TextureInfo *texture_info) { return 1; } +unsigned char TextureInfoPlatInfo::SetImage(int width, int height, int mip, int format, void *imageData, void *imagePal, + int alphaUsageType, int clamp) { + int wrap_s = 0; + int wrap_t = 0; + int texture_format = format & 0x7FFFFFFF; + GXTlutFmt tlut_format = static_cast(format >= 0 ? GX_TL_RGB5A3 : GX_TL_IA8); + + if (clamp & 1) { + int width_lsb = width & -width; + + if (width == width_lsb) { + wrap_s = 1; + } + } + + if (clamp & 2) { + int height_lsb = height & -height; + + if (height == height_lsb) { + wrap_t = 1; + } + } + + if (HasClut()) { + GXTexObj *obj = &ImageInfos.obj; + + GXInitTexObjCI(obj, imageData, static_cast(width), static_cast(height), static_cast(texture_format), + static_cast(wrap_s), static_cast(wrap_t), static_cast(mip), 0); + GXInitTlutObj(&ImageInfos.objClut, imagePal, tlut_format, texture_format == GX_TF_C4 ? 0x10 : 0x100); + } else { + GXInitTexObj(&ImageInfos.obj, imageData, static_cast(width), static_cast(height), static_cast(texture_format), + static_cast(wrap_s), static_cast(wrap_t), static_cast(mip)); + } + + if (mip) { + float max_lod = static_cast(mip - 1); + + if (alphaUsageType && max_lod > 1.0f) { + max_lod -= 1.0f; + } + + GXInitTexObjLOD(&ImageInfos.obj, GX_LIN_MIP_LIN, GX_LINEAR, 0.0f, max_lod, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + } else { + GXInitTexObjLOD(&ImageInfos.obj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + } + + return 1; +} + int eSetTexture(TextureInfo *texture_info, int stage) { static int stagePrev; TextureInfoPlatInfo *plat_info = texture_info->GetPlatInfo(); From 42dd3b9e973da8ad9d48c3f3acb6d26a58f41798 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:51:34 +0100 Subject: [PATCH 031/172] 34.9%: add SunE visibility helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 232 ++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 1054edf2f..2b3d67d0f 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -1,10 +1,240 @@ #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" #include "Speed/Indep/Src/World/Sun.hpp" +#include "dolphin.h" SunLayer vis_layer_fix; ePoly sun_vis_poly_fix; +float sun_vis_poly_fix_ini[16]; -void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float size, float x, float y); +extern float SunPosX; +extern float SunPosY; +extern float SunVisibility; +extern int DoSunVisibility; +extern float SunMaxIntensity; +extern unsigned int eFrameCounter; + +int eGetScreenWidth(); +int eGetScreenHeight(); +u16 GXReadDrawSync(); +void eSetColourUpdate(Bool bRGB, Bool bAlpha); +void eSetOrthographicMatrixToHW(); +void eRecalculateOthographicProjection(int view_id, float far_clip); +void SetCurrentSunInfo(); + +void RenderViewPoly(eView *view, ePoly *poly, TextureInfo *texture_info, int flags) + __asm__("Render__18eViewPlatInterfaceP5ePolyP11TextureInfoi"); +void GetScreenPosition(eView *view, bVector3 *screen, const bVector3 *world) + __asm__("GetScreenPosition__18eViewPlatInterfaceP8bVector3PC8bVector3"); +void eMulVector(bVector4 *dst, const bMatrix4 *matrix, const bVector4 *src) __asm__("eMulVector__FP8bVector4PC8bMatrix4PC8bVector4"); +void ConstructePoly(ePoly *poly) __asm__("__5ePoly"); + +void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float y); +void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y); +void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y); +void eCalcSunVisibility(eView *view, float x, float y); + +void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { + float screen_width = static_cast(eGetScreenWidth()); + float half_size; + float sin_angle; + float cos_angle; + float diagonal0; + float diagonal1; + float intensity; + float center_x; + float center_y; + unsigned char alpha; + unsigned short angle; + + eGetScreenHeight(); + + if (layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + max_size = layer->Size; + } + + half_size = layer->Size * 0.5f; + angle = static_cast( + layer->Angle + + static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) + ); + sin_angle = bSin(angle); + cos_angle = bCos(angle); + + poly->Vertices[0].z = 1.0f; + poly->Vertices[1].z = 1.0f; + poly->Vertices[2].z = 1.0f; + poly->Vertices[3].z = 1.0f; + + diagonal0 = half_size * cos_angle; + diagonal1 = half_size * sin_angle; + intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; + center_x = x + layer->OffsetX; + center_y = y + layer->OffsetY; + + if (intensity >= 28.0f) { + alpha = static_cast(static_cast(intensity - 28.0f)); + } else { + alpha = static_cast(static_cast(intensity)); + } + + poly->Vertices[3].x = center_x - (diagonal0 - diagonal1); + poly->Vertices[0].x = center_x - (diagonal1 + diagonal0); + poly->Vertices[3].y = center_y + (diagonal1 + diagonal0); + poly->Vertices[0].y = center_y - (diagonal0 - diagonal1); + poly->Vertices[1].y = center_y - (diagonal1 + diagonal0); + poly->Vertices[1].x = center_x + (diagonal0 - diagonal1); + poly->Vertices[2].x = center_x + (diagonal1 + diagonal0); + poly->Vertices[2].y = center_y + (diagonal0 - diagonal1); + + poly->Colours[3][3] = alpha; + poly->Colours[3][0] = layer->Colour[0]; + poly->Colours[3][1] = layer->Colour[1]; + poly->Colours[3][2] = layer->Colour[2]; + poly->Colours[0][0] = layer->Colour[0]; + poly->Colours[0][1] = layer->Colour[1]; + poly->Colours[0][2] = layer->Colour[2]; + poly->Colours[0][3] = alpha; + poly->Colours[1][0] = layer->Colour[0]; + poly->Colours[1][1] = layer->Colour[1]; + poly->Colours[1][2] = layer->Colour[2]; + poly->Colours[1][3] = alpha; + poly->Colours[2][0] = layer->Colour[0]; + poly->Colours[2][1] = layer->Colour[1]; + poly->Colours[2][2] = layer->Colour[2]; + poly->Colours[2][3] = alpha; +} + +void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { + float screen_width = static_cast(eGetScreenWidth()); + float half_size; + float sin_angle; + float cos_angle; + float diagonal0; + float diagonal1; + float intensity; + float center_x; + float center_y; + unsigned char alpha; + unsigned short angle; + + eGetScreenHeight(); + + if (layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + max_size = layer->Size; + } + + half_size = layer->Size * 0.5f; + angle = static_cast( + layer->Angle + + static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) + ); + sin_angle = bSin(angle); + cos_angle = bCos(angle); + + sun_vis_poly_fix_ini[2] = 1.0f; + poly->Vertices[1].z = 1.0f; + poly->Vertices[2].z = sun_vis_poly_fix_ini[2]; + poly->Vertices[3].z = sun_vis_poly_fix_ini[2]; + + diagonal0 = half_size * cos_angle; + diagonal1 = half_size * sin_angle; + intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; + center_x = x + layer->OffsetX; + center_y = y + layer->OffsetY; + + if (intensity >= 28.0f) { + alpha = static_cast(static_cast(intensity - 28.0f)); + } else { + alpha = static_cast(static_cast(intensity)); + } + + poly->Vertices[3].x = center_x - (diagonal0 - diagonal1); + poly->Vertices[3].y = center_y + (diagonal1 + diagonal0); + poly->Vertices[0].y = center_y - (diagonal0 - diagonal1); + sun_vis_poly_fix_ini[0] = center_x - (diagonal1 + diagonal0); + poly->Vertices[0].x = sun_vis_poly_fix_ini[0]; + poly->Vertices[1].y = center_y - (diagonal1 + diagonal0); + poly->Vertices[1].x = center_x + (diagonal0 - diagonal1); + poly->Vertices[2].y = center_y + (diagonal0 - diagonal1); + poly->Vertices[2].x = center_x + (diagonal1 + diagonal0); + + sun_vis_poly_fix_ini[1] = poly->Vertices[0].y; + sun_vis_poly_fix_ini[4] = poly->Vertices[1].x; + sun_vis_poly_fix_ini[5] = poly->Vertices[1].y; + sun_vis_poly_fix_ini[8] = poly->Vertices[2].x; + sun_vis_poly_fix_ini[9] = poly->Vertices[2].y; + sun_vis_poly_fix_ini[12] = poly->Vertices[3].x; + sun_vis_poly_fix_ini[13] = poly->Vertices[3].y; + + poly->Colours[0][0] = layer->Colour[0]; + poly->Colours[0][1] = layer->Colour[1]; + poly->Colours[0][2] = layer->Colour[2]; + poly->Colours[0][3] = alpha; + poly->Colours[1][0] = layer->Colour[0]; + poly->Colours[1][1] = layer->Colour[1]; + poly->Colours[1][2] = layer->Colour[2]; + poly->Colours[1][3] = alpha; + poly->Colours[2][0] = layer->Colour[0]; + poly->Colours[2][1] = layer->Colour[1]; + poly->Colours[2][2] = layer->Colour[2]; + poly->Colours[2][3] = alpha; + poly->Colours[3][0] = layer->Colour[0]; + poly->Colours[3][1] = layer->Colour[1]; + poly->Colours[3][2] = layer->Colour[2]; + poly->Colours[3][3] = alpha; +} + +void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { + float intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; + unsigned char alpha; + + if (intensity >= 28.0f) { + alpha = static_cast(static_cast(intensity - 28.0f)); + } else { + alpha = static_cast(static_cast(intensity)); + } + + poly->Vertices[0].x = sun_vis_poly_fix_ini[0] + x; + poly->Vertices[0].y = sun_vis_poly_fix_ini[1] + y; + poly->Vertices[1].x = sun_vis_poly_fix_ini[4] + x; + poly->Vertices[1].y = sun_vis_poly_fix_ini[5] + y; + poly->Vertices[2].x = sun_vis_poly_fix_ini[8] + x; + poly->Vertices[2].y = sun_vis_poly_fix_ini[9] + y; + poly->Vertices[3].x = sun_vis_poly_fix_ini[12] + x; + poly->Colours[3][3] = alpha; + poly->Colours[0][3] = alpha; + poly->Vertices[3].y = sun_vis_poly_fix_ini[13] + y; + poly->Colours[1][3] = alpha; + poly->Colours[2][3] = alpha; +} + +void eCalcSunVisibility(eView *view, float x, float y) { + if (DoSunVisibility) { + if (eFrameCounter == (eFrameCounter / 5) * 5) { + u32 top_in; + u32 top_out; + u32 bottom_in; + u32 bottom_out; + u32 clear_in; + u32 copy_clocks; + + eUpdateSunPolyFix(&sun_vis_poly_fix, &vis_layer_fix, 1.0f, x, y); + GXClearPixMetric(); + eSetColourUpdate(0, 0); + RenderViewPoly(view, &sun_vis_poly_fix, DefaultTextureInfo, 0); + GXSetDrawSync(0xBEEF); + GXFlush(); + + while (GXReadDrawSync() != 0xBEEF) { + // nop + } + + GXReadPixMetric(&top_in, &top_out, &bottom_in, &bottom_out, &clear_in, ©_clocks); + eSetColourUpdate(1, 1); + SunVisibility = static_cast(top_out) / (static_cast(top_in) + 1.0f); + } + } +} void eInitSunPat() { vis_layer_fix.Texture = SUNTEX_CENTER; From ca2e7e6339b460087de83554682d79593cfb9bc5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Tue, 17 Mar 2026 23:54:01 +0100 Subject: [PATCH 032/172] 36.7%: add SunE render path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 2b3d67d0f..8f108b5be 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -1,5 +1,7 @@ #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/Misc/GameFlow.hpp" #include "Speed/Indep/Src/World/Sun.hpp" +#include "Speed/Indep/Src/Camera/Camera.hpp" #include "dolphin.h" SunLayer vis_layer_fix; @@ -12,6 +14,7 @@ extern float SunVisibility; extern int DoSunVisibility; extern float SunMaxIntensity; extern unsigned int eFrameCounter; +extern TextureInfo *SunTextures[5]; int eGetScreenWidth(); int eGetScreenHeight(); @@ -32,6 +35,7 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y); void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y); void eCalcSunVisibility(eView *view, float x, float y); +void eRenderSun(eView *view); void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { float screen_width = static_cast(eGetScreenWidth()); @@ -236,6 +240,73 @@ void eCalcSunVisibility(eView *view, float x, float y) { } } +void eRenderSun(eView *view) { + SetCurrentSunInfo(); + + if (IsGameFlowInGame()) { + SunChunkInfo *sun_info = SunInfo; + Camera *camera = view->GetCamera(); + bMatrix4 *world_view = camera->GetCameraMatrix(); + bVector4 position3d; + bVector4 position2d; + bVector4 view3d; + float screen_widthf; + float screen_heightf; + float x; + float y; + float max_size; + + position3d.x = sun_info->PositionX; + position3d.y = sun_info->PositionY; + position3d.z = sun_info->PositionZ; + position3d.w = 1.0f; + GetScreenPosition(view, reinterpret_cast(&position2d), reinterpret_cast(&position3d)); + eMulVector(&view3d, world_view, &position3d); + + screen_widthf = static_cast(eGetScreenWidth()); + screen_heightf = static_cast(eGetScreenHeight()); + + if (SunPosX != 0.0f || SunPosY != 0.0f) { + x = SunPosX; + y = SunPosY; + } else { + x = position2d.x; + y = position2d.y; + } + + max_size = 0.0f; + + for (int i = 0; i < 4; i++) { + SunLayer *layer = &sun_info->SunLayers[i]; + + if (0.0f < layer->IntensityScale && layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + max_size = layer->Size; + } + } + + if (0.0f <= view3d.z && -max_size <= x && x <= screen_widthf + max_size && -max_size <= y && y <= screen_heightf + max_size) { + eRecalculateOthographicProjection(1, 100000.0f); + eSetOrthographicMatrixToHW(); + eCalcSunVisibility(eGetView(0, false), x, y); + eRecalculateOthographicProjection(1, 0.0f); + eSetOrthographicMatrixToHW(); + + for (int i = 0; i < 4; i++) { + SunLayer *layer = &sun_info->SunLayers[i]; + TextureInfo *texture_info = SunTextures[layer->Texture]; + + if (texture_info) { + ePoly sun_poly; + + ConstructePoly(&sun_poly); + eBuildSunPoly(&sun_poly, layer, max_size, x, y); + RenderViewPoly(view, &sun_poly, texture_info, 0); + } + } + } + } +} + void eInitSunPat() { vis_layer_fix.Texture = SUNTEX_CENTER; vis_layer_fix.IntensityScale = 32.0f; From ac29e584d5ab70bc40724142235187c28cd91a3b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:05:14 +0100 Subject: [PATCH 033/172] 41.0%: add GCHW_VD::iDraw Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Movie_GC.cpp | 136 +++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 5ec7f7c63..409a659af 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -29,15 +29,36 @@ struct TextureElement : public ShapeElement { }; namespace RealShape { -struct Shape { +struct Shape : public ShapeElement { TextureElement *GetTexture() const; + + const void *GetData() const { + if (mDataOffset == 0) { + return 0; + } + return reinterpret_cast(this) + mDataOffset; + } + + int GetWidth() const { + return GetTexture()->GetWidth(); + } + + int GetHeight() const { + return GetTexture()->GetHeight(); + } }; } struct tBigYUVSwizzler; struct tBigYUVSwizzler *NEW_tBigYUVSwizzlerTexture(GXTexObj *tYTexp, GXTexObj *tUTexp, GXTexObj *tVTexp) __asm__("NEW_tBigYUVSwizzlerTexture__FP9_GXTexObjN20"); +void tBigYUVSwizzler_DrawSetup(struct tBigYUVSwizzler *This, GXTexObj *tYTexp, GXTexObj *tUTexp, GXTexObj *tVTexp) + __asm__("tBigYUVSwizzler_DrawSetup__FP15tBigYUVSwizzlerP9_GXTexObjN21"); void DELETE_tBigYUVSwizzler(struct tBigYUVSwizzler *This); +void __InitGXlite(); + +extern int ScreenWidth; +extern int ScreenHeight; struct GCHW_VD { tBigYUVSwizzler *m_pYUVSwizzler; @@ -87,6 +108,119 @@ GCHW_VD::~GCHW_VD() { DELETE_tBigYUVSwizzler(m_pYUVSwizzler); } +void GCHW_VD::iDraw() { + if (mCurrentFrame) { + char *y = reinterpret_cast(const_cast(mCurrentFrame->GetData())); + int w = mCurrentFrame->GetWidth(); + int h = mCurrentFrame->GetHeight(); + char *cb; + char *cr; + unsigned long size; + float u0; + float u1; + float v0; + float v1; + float m_l; + float m_t; + float m_r; + float m_b; + float m_z; + + if (!mIsVP6) { + cb = y + w * h; + cr = cb + (w / 2) * (h / 2); + } else { + const int vp6Border = 0x60; + int dataOfs = w + vp6Border; + int uvOfs = (dataOfs / 2) * 0x18; + + cb = y + dataOfs * (h + vp6Border); + y += dataOfs * 0x30; + cr = cb + (dataOfs / 2) * ((h + vp6Border) / 2) + uvOfs; + cb += uvOfs; + w = dataOfs; + } + + GXSetCullMode(GX_CULL_NONE); + GXSetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_NOOP); + size = static_cast(w * h) >> 2; + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetNumTevStages(1); + GXSetTexCoordGen2(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, 0x3C, GX_FALSE, 0x7D); + GXSetNumTexGens(1); + GXSetNumChans(1); + GXSetChanCtrl(static_cast(4), GX_FALSE, static_cast(0), static_cast(1), GX_LIGHT_NULL, + static_cast(2), static_cast(2)); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR); + DCFlushRangeNoSync(y, w * h); + DCFlushRangeNoSync(cr, size); + DCFlushRangeNoSync(cb, size); + GXInitTexObjCI(&YTexObj, y, static_cast(w), static_cast(h), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 2); + GXInitTexObjLOD(&YTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + GXInitTexObjCI(&CrTexObj, cr, static_cast(w / 2), static_cast(h / 2), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 1); + GXInitTexObjLOD(&CrTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + GXInitTexObjCI(&CbTexObj, cb, static_cast(w / 2), static_cast(h / 2), static_cast(9), GX_CLAMP, GX_CLAMP, 0, 0); + GXInitTexObjLOD(&CbTexObj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + tBigYUVSwizzler_DrawSetup(m_pYUVSwizzler, &YTexObj, &CrTexObj, &CbTexObj); + PPCSync(); + + if (mIsVP6) { + u0 = 48.0f / static_cast(w); + u1 = static_cast(w - 0x30) / static_cast(w); + v0 = 1.0f; + v1 = 0.0f; + } else { + u0 = 0.0f; + u1 = 1.0f; + v0 = 0.0f; + v1 = 1.0f; + } + + m_l = 0.0f; + m_t = 0.0f; + m_r = static_cast(ScreenWidth) - 1.0f; + m_b = static_cast(ScreenHeight) - 1.0f; + m_z = 0.0f; + + GXBegin(GX_QUADS, GX_VTXFMT0, 4); + GXPosition3f32(m_l, m_t, m_z); + GXColor1u32(0xFFFFFFFF); + GXTexCoord2f32(u0, v0); + GXPosition3f32(m_r, m_t, m_z); + GXColor1u32(0xFFFFFFFF); + GXTexCoord2f32(u1, v0); + GXPosition3f32(m_r, m_b, m_z); + GXColor1u32(0xFFFFFFFF); + GXTexCoord2f32(u1, v1); + GXPosition3f32(m_l, m_b, m_z); + GXColor1u32(0xFFFFFFFF); + GXTexCoord2f32(u0, v1); + GXEnd(); + + GXSetNumIndStages(0); + GXSetTevSwapModeTable(static_cast(0), static_cast(0), static_cast(1), + static_cast(2), static_cast(3)); + GXSetTevSwapModeTable(static_cast(1), static_cast(0), static_cast(0), + static_cast(0), static_cast(3)); + GXSetTevSwapModeTable(static_cast(2), static_cast(1), static_cast(1), + static_cast(1), static_cast(3)); + GXSetTevSwapModeTable(static_cast(3), static_cast(2), static_cast(2), + static_cast(2), static_cast(3)); + GXSetTevDirect(static_cast(0)); + GXSetTevDirect(static_cast(1)); + GXSetTevDirect(static_cast(2)); + GXSetTevDirect(static_cast(3)); + GXSetTevDirect(static_cast(4)); + GXSetTevSwapMode(static_cast(1), static_cast(0), static_cast(0)); + GXSetTevSwapMode(static_cast(2), static_cast(0), static_cast(0)); + __InitGXlite(); + } +} + void MoviePlayer::FillInTextureInfo(unsigned int *buffer, TextureInfo *texture_info, RealShape::Shape *yuv_shape) { if (gGCVD) { gGCVD->mCurrentFrame = yuv_shape; From 20b04f827359f211a2c0a6e4180ba97555d1f7ac Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:09:16 +0100 Subject: [PATCH 034/172] 42.0%: add AcidFX particle setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/AcidFX_G.cpp | 55 ++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp index e69de29bb..b17adda6f 100644 --- a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp +++ b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp @@ -0,0 +1,55 @@ +#include "Speed/GameCube/Src/Ecstasy/eViewPlat.hpp" +#include "Speed/Indep/Src/Camera/Camera.hpp" +#include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/Ecstasy/Texture.hpp" + +bVector4 BillboardedParticleBasisX; +bVector4 BillboardedParticleBasisY; +int crtVtxFmt; + +void eLoadPosMtxImm(bMatrix4 &matrix, GXPosNrmMtx id); +int vsModel(int flags, int matrix_blend); +void ps_Model(int shader_id, void *light_material, int flags); +void ps_NoLighting(int lighting, int flags); +int eSetTexture(TextureInfo *texture_info, int stage); +int eSetBlendMode(TextureInfo *texture_info, unsigned char alpha_mode); + +inline bMatrix4 *eViewPlatInfo::GetWorldViewMatrix() { + return &WorldViewMatrix; +} + +int afxBeginBillboardedParticles(eView *view) { + eViewPlatInfo *plat_info = view->GetPlatInfo(); + Camera *camera = view->GetCamera(); + bMatrix4 *world_view = camera->GetCameraMatrix(); + bMatrix4 *world_view_gc = plat_info->GetWorldViewMatrix(); + + { + eLoadPosMtxImm(*world_view_gc, GX_PNMTX0); + crtVtxFmt = vsModel(0, -1); + ps_Model(5, 0, 0); + ps_NoLighting(1, 0); + } + + BillboardedParticleBasisX.x = world_view->v0.x; + BillboardedParticleBasisX.y = world_view->v1.x; + BillboardedParticleBasisX.z = world_view->v2.x; + BillboardedParticleBasisX.w = 0.0f; + BillboardedParticleBasisY.x = world_view->v0.y; + BillboardedParticleBasisY.y = world_view->v1.y; + BillboardedParticleBasisY.z = world_view->v2.y; + BillboardedParticleBasisY.w = 0.0f; + return 1; +} + +int afxBeginBillboardedParticleBatch(TextureInfo *texture_info) { + eSetTexture(texture_info, 0); + eSetBlendMode(texture_info, 0); + return 1; +} + +bool PlatStartParticleRender(eView *view, TextureInfo *mTextureInfo, unsigned int mNumParticles) { + afxBeginBillboardedParticles(view); + afxBeginBillboardedParticleBatch(mTextureInfo); + return 1; +} From 229c141d3e25e114962f370802991efea4c88db6 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:14:55 +0100 Subject: [PATCH 035/172] 42.9%: add xSprites sprite owners Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 92 +++++++++++++++++++++ src/Speed/GameCube/Src/xSparks.cpp | 3 + 2 files changed, 95 insertions(+) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index e69de29bb..efa6fb069 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -0,0 +1,92 @@ +#include "Speed/Indep/Libs/Support/Utility/UMath.h" +#include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/Ecstasy/Texture.hpp" + +struct NGParticle { + // total size: 0x30 + UMath::Vector3 initialPos; // offset 0x0, size 0xC + uint32 color; // offset 0xC, size 0x4 + UMath::Vector3 vel; // offset 0x10, size 0xC + float gravity; // offset 0x1C, size 0x4 + uint16 life; // offset 0x20, size 0x2 + uint8 length; // offset 0x22, size 0x1 + uint8 width; // offset 0x23, size 0x1 + unsigned char uv[1]; // offset 0x24, size 0x1 + unsigned char pad[3]; // offset 0x25, size 0x3 + float padAlign; // offset 0x28, size 0x4 + float age; // offset 0x2C, size 0x4 +}; + +struct SpriteDef { + // total size: 0x2C + TextureInfo *texture_info; // offset 0x0, size 0x4 + uint32 color; // offset 0x4, size 0x4 + float width; // offset 0x8, size 0x4 + bVector3 startPos; // offset 0xC, size 0x10 + bVector3 EndPosPos; // offset 0x1C, size 0x10 +}; + +struct XSpriteManager { + // total size: 0x3394 + uint32 position; // offset 0x0, size 0x4 + SpriteDef XSpriteBuffer[300]; // offset 0x4, size 0x3390 + + void AddSpark(const NGParticle &particle, TextureInfo *CurrentTexture); + void RenderAll(eView *view); +}; + +extern bMatrix4 eMathIdentityMatrix; + +void RenderViewPolyEx(eView *view, ePoly *poly, TextureInfo *texture_info, bMatrix4 *matrix, int flags, float z_bias) + __asm__("Render__18eViewPlatInterfaceP5ePolyP11TextureInfoP8bMatrix4if"); + +void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTexture) { + if (this->position < 300) { + UMath::Vector3 startPos; + UMath::Vector3 endPos; + float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; + float width = static_cast(particle.width) * (1.0f / 2048.0f); + SpriteDef *XSpriteBufferP = &this->XSpriteBuffer[this->position]; + + UMath::ScaleAdd(particle.initialPos, particle.age, particle.vel, startPos); + startPos.z += particle.gravity * particle.age * particle.age; + UMath::ScaleAdd(particle.initialPos, endAge, particle.vel, endPos); + endPos.z += particle.gravity * endAge * endAge; + + XSpriteBufferP->texture_info = CurrentTexture; + XSpriteBufferP->color = + particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; + XSpriteBufferP->width = width; + XSpriteBufferP->startPos = startPos; + XSpriteBufferP->EndPosPos = endPos; + this->position++; + } +} + +void XSpriteManager::RenderAll(eView *view) { + unsigned int i = 0; + ePoly poly; + + if (this->position != 0) { + do { + SpriteDef *sprite = &this->XSpriteBuffer[i]; + + poly.Vertices[0] = sprite->startPos; + poly.Vertices[1] = sprite->startPos; + poly.Vertices[1].z += sprite->width; + poly.Vertices[2] = sprite->EndPosPos; + poly.Vertices[2].z += sprite->width; + poly.Vertices[3] = sprite->EndPosPos; + + *reinterpret_cast(poly.Colours[0]) = sprite->color; + *reinterpret_cast(poly.Colours[1]) = sprite->color; + *reinterpret_cast(poly.Colours[2]) = sprite->color; + *reinterpret_cast(poly.Colours[3]) = sprite->color; + + i++; + RenderViewPolyEx(view, &poly, sprite->texture_info, &eMathIdentityMatrix, 0, 0.0f); + } while (i < this->position); + } + + this->position = 0; +} diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index e69de29bb..0f96936db 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -0,0 +1,3 @@ +#include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" + +void DrawXenonEmitters(eView *view) {} From 9a9c22cf8cb23cd62c82f072d795f1779ddd69e4 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:19:05 +0100 Subject: [PATCH 036/172] 44.6%: add Xenon effect owners Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 74 +++++++++++---------- src/Speed/GameCube/Src/xSparks.cpp | 57 ++++++++++++++++ 2 files changed, 95 insertions(+), 36 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index efa6fb069..958618820 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -1,6 +1,7 @@ #include "Speed/Indep/Libs/Support/Utility/UMath.h" #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" #include "Speed/Indep/Src/Ecstasy/Texture.hpp" +#include "Speed/Indep/Src/Ecstasy/eMath.hpp" struct NGParticle { // total size: 0x30 @@ -35,57 +36,58 @@ struct XSpriteManager { void RenderAll(eView *view); }; -extern bMatrix4 eMathIdentityMatrix; - void RenderViewPolyEx(eView *view, ePoly *poly, TextureInfo *texture_info, bMatrix4 *matrix, int flags, float z_bias) __asm__("Render__18eViewPlatInterfaceP5ePolyP11TextureInfoP8bMatrix4if"); void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTexture) { if (this->position < 300) { - UMath::Vector3 startPos; - UMath::Vector3 endPos; - float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; - float width = static_cast(particle.width) * (1.0f / 2048.0f); - SpriteDef *XSpriteBufferP = &this->XSpriteBuffer[this->position]; + { + UMath::Vector3 startPos; + UMath::Vector3 endPos; + float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; + float width = static_cast(particle.width) * (1.0f / 2048.0f); + SpriteDef *XSpriteBufferP = &this->XSpriteBuffer[this->position]; - UMath::ScaleAdd(particle.initialPos, particle.age, particle.vel, startPos); - startPos.z += particle.gravity * particle.age * particle.age; - UMath::ScaleAdd(particle.initialPos, endAge, particle.vel, endPos); - endPos.z += particle.gravity * endAge * endAge; + UMath::ScaleAdd(particle.initialPos, particle.age, particle.vel, startPos); + startPos.z += particle.gravity * particle.age * particle.age; + UMath::ScaleAdd(particle.initialPos, endAge, particle.vel, endPos); + endPos.z += particle.gravity * endAge * endAge; - XSpriteBufferP->texture_info = CurrentTexture; - XSpriteBufferP->color = - particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; - XSpriteBufferP->width = width; - XSpriteBufferP->startPos = startPos; - XSpriteBufferP->EndPosPos = endPos; - this->position++; + XSpriteBufferP->texture_info = CurrentTexture; + XSpriteBufferP->color = + particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; + XSpriteBufferP->width = width; + XSpriteBufferP->startPos = startPos; + XSpriteBufferP->EndPosPos = endPos; + this->position++; + } } } void XSpriteManager::RenderAll(eView *view) { - unsigned int i = 0; - ePoly poly; + ePoly pPoly; + SpriteDef *XSpriteBufferP = this->XSpriteBuffer; - if (this->position != 0) { - do { - SpriteDef *sprite = &this->XSpriteBuffer[i]; + { + int i; + bMatrix4 *identity = eGetIdentityMatrix(); - poly.Vertices[0] = sprite->startPos; - poly.Vertices[1] = sprite->startPos; - poly.Vertices[1].z += sprite->width; - poly.Vertices[2] = sprite->EndPosPos; - poly.Vertices[2].z += sprite->width; - poly.Vertices[3] = sprite->EndPosPos; + for (i = 0; i < static_cast(this->position); i++) { + pPoly.Vertices[0] = XSpriteBufferP->startPos; + pPoly.Vertices[1] = XSpriteBufferP->startPos; + pPoly.Vertices[1].z += XSpriteBufferP->width; + pPoly.Vertices[2] = XSpriteBufferP->EndPosPos; + pPoly.Vertices[2].z += XSpriteBufferP->width; + pPoly.Vertices[3] = XSpriteBufferP->EndPosPos; - *reinterpret_cast(poly.Colours[0]) = sprite->color; - *reinterpret_cast(poly.Colours[1]) = sprite->color; - *reinterpret_cast(poly.Colours[2]) = sprite->color; - *reinterpret_cast(poly.Colours[3]) = sprite->color; + *reinterpret_cast(pPoly.Colours[0]) = XSpriteBufferP->color; + *reinterpret_cast(pPoly.Colours[1]) = XSpriteBufferP->color; + *reinterpret_cast(pPoly.Colours[2]) = XSpriteBufferP->color; + *reinterpret_cast(pPoly.Colours[3]) = XSpriteBufferP->color; - i++; - RenderViewPolyEx(view, &poly, sprite->texture_info, &eMathIdentityMatrix, 0, 0.0f); - } while (i < this->position); + RenderViewPolyEx(view, &pPoly, XSpriteBufferP->texture_info, identity, 0, 0.0f); + XSpriteBufferP++; + } } this->position = 0; diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 0f96936db..881066398 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -1,3 +1,60 @@ #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/Ecstasy/EmitterSystem.h" +#include "Speed/Indep/Libs/Support/Utility/UTypes.h" + +struct XenonEffectDef { + // total size: 0x58 + UMath::Vector4 vel; // offset 0x0, size 0x10 + UMath::Matrix4 mat; // offset 0x10, size 0x40 + Attrib::Collection *spec; // offset 0x50, size 0x4 + EmitterGroup *piggyback_effect; // offset 0x54, size 0x4 +}; + +struct XenonEffectVec { + XenonEffectDef *start; // offset 0x0, size 0x4 + XenonEffectDef *finish; // offset 0x4, size 0x4 + void *unused; // offset 0x8, size 0x4 + XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 +}; + +struct XenonEffectLists { + XenonEffectVec active; // offset 0x0, size 0x10 + XenonEffectVec staging; // offset 0x10, size 0x10 +}; + +extern XenonEffectLists gNGEffectList; + +void reserveXenonEffectVec(void *vec, unsigned int count) + __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); void DrawXenonEmitters(eView *view) {} + +void ClearXenonEmitters() { + gNGEffectList.active.finish = gNGEffectList.active.start; + gNGEffectList.staging.finish = gNGEffectList.staging.start; +} + +void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { + unsigned int size = gNGEffectList.active.finish - gNGEffectList.active.start; + if (size < 20) { + unsigned int active_capacity = gNGEffectList.active.end_of_storage - gNGEffectList.active.start; + if (active_capacity < 20) { + reserveXenonEffectVec(&gNGEffectList.active, 20); + } + + unsigned int staging_capacity = gNGEffectList.staging.end_of_storage - gNGEffectList.staging.start; + if (staging_capacity < 20) { + reserveXenonEffectVec(&gNGEffectList.staging, 20); + } + + XenonEffectDef effect; + effect.mat = UMath::Matrix4::kIdentity; + effect.mat.v3 = mat->v3; + effect.spec = const_cast(spec); + effect.piggyback_effect = piggyback_fx; + effect.vel = *vel; + + *gNGEffectList.active.finish = effect; + ++gNGEffectList.active.finish; + } +} From e129e44e57135da4f860e0880a7279452ddc2c2b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:20:41 +0100 Subject: [PATCH 037/172] 45.5%: extend Xenon update path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 881066398..b37bbe52d 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -22,7 +22,28 @@ struct XenonEffectLists { XenonEffectVec staging; // offset 0x10, size 0x10 }; +class NGEffect { + char mEffectDef[0x14]; + + public: + NGEffect(const XenonEffectDef &effect); +}; + +class ParticleList { + NGParticle mParticles[300]; + uint32 mNumParticles; + TextureInfo *mContrail_tex; + TextureInfo *mSparks_tex; + TextureInfo *mCurrentTexture; + + public: + void GeneratePolys(); + void AgeParticles(float dt); + uint32 GetNumParticles(); +}; + extern XenonEffectLists gNGEffectList; +extern ParticleList gParticleList; void reserveXenonEffectVec(void *vec, unsigned int count) __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); @@ -58,3 +79,25 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, ++gNGEffectList.active.finish; } } + +void UpdateXenonEmitters(float dt) { + gParticleList.AgeParticles(dt); + + XenonEffectDef *effect = gNGEffectList.active.start; + while (effect != gNGEffectList.active.finish) { + *gNGEffectList.staging.finish = *effect; + ++gNGEffectList.staging.finish; + ++effect; + } + + gNGEffectList.active.finish = gNGEffectList.active.start; + + effect = gNGEffectList.staging.start; + while (effect != gNGEffectList.staging.finish) { + NGEffect ng_effect(*effect); + ++effect; + } + + gNGEffectList.staging.finish = gNGEffectList.staging.start; + gParticleList.GeneratePolys(); +} From cafb76eb41d419773ac1bf7ad620fa6b80fafc16 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:23:05 +0100 Subject: [PATCH 038/172] 47.0%: add Xenon constructors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 39 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index b37bbe52d..293add3a3 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -1,5 +1,8 @@ #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" #include "Speed/Indep/Src/Ecstasy/EmitterSystem.h" +#include "Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h" +#include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h" +#include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h" #include "Speed/Indep/Libs/Support/Utility/UTypes.h" struct XenonEffectDef { @@ -22,11 +25,20 @@ struct XenonEffectLists { XenonEffectVec staging; // offset 0x10, size 0x10 }; -class NGEffect { - char mEffectDef[0x14]; +struct CGEmitter { + Attrib::Gen::fuelcell_emitter mEmitterDef; + Attrib::Gen::emitteruv mTextureUVs; + UMath::Vector4 mVel; + UMath::Matrix4 mLocalWorld; - public: - NGEffect(const XenonEffectDef &effect); + CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef); + void SpawnParticles(float, float); +}; + +struct NGEffect { + Attrib::Gen::fuelcell_effect mEffectDef; + + NGEffect(const XenonEffectDef &eDef); }; class ParticleList { @@ -48,6 +60,25 @@ extern ParticleList gParticleList; void reserveXenonEffectVec(void *vec, unsigned int count) __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); +CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) + : mEmitterDef(spec, 0, nullptr) // + , mTextureUVs(mEmitterDef.emitteruv(), 0, nullptr) // + , mVel(eDef.vel) // + , mLocalWorld(eDef.mat) {} + +NGEffect::NGEffect(const XenonEffectDef &eDef) + : mEffectDef(eDef.spec, 0, nullptr) { + if (mEffectDef.GetCollection() != 0) { + int i = 0; + int length = mEffectDef.Num_NGEmitter(); + while (i < length) { + CGEmitter emitter(mEffectDef.NGEmitter(i).GetCollection(), eDef); + emitter.SpawnParticles(1.0f / 30.0f, 1.0f); + i++; + } + } +} + void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { From 7b477cb7cc8f010256614f0f41f240614636139e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:24:56 +0100 Subject: [PATCH 039/172] 48.2%: add Xenon particle spawning Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 293add3a3..25c293bd1 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -3,6 +3,7 @@ #include "Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h" +#include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" #include "Speed/Indep/Libs/Support/Utility/UTypes.h" struct XenonEffectDef { @@ -51,14 +52,17 @@ class ParticleList { public: void GeneratePolys(); void AgeParticles(float dt); + NGParticle *GetNextParticle(); uint32 GetNumParticles(); }; extern XenonEffectLists gNGEffectList; extern ParticleList gParticleList; +extern unsigned int randomSeed; void reserveXenonEffectVec(void *vec, unsigned int count) __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); +float bRandom(float range, unsigned int *seed); CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) : mEmitterDef(spec, 0, nullptr) // @@ -66,6 +70,90 @@ CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) , mVel(eDef.vel) // , mLocalWorld(eDef.mat) {} +void CGEmitter::SpawnParticles(float dt, float intensity) { + unsigned int seed = randomSeed; + + if (intensity > 0.0f) { + UMath::Matrix4 local_world = mLocalWorld; + UMath::Vector4 velocity_base; + UMath::Vector4 velocity_center; + UMath::Vector4 volume_extent; + UMath::Vector4 spawn_point; + UMath::Vector4 world_spawn_point; + float age = 0.0f; + float count = intensity * mEmitterDef.NumParticles(); + float life = mEmitterDef.Life(); + float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; + float colour_r = mEmitterDef.Colour1().x; + float colour_g = mEmitterDef.Colour1().y; + float colour_b = mEmitterDef.Colour1().z; + unsigned int colour_a = static_cast(mEmitterDef.Colour1().w * 255.0f); + + VU0_v4scalexyz(mVel, mEmitterDef.VelocityInherit().x, velocity_base); + UMath::RotateTranslate(mEmitterDef.VolumeCenter(), local_world, velocity_center); + velocity_base.x += velocity_center.x; + velocity_base.y += velocity_center.y; + velocity_base.z += velocity_center.z; + + if (count_after_variance != 0.0f) { + float particle_step = dt / count_after_variance; + while (count_after_variance != 0.0f) { + NGParticle *particle; + float length_start; + float length_clamped; + float gravity; + + count_after_variance -= 1.0f; + particle = gParticleList.GetNextParticle(); + if (!particle) { + break; + } + + length_start = mEmitterDef.LengthStart() + bRandom(mEmitterDef.LengthDelta(), &seed); + if (length_start < 0.0f) { + break; + } + + length_clamped = 1.0f; + if (length_start < 1.0f) { + length_clamped = length_start; + } + + volume_extent.x = 1.0f - (mEmitterDef.VolumeExtent().x - bRandom(mEmitterDef.VolumeExtent().x, &seed) * 2.0f); + volume_extent.y = 1.0f - (mEmitterDef.VolumeExtent().y - bRandom(mEmitterDef.VolumeExtent().y, &seed) * 2.0f); + volume_extent.z = 1.0f - (mEmitterDef.VolumeExtent().z - bRandom(mEmitterDef.VolumeExtent().z, &seed) * 2.0f); + volume_extent.w = 1.0f; + + spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeCenter().x, &seed) - mEmitterDef.VolumeCenter().x * 0.5f); + spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeCenter().y, &seed) - mEmitterDef.VolumeCenter().y * 0.5f); + spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeCenter().z, &seed) - mEmitterDef.VolumeCenter().z * 0.5f); + spawn_point.w = 1.0f; + + UMath::RotateTranslate(spawn_point, local_world, world_spawn_point); + VU0_v4scalexyz(velocity_base, volume_extent.x, velocity_center); + VU0_v3scaleadd(UMath::Vector4To3(velocity_center), age, UMath::Vector4To3(world_spawn_point), + *reinterpret_cast(particle)); + + gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &seed) * 2.0f; + particle->initialPos = UMath::Vector4To3(world_spawn_point); + particle->vel = UMath::Vector4To3(velocity_center); + particle->age = age; + particle->gravity = gravity; + particle->life = static_cast((life - life * mEmitterDef.LifeVariance()) * 65535.0f); + particle->color = + colour_a << 24 | static_cast(colour_b * 255.0f) << 16 | static_cast(colour_g * 255.0f) << 8 | + static_cast(colour_r * 255.0f); + particle->length = static_cast(length_clamped * 255.0f); + particle->width = static_cast(mEmitterDef.HeightStart()); + + age += particle_step; + } + } + + randomSeed = seed; + } +} + NGEffect::NGEffect(const XenonEffectDef &eDef) : mEffectDef(eDef.spec, 0, nullptr) { if (mEffectDef.GetCollection() != 0) { From 83e7cfe2b3cac358d0e4f06f2834dffeca0786f9 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:25:58 +0100 Subject: [PATCH 040/172] 49.2%: add Xenon particle aging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 25c293bd1..2e2011bdc 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -70,6 +70,13 @@ CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) , mVel(eDef.vel) // , mLocalWorld(eDef.mat) {} +NGParticle *ParticleList::GetNextParticle() { + if (mNumParticles < 300) { + return &mParticles[mNumParticles++]; + } + return 0; +} + void CGEmitter::SpawnParticles(float dt, float intensity) { unsigned int seed = randomSeed; @@ -167,6 +174,29 @@ NGEffect::NGEffect(const XenonEffectDef &eDef) } } +void ParticleList::AgeParticles(float dt) { + int alive = 0; + int i = 0; + + if (static_cast(mNumParticles) > 0) { + NGParticle *src = mParticles; + NGParticle *dst = mParticles; + do { + if (dt * 8191.0f <= static_cast(src->life)) { + alive++; + *dst = *src; + dst->age += dt; + dst->life = static_cast(static_cast(src->life) - dt * 8191.0f); + dst++; + } + src++; + i++; + } while (i < static_cast(mNumParticles)); + } + + mNumParticles = alive; +} + void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { From 9d6ea2d90298953b2a97dcb26ba562589e1ad32d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:26:58 +0100 Subject: [PATCH 041/172] 49.8%: add Xenon poly generation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 2e2011bdc..b2ddda8e5 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -58,11 +58,14 @@ class ParticleList { extern XenonEffectLists gNGEffectList; extern ParticleList gParticleList; +extern XSpriteManager NGSpriteManager; extern unsigned int randomSeed; void reserveXenonEffectVec(void *vec, unsigned int count) __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); float bRandom(float range, unsigned int *seed); +unsigned int bStringHash(const char *str); +TextureInfo *GetTextureInfo(unsigned int name_hash, int allow_default, int force_local); CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) : mEmitterDef(spec, 0, nullptr) // @@ -197,6 +200,33 @@ void ParticleList::AgeParticles(float dt) { mNumParticles = alive; } +void ParticleList::GeneratePolys() { + NGParticle *particle = mParticles; + + if (mNumParticles != 0) { + if (!mContrail_tex) { + mContrail_tex = GetTextureInfo(bStringHash("PS2_CONTRAIL"), 0, 0); + mSparks_tex = GetTextureInfo(bStringHash("PS2_SPARKS"), 0, 0); + } + + { + unsigned int i = 0; + + do { + if (particle->uv[0] == 0x7f) { + mCurrentTexture = mContrail_tex; + } else { + mCurrentTexture = mSparks_tex; + } + + i++; + NGSpriteManager.AddSpark(*particle, mCurrentTexture); + particle++; + } while (i < mNumParticles); + } + } +} + void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { From 80df004796b6ca9b25d64be548945ab9b4377cfb Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 00:56:02 +0100 Subject: [PATCH 042/172] 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 418f610129444819fd70e0418be3d5d210bb9e75 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 01:20:24 +0100 Subject: [PATCH 043/172] 50.7%: restructure LGWheels Play* functions for correct code layout Convert early-return `if (!IsConnected) { error; return; }` pattern to `if (IsConnected) { ... } else { error; }` structure to match target branch direction. Fixes code layout for PlaySpringForce, PlayConstantForce, PlayDamperForce, PlayFrontalCollisionForce, PlayDirtRoadEffect, PlayBumpyRoadEffect, PlaySlipperyRoadEffect, PlaySurfaceEffect, and PlayCarAirborne - all from 0% to 77-91% match. Also fix missing RELOC_DIFF_CHOICES in decomp-diff.py. Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 489 +++++++++---------- tools/decomp-diff.py | 1 + 2 files changed, 241 insertions(+), 249 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 80739ead7..e655e2efd 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -326,47 +326,46 @@ void LGWheels::PlaySpringForce(long channel, signed char offset, unsigned char s return; } - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) != 0) { + if (SameSpringForceParams(channel, offset, saturation, coefficient)) { + return; + } + + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + if (ret < 0) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) != 0) { - if (SameSpringForceParams(channel, offset, saturation, coefficient)) { + LGWheelsGetSpringForceParams(this)[channel].offset = offset; + LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; + LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + } else if (SameSpringForceParams(channel, offset, saturation, coefficient)) { + LGWheelsGetCondition(this)->Start(channel, 0); return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); } - LGWheelsGetSpringForceParams(this)[channel].offset = offset; - LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; - LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; - return; - } + if (ret >= 0) { + LGWheelsGetSpringForceParams(this)[channel].offset = offset; + LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; + LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); - } else if (SameSpringForceParams(channel, offset, saturation, coefficient)) { LGWheelsGetCondition(this)->Start(channel, 0); - return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); - } - - if (ret >= 0) { - LGWheelsGetSpringForceParams(this)[channel].offset = offset; - LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; - LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + OSReport(kPlayForceError, channel); } - - LGWheelsGetCondition(this)->Start(channel, 0); } void LGWheels::StopSpringForce(long channel) { @@ -381,45 +380,44 @@ bool LGWheels::SameSpringForceParams(long channel, signed char offset, unsigned void LGWheels::PlayConstantForce(long channel, short magnitude, unsigned short direction) { int ret; - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, 0) != 0) { + if (SameConstantForceParams(channel, magnitude, direction)) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, 0) != 0) { - if (SameConstantForceParams(channel, magnitude, direction)) { + ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); + if (ret < 0) { + return; + } + + LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; + LGWheelsGetConstantForceParams(this)[channel].direction = direction; return; } - ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetConstant(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetConstant(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); + } else if (SameConstantForceParams(channel, magnitude, direction)) { + LGWheelsGetConstant(this)->Start(channel, 0); return; + } else { + ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); } - LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; - LGWheelsGetConstantForceParams(this)[channel].direction = direction; - return; - } + if (ret >= 0) { + LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; + LGWheelsGetConstantForceParams(this)[channel].direction = direction; + } - if (LGWheelsGetEffectID(LGWheelsGetConstant(this), channel, 0) == static_cast(-1)) { - ret = LGWheelsGetConstant(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); - } else if (SameConstantForceParams(channel, magnitude, direction)) { LGWheelsGetConstant(this)->Start(channel, 0); - return; } else { - ret = LGWheelsGetConstant(this)->UpdateForce(channel, 0, static_cast(-1), 0, magnitude, direction, 0, 0, 0, 0); - } - - if (ret >= 0) { - LGWheelsGetConstantForceParams(this)[channel].magnitude = magnitude; - LGWheelsGetConstantForceParams(this)[channel].direction = direction; + OSReport(kPlayForceError, channel); } - - LGWheelsGetConstant(this)->Start(channel, 0); } void LGWheels::StopConstantForce(long channel) { @@ -438,43 +436,42 @@ void LGWheels::PlayDamperForce(long channel, short coefficient) { return; } - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) != 0) { + if (SameDamperForceParams(channel, coefficient)) { + return; + } + + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + if (ret < 0) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) != 0) { - if (SameDamperForceParams(channel, coefficient)) { + LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 1) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + } else if (SameDamperForceParams(channel, coefficient)) { + LGWheelsGetCondition(this)->Start(channel, 1); return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); } - LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; - return; - } + if (ret >= 0) { + LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 1) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); - } else if (SameDamperForceParams(channel, coefficient)) { LGWheelsGetCondition(this)->Start(channel, 1); - return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); - } - - if (ret >= 0) { - LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + OSReport(kPlayForceError, channel); } - - LGWheelsGetCondition(this)->Start(channel, 1); } void LGWheels::StopDamperForce(long channel) { @@ -488,35 +485,34 @@ bool LGWheels::SameDamperForceParams(long channel, short coefficient) { void LGWheels::PlayFrontalCollisionForce(long channel, unsigned char magnitude) { int ret; - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 0) != 0) { + if (!SameFrontalCollisionForceParams(channel, magnitude)) { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 0, 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); + if (ret >= 0) { + LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; + } + } + LGWheelsGetPeriodic(this)->Start(channel, 0); + return; + } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 0) != 0) { - if (!SameFrontalCollisionForceParams(channel, magnitude)) { + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 0) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); + } else if (SameFrontalCollisionForceParams(channel, magnitude)) { + ret = 0; + } else { ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 0, 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); - if (ret >= 0) { - LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; - } } - LGWheelsGetPeriodic(this)->Start(channel, 0); - return; - } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 0) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); - } else if (SameFrontalCollisionForceParams(channel, magnitude)) { - ret = 0; - } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 0, 3, 150, 0, magnitude, 90, 75, 0, 0, 20, 0, 0, 0); - } + if (ret >= 0) { + LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; + } - if (ret >= 0) { - LGWheelsGetFrontalCollisionParams(this)[channel].magnitude = magnitude; + LGWheelsGetPeriodic(this)->Start(channel, 0); + } else { + OSReport(kPlayForceError, channel); } - - LGWheelsGetPeriodic(this)->Start(channel, 0); } bool LGWheels::SameFrontalCollisionForceParams(long channel, short magnitude) { @@ -526,43 +522,42 @@ bool LGWheels::SameFrontalCollisionForceParams(long channel, short magnitude) { void LGWheels::PlayDirtRoadEffect(long channel, unsigned char magnitude) { int ret; - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 1) != 0) { + if (SameDirtRoadEffectParams(channel, magnitude)) { + return; + } + + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + if (ret < 0) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 1) != 0) { - if (SameDirtRoadEffectParams(channel, magnitude)) { + LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; return; } - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 1) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + } else if (SameDirtRoadEffectParams(channel, magnitude)) { + LGWheelsGetPeriodic(this)->Start(channel, 1); return; + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); } - LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; - return; - } + if (ret >= 0) { + LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; + } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 1) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); - } else if (SameDirtRoadEffectParams(channel, magnitude)) { LGWheelsGetPeriodic(this)->Start(channel, 1); - return; } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); - } - - if (ret >= 0) { - LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; + OSReport(kPlayForceError, channel); } - - LGWheelsGetPeriodic(this)->Start(channel, 1); } void LGWheels::StopDirtRoadEffect(long channel) { @@ -576,43 +571,42 @@ bool LGWheels::SameDirtRoadEffectParams(long channel, short magnitude) { void LGWheels::PlayBumpyRoadEffect(long channel, unsigned char magnitude) { int ret; - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 2) != 0) { + if (SameBumpyRoadEffectParams(channel, magnitude)) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 2) != 0) { - if (SameBumpyRoadEffectParams(channel, magnitude)) { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + if (ret < 0) { + return; + } + + LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; return; } - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 2) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + } else if (SameBumpyRoadEffectParams(channel, magnitude)) { + LGWheelsGetPeriodic(this)->Start(channel, 2); return; + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); } - LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; - return; - } + if (ret >= 0) { + LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; + } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 2) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); - } else if (SameBumpyRoadEffectParams(channel, magnitude)) { LGWheelsGetPeriodic(this)->Start(channel, 2); - return; } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); - } - - if (ret >= 0) { - LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; + OSReport(kPlayForceError, channel); } - - LGWheelsGetPeriodic(this)->Start(channel, 2); } void LGWheels::StopBumpyRoadEffect(long channel) { @@ -638,43 +632,42 @@ void LGWheels::PlaySlipperyRoadEffect(long channel, short magnitude) { LGWheelsGetSpringWasPlaying(this, channel) = 1; } - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + if (SameSlipperyRoadEffectParams(channel, magnitude)) { + return; + } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { - if (SameSlipperyRoadEffectParams(channel, magnitude)) { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + if (ret < 0) { + return; + } + + LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); - if (ret < 0) { + if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 2) == static_cast(-1)) { + ret = LGWheelsGetCondition(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + } else if (SameSlipperyRoadEffectParams(channel, magnitude)) { + LGWheelsGetCondition(this)->Start(channel, 2); return; + } else { + ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); } - LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; - return; - } + if (ret >= 0) { + LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 2) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); - } else if (SameSlipperyRoadEffectParams(channel, magnitude)) { LGWheelsGetCondition(this)->Start(channel, 2); - return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); - } - - if (ret >= 0) { - LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + OSReport(kPlayForceError, channel); } - - LGWheelsGetCondition(this)->Start(channel, 2); } void LGWheels::StopSlipperyRoadEffect(long channel) { @@ -688,24 +681,51 @@ bool LGWheels::SameSlipperyRoadEffectParams(long channel, short magnitude) { void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { int ret; - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { - OSReport(kPlayForceError, channel); - return; - } + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetIsAirborne(this, channel)) { + return; + } - if (LGWheelsGetIsAirborne(this, channel)) { - return; - } + if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 3) != 0) { + if (SameSurfaceEffectParams(channel, type, magnitude, period)) { + return; + } + + if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { + LGWheelsGetPeriodic(this)->Destroy(channel, 3); + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + LGWheelsGetPeriodic(this)->Start(channel, 3); + } else { + ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + } + + if (ret >= 0) { + LGWheelsGetSurfaceEffectParams(this)[channel].type = type; + LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; + LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + } + return; + } + + if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 3) == static_cast(-1)) { + ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + if (ret >= 0) { + LGWheelsGetSurfaceEffectParams(this)[channel].type = type; + LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; + LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + } + LGWheelsGetPeriodic(this)->Start(channel, 3); + return; + } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 3) != 0) { if (SameSurfaceEffectParams(channel, type, magnitude, period)) { + LGWheelsGetPeriodic(this)->Start(channel, 3); return; } if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { LGWheelsGetPeriodic(this)->Destroy(channel, 3); ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); - LGWheelsGetPeriodic(this)->Start(channel, 3); } else { ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); } @@ -715,39 +735,11 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; LGWheelsGetSurfaceEffectParams(this)[channel].period = period; } - return; - } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 3) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); - if (ret >= 0) { - LGWheelsGetSurfaceEffectParams(this)[channel].type = type; - LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; - LGWheelsGetSurfaceEffectParams(this)[channel].period = period; - } LGWheelsGetPeriodic(this)->Start(channel, 3); - return; - } - - if (SameSurfaceEffectParams(channel, type, magnitude, period)) { - LGWheelsGetPeriodic(this)->Start(channel, 3); - return; - } - - if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { - LGWheelsGetPeriodic(this)->Destroy(channel, 3); - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); - } - - if (ret >= 0) { - LGWheelsGetSurfaceEffectParams(this)[channel].type = type; - LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; - LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + OSReport(kPlayForceError, channel); } - - LGWheelsGetPeriodic(this)->Start(channel, 3); } void LGWheels::StopSurfaceEffect(long channel) { @@ -760,39 +752,38 @@ bool LGWheels::SameSurfaceEffectParams(long channel, unsigned char type, unsigne } void LGWheels::PlayCarAirborne(long channel) { - if (!LGWheelsGetWheels(this)->IsConnected(channel)) { + if (LGWheelsGetWheels(this)->IsConnected(channel)) { + LGWheelsGetIsAirborne(this, channel) = 1; + if (IsPlaying(channel, 0)) { + StopSpringForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) = 1; + } + if (IsPlaying(channel, 1)) { + StopConstantForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) = 1; + } + if (IsPlaying(channel, 2)) { + StopDamperForce(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) = 1; + } + if (IsPlaying(channel, 5)) { + StopDirtRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) = 1; + } + if (IsPlaying(channel, 6)) { + StopBumpyRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) = 1; + } + if (IsPlaying(channel, 7)) { + StopSlipperyRoadEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) = 1; + } + if (IsPlaying(channel, 8)) { + StopSurfaceEffect(channel); + LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) = 1; + } + } else { OSReport(kPlayForceError, channel); - return; - } - - LGWheelsGetIsAirborne(this, channel) = 1; - if (IsPlaying(channel, 0)) { - StopSpringForce(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) = 1; - } - if (IsPlaying(channel, 1)) { - StopConstantForce(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) = 1; - } - if (IsPlaying(channel, 2)) { - StopDamperForce(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) = 1; - } - if (IsPlaying(channel, 5)) { - StopDirtRoadEffect(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) = 1; - } - if (IsPlaying(channel, 6)) { - StopBumpyRoadEffect(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) = 1; - } - if (IsPlaying(channel, 7)) { - StopSlipperyRoadEffect(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) = 1; - } - if (IsPlaying(channel, 8)) { - StopSurfaceEffect(channel); - LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) = 1; } } diff --git a/tools/decomp-diff.py b/tools/decomp-diff.py index 13e54e26f..2c799bb27 100644 --- a/tools/decomp-diff.py +++ b/tools/decomp-diff.py @@ -28,6 +28,7 @@ root_dir = ROOT_DIR OBJDIFF_CLI = os.path.join(root_dir, "build", "tools", "objdiff-cli") +RELOC_DIFF_CHOICES = ["none", "all"] def run_objdiff( From 932587c5cc4c2d760aaad92c771827f3018584c3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 01:37:04 +0100 Subject: [PATCH 044/172] add stubs and fix matches: PlatGetViewVectors, CGEmitter dtor, RCMP, bEURGB60 - PlatGetViewVectors 100% match (108B) - CGEmitter::~CGEmitter 100% match (108B) with FastMem operator delete - RCMP_GetMaxFramesOutStanding stub (8B) - Fix bEURGB60 type from bool to char for InitDisplaySystem match - Add PlatEndParticleRender, afxEnd* stubs Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Movie_GC.cpp | 4 ++++ src/Speed/GameCube/Src/Platform_G.cpp | 2 +- src/Speed/GameCube/Src/Render/AcidFX_G.cpp | 26 ++++++++++++++++++++++ src/Speed/GameCube/Src/xSparks.cpp | 10 +++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 409a659af..b148d59fe 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -239,6 +239,10 @@ void PlatSetFirstMovieFrame(TextureInfo *texture_info, RealShape::Shape *yuv_sha } } +int RCMP_GetMaxFramesOutStanding() { + return 2; +} + void PlatFinishMovie() { if (gGCVD) { delete gGCVD; diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 6eea73d70..22ae97cf1 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -34,7 +34,7 @@ enum eLanguages { eLANGUAGE_MAX = 16, }; -extern bool bEURGB60; +extern char bEURGB60; extern "C" void OSResetSystem(BOOL reset, u32 resetCode, BOOL forceMenu); struct FILESYSOPTS { diff --git a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp index b17adda6f..d7415a2ad 100644 --- a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp +++ b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp @@ -2,6 +2,8 @@ #include "Speed/Indep/Src/Camera/Camera.hpp" #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" #include "Speed/Indep/Src/Ecstasy/Texture.hpp" +#include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" +#include bVector4 BillboardedParticleBasisX; bVector4 BillboardedParticleBasisY; @@ -53,3 +55,27 @@ bool PlatStartParticleRender(eView *view, TextureInfo *mTextureInfo, unsigned in afxBeginBillboardedParticleBatch(mTextureInfo); return 1; } + +void afxEndBillboardedParticleBatch(TextureInfo *texture_info, float f, int i) {} + +void afxEndBillboardedParticles() {} + +void PlatEndParticleRender() { + afxEndBillboardedParticleBatch(0, 0.0f, 0); + afxEndBillboardedParticles(); +} + +void PlatGetViewVectors(eView *view, UMath::Vector3 &right, UMath::Vector3 &up, UMath::Vector3 &forward) { + eViewPlatInfo *plat_info = view->GetPlatInfo(); + Mtx44 local_matrix; + PSMTX44Copy(reinterpret_cast(&plat_info->WorldViewMatrix), local_matrix); + right.x = local_matrix[0][0]; + right.y = local_matrix[1][1]; + right.z = local_matrix[2][2]; + up.x = local_matrix[0][0]; + up.y = local_matrix[1][1]; + up.z = local_matrix[2][2]; + forward.x = local_matrix[0][0]; + forward.y = local_matrix[1][1]; + forward.z = local_matrix[2][2]; +} diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index b2ddda8e5..a6c862e95 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -1,5 +1,6 @@ #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" #include "Speed/Indep/Src/Ecstasy/EmitterSystem.h" +#include "Speed/Indep/Libs/Support/Utility/FastMem.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h" @@ -33,7 +34,14 @@ struct CGEmitter { UMath::Matrix4 mLocalWorld; CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef); + ~CGEmitter(); void SpawnParticles(float, float); + + void operator delete(void *ptr) { + if (ptr) { + gFastMem.Free(ptr, 0x78, 0); + } + } }; struct NGEffect { @@ -73,6 +81,8 @@ CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) , mVel(eDef.vel) // , mLocalWorld(eDef.mat) {} +CGEmitter::~CGEmitter() {} + NGParticle *ParticleList::GetNextParticle() { if (mNumParticles < 300) { return &mParticles[mNumParticles++]; From 7013789057b5c11929179225992f6634c2b2fcfd Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 01:53:58 +0100 Subject: [PATCH 045/172] fix GCHW_VD::GCHW_VD to 100% match by using Shape::GetWidth/GetHeight Using the Shape-level GetWidth/GetHeight inlines instead of directly accessing TextureElement members produces single GetTexture call matching target. Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Movie_GC.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index b148d59fe..6746947b0 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -80,9 +80,8 @@ struct MoviePlayer { GCHW_VD *gGCVD; GCHW_VD::GCHW_VD(RealShape::Shape *yuvshp, bool isVP6Movie) : mIsVP6(isVP6Movie) { - TextureElement *texture = yuvshp->GetTexture(); - int w = texture->GetWidth(); - int h = texture->GetHeight(); + int w = yuvshp->GetWidth(); + int h = yuvshp->GetHeight(); if (mIsVP6) { w += 0x60; From 149e18f71022b9fea5cd3032143ac716f2710e52 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 02:12:46 +0100 Subject: [PATCH 046/172] 69.8%: implement DVDErrorTask (1904B, 86.5% match) Major disc error handler function with reset detection, memory card safety, FEng error display, TrackStreamer servicing, and Logitech wheel force effect management during disc errors. Co-Authored-By: Claude Opus 4.6 --- .../GameCube/Src/MemoryCard/MemoryCardImp.cpp | 7 - src/Speed/GameCube/Src/Movie_GC.cpp | 4 +- src/Speed/GameCube/Src/Platform_G.cpp | 368 ++++++++++++++++++ .../Src/Frontend/MemoryCard/MemoryCard.hpp | 2 + src/Speed/Indep/Src/World/TrackStreamer.hpp | 1 - 5 files changed, 371 insertions(+), 11 deletions(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index 4dbb86deb..e5a0c1d00 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -80,13 +80,6 @@ struct SaveReq { }; } // namespace RealmcIface -class cFEng { - public: - static cFEng *mInstance; - - void QueuePackageMessage(unsigned int pMessage, const char *pPackageName, FEObject *obj); -}; - struct MemoryCardImp { static unsigned short *gEntryType[MemoryCard::ST_MAX]; static unsigned short gContentName[]; diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 6746947b0..002ecb44b 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -73,9 +73,7 @@ struct GCHW_VD { void iDraw(); }; -struct MoviePlayer { - void FillInTextureInfo(unsigned int *buffer, TextureInfo *texture_info, RealShape::Shape *yuv_shape); -}; +/* MoviePlayer defined in Platform_G.cpp */ GCHW_VD *gGCVD; diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 22ae97cf1..3e0fe7cf6 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -1,10 +1,96 @@ #include "Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp" #include "Speed/Indep/Src/Misc/BuildRegion.hpp" +#include "Speed/Indep/Src/Misc/GameFlow.hpp" #include "Speed/Indep/Src/Misc/Platform.h" +#include "Speed/Indep/Src/World/TrackStreamer.hpp" #include "Speed/Indep/bWare/Inc/bMemory.hpp" #include #include "dolphin.h" +/* LGWheels is defined later in the unity build, so forward-declare wrappers with asm labels */ +class LGWheels; +extern LGWheels *plat_lgwheels; + +void LGWheels_ReadAll(LGWheels *) asm("ReadAll__8LGWheels"); +int LGWheels_IsConnected(LGWheels *, long) asm("IsConnected__8LGWheelsl"); +void LGWheels_StopConstantForce(LGWheels *, long) asm("StopConstantForce__8LGWheelsl"); +void LGWheels_StopSurfaceEffect(LGWheels *, long) asm("StopSurfaceEffect__8LGWheelsl"); +void LGWheels_StopDamperForce(LGWheels *, long) asm("StopDamperForce__8LGWheelsl"); +void LGWheels_StopCarAirborne(LGWheels *, long) asm("StopCarAirborne__8LGWheelsl"); +void LGWheels_StopSlipperyRoadEffect(LGWheels *, long) asm("StopSlipperyRoadEffect__8LGWheelsl"); +void LGWheels_PlaySpringForce(LGWheels *, long, signed char, unsigned char, short) asm("PlaySpringForce__8LGWheelslScUcs"); + +class IOModule { +public: + static IOModule &GetIOModule(); + void Update(); +}; + +class cFEngJoyInput { +public: + static cFEngJoyInput *mInstance; + void HandleJoy(); +}; + +class FEObject; + +class cFEng { +public: + static cFEng *mInstance; + void MakeLoadedPackagesDirty(); + int IsPackagePushed(const char *); + void PushErrorPackage(const char *, int, unsigned long); + void PopErrorPackage(); + void QueueGameMessage(unsigned int, const char *, unsigned int); + void QueuePackageMessage(unsigned int, const char *, FEObject *); +}; + +class EAXSound { +public: + void Update(float); +}; + +class TextureInfo; +namespace RealShape { class Shape; } + +struct MoviePlayer { + void Stop(); + void FillInTextureInfo(unsigned int *, TextureInfo *, RealShape::Shape *); +}; + +class FEManager { +public: + static FEManager *Get(); + void Render(); +}; + +void bSyncTaskRun(); +int DVDCheckDisk(); +void SoundPause(bool, int); +void SetSoundControlState(bool, int, const char *); +void FEPrintf(const char *, int, const char *, ...); +void FEngTickSinglePackage(const char *, unsigned int); +void eBeginScene(); +void eEndScene(); +int ServiceResourceLoading(); +int bStrLen(const char *); +char *bStrNCpy(char *, const char *, int); +char *bStrCat(char *, char const *, char const *); +int ActualReadJoystickData(); +extern "C" { +void bMemSet(void *, unsigned char, unsigned int); +void bMemCpy(void *, const void *, unsigned int); +} + +extern int FinishedLoadingGlobalSuccesful; +extern int g_discErrorOccured; +extern int g_discErrorNumber; +extern EAXSound *g_pEAXSound; +extern MoviePlayer *gMoviePlayer; +extern const char *s_OpenCover_ErrorText[]; +extern const char FEngDiscErrorPackage[]; +extern PADStatus HardwarePadStatus[4]; + enum VIDEO_MODE { MODE_PAL = 0, MODE_PAL60 = 1, @@ -192,6 +278,288 @@ int DVDValidErrorState(int error) { } } +void DVDErrorTask(void *, int) { + static int resetButtonPressed; + static int queuedSavingResetButtonPressed; + static int resetMode = -1; + static int softwareResetCheckStarted; + static u32 softwareResetStartTick; + static int num_queued_resets; + + int errorIndex = 0; + unsigned int frame = 0; + int scrollIndex = 0; + int resetButtonPressedLocal = 0; + int language = 0; + unsigned int prevButtons = 0; + int scrollOffset = 0; + int errorState = 0; + unsigned int nextFrame; + int driveStatus; + int movieWasPlaying; + long ch; + int textLen; + int scrollLen; + int buttonMask; + cFEng *feng; + const char *pkgName; + char textBuf[16]; + PADStatus padBuf[4]; + + do { + IOModule::GetIOModule().Update(); + + if (cFEngJoyInput::mInstance != 0) { + cFEngJoyInput::mInstance->HandleJoy(); + } + + if (!FinishedLoadingGlobalSuccesful) { + ActualReadJoystickData(); + } + + /* Check for software reset combo (L+R+Start = 0x1600) on pad 0 or pad 1 */ + if ((HardwarePadStatus[0].button & 0x1600) == 0x1600 || + (HardwarePadStatus[1].button & 0x1600) == 0x1600) { + if (!softwareResetCheckStarted) { + softwareResetStartTick = OSGetTick(); + softwareResetCheckStarted = 1; + } else { + u32 currentTick = OSGetTick(); + u32 ticksPerMs = *(volatile u32 *)0x800000F8 / 4000; + u32 elapsed = currentTick - softwareResetStartTick; + u32 msElapsed = elapsed / ticksPerMs; + if (msElapsed > 500) { + resetMode = 0; + resetButtonPressedLocal = 1; + } + } + } else { + softwareResetCheckStarted = 0; + } + + if (MemoryCard::IsCardBusy()) { + /* Card is busy - check for reset button press and queue it */ + if (OSGetResetSwitchState() || resetButtonPressedLocal) { + queuedSavingResetButtonPressed = 1; + } else if (queuedSavingResetButtonPressed) { + resetButtonPressed = 1; + num_queued_resets = num_queued_resets + 1; + } + + nextFrame = frame + 1; + if (MemoryCard::s_pThis != 0) { + MemoryCard::s_pThis->Tick(16); + } + goto loop_end; + } + + if (errorState != 0) { + /* Error state active - run sync tasks and handle input */ + bSyncTaskRun(); + if (MemoryCard::s_pThis != 0) { + MemoryCard::s_pThis->Tick(16); + } + DVDCheckDisk(); + + bMemSet(textBuf, 0, 16); + *(u32 *)&textBuf[0] = 2; + *(u32 *)&textBuf[4] = 2; + *(u32 *)&textBuf[8] = 2; + *(u32 *)&textBuf[12] = 2; + PADControlAllMotors((const u32 *)textBuf); + + LGWheels_ReadAll(plat_lgwheels); + for (ch = 0; ch <= 3; ch++) { + if (LGWheels_IsConnected(plat_lgwheels, ch)) { + LGWheels_StopConstantForce(plat_lgwheels, ch); + LGWheels_StopSurfaceEffect(plat_lgwheels, ch); + LGWheels_StopDamperForce(plat_lgwheels, ch); + LGWheels_StopCarAirborne(plat_lgwheels, ch); + LGWheels_StopSlipperyRoadEffect(plat_lgwheels, ch); + LGWheels_PlaySpringForce(plat_lgwheels, ch, + *(signed char *)((char *)plat_lgwheels + ch * 10 + 3), + 0xb4, 0xb4); + } + } + } + + /* Check for hardware reset button */ + if (num_queued_resets == 0 && resetMode == -1) { + if (OSGetResetSwitchState()) { + resetButtonPressed = 1; + } + } + if (num_queued_resets > 0 || resetButtonPressed) { + resetMode = 0; + } + + driveStatus = DVDGetDriveStatus(); + + if (driveStatus != -1 && resetMode != -1) { + CheckReset(resetMode); + } + + /* Map drive status to error index */ + switch (driveStatus) { + case 5: + errorIndex = 0; + break; + case 4: + errorIndex = 1; + break; + case 6: + errorIndex = 2; + break; + case 11: + errorIndex = 3; + break; + case -1: + errorIndex = 4; + break; + } + + if (MemoryCard::IsCardBusy()) { + return; + } + + errorState = DVDValidErrorState(driveStatus); + if (errorState != 0) { + /* New error detected */ + language = GC_GetOSLanguage(); + g_discErrorNumber = errorState; + if (gMoviePlayer != 0) { + gMoviePlayer->Stop(); + } + g_discErrorOccured = 1; + SoundPause(true, -1); + SetSoundControlState(true, 0x10, "GC Error"); + if (g_pEAXSound != 0) { + g_pEAXSound->Update(0.1f); + } + + feng = cFEng::mInstance; + pkgName = "DiscError.fng"; + if (!feng->IsPackagePushed(pkgName)) { + feng->PushErrorPackage(pkgName, 0, 0xff); + } + + nextFrame = frame + 1; + FEPrintf(pkgName, 0xEEFFD04F, + s_OpenCover_ErrorText[language * 6 + errorIndex]); + } else if (g_discErrorOccured == 0) { + nextFrame = frame + 1; + goto loop_end; + } else { + /* Disc error was active, check if we should service streaming */ + nextFrame = frame + 1; + + if (TheTrackStreamer.UserMemoryAllocationSize <= 0 && + TheTrackStreamer.LoadingPhase != TrackStreamer::LOADING_IDLE) { + ServiceResourceLoading(); + driveStatus = 1; + TheTrackStreamer.ServiceNonGameState(); + TheTrackStreamer.ServiceGameState(); + } + + if (driveStatus != 0) { + /* Scrolling text display */ + scrollLen = (signed char)bStrLen( + s_OpenCover_ErrorText[language * 6 + errorIndex]); + + bMemSet(textBuf, 0, 16); + + buttonMask = 0x10; + if (TheGameFlowManager.GetState() == GAMEFLOW_STATE_RACING) { + buttonMask = 0x40; + } + + if ((frame & buttonMask) != (prevButtons & buttonMask)) { + int rem; + prevButtons = frame; + rem = scrollIndex; + if (scrollIndex < 0) { + rem = scrollIndex + 3; + } + rem = rem & ~3; + scrollOffset = (signed char)(3 - (scrollIndex - rem)); + scrollIndex = scrollIndex + 1; + } + + bStrNCpy(textBuf, + s_OpenCover_ErrorText[language * 6 + errorIndex], + scrollLen - scrollOffset); + + nextFrame = frame + 1; + textLen = bStrLen(textBuf); + while (textLen <= scrollLen) { + bStrCat(textBuf, textBuf, " "); + textLen = textLen + 1; + } + + FEPrintf("DiscError.fng", 0xEEFFD04F, textBuf); + + if (MemoryCard::s_pThis != 0) { + MemoryCard::s_pThis->Tick(16); + } + } else { + /* Error resolved */ + g_discErrorNumber = 0; + g_discErrorOccured = 0; + errorState = 0; + + SoundPause(false, -1); + SetSoundControlState(false, 0x10, "GC Error"); + if (g_pEAXSound != 0) { + g_pEAXSound->Update(0.1f); + } + + movieWasPlaying = 0; + if (gMoviePlayer != 0) { + movieWasPlaying = 1; + gMoviePlayer->Stop(); + } + + feng = cFEng::mInstance; + feng->MakeLoadedPackagesDirty(); + if (feng->IsPackagePushed("DiscError.fng")) { + feng->PopErrorPackage(); + } + nextFrame = frame + 1; + if (movieWasPlaying) { + feng->QueueGameMessage(0xC3960EB9, 0, 0xff); + } + } + } + + /* Render error screen if disc error is active */ + if (g_discErrorOccured != 0) { + FEngTickSinglePackage(FEngDiscErrorPackage, frame); + eBeginScene(); + FEManager::Get()->Render(); + eEndScene(); + + /* Read Logitech wheel data for pad input */ + if (LGWheels_IsConnected(plat_lgwheels, 0) || + LGWheels_IsConnected(plat_lgwheels, 1)) { + LGWheels_ReadAll(plat_lgwheels); + HardwarePadStatus[0].button = *(u16 *)((char *)plat_lgwheels); + HardwarePadStatus[1].button = *(u16 *)((char *)plat_lgwheels + 10); + } else { + PADRead(padBuf); + if (padBuf[0].err == 0) { + bMemCpy(&HardwarePadStatus[0], &padBuf[0], 0xc); + } + if (padBuf[1].err == 0) { + bMemCpy(&HardwarePadStatus[1], &padBuf[1], 0xc); + } + } + } + + loop_end: + frame = nextFrame; + } while (g_discErrorOccured != 0); +} + void ServicePlatform() {} void eInitTexture() {} diff --git a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp index 3ee8e1c38..8599f5122 100644 --- a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp +++ b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp @@ -14,6 +14,8 @@ class MemoryCard { ST_MAX = 3, }; + void Tick(int); + static MemoryCard *s_pThis; static int IsCardBusy(); }; diff --git a/src/Speed/Indep/Src/World/TrackStreamer.hpp b/src/Speed/Indep/Src/World/TrackStreamer.hpp index aa1028f1b..5edf1f2b6 100644 --- a/src/Speed/Indep/Src/World/TrackStreamer.hpp +++ b/src/Speed/Indep/Src/World/TrackStreamer.hpp @@ -162,7 +162,6 @@ class TrackStreamer { return CurrentVisibleSectionTable.IsSet(section_number); } - private: TrackStreamingSection *pTrackStreamingSections; // offset 0x0, size 0x4 int NumTrackStreamingSections; // offset 0x4, size 0x4 DiscBundleSection *pDiscBundleSections; // offset 0x8, size 0x4 From 108bda3692288b2e3ac45086cf181e6472f2d015 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 02:44:24 +0100 Subject: [PATCH 047/172] 72.0%: add PlatAddParticle, fix afxEnd return types, improve ClearXenonEmitters - PlatAddParticle: billboard quad particle rendering (100% match, 608B) - afxEndBillboardedParticleBatch: return int instead of void (100%) - afxEndBillboardedParticles: return int instead of void (100%) - ClearXenonEmitters: add destructor loops via XenonEffectVec::clear (69.3%) Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Render/AcidFX_G.cpp | 62 +++++++++++++++++++++- src/Speed/GameCube/Src/xSparks.cpp | 14 ++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp index d7415a2ad..83d86a66a 100644 --- a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp +++ b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp @@ -1,8 +1,11 @@ #include "Speed/GameCube/Src/Ecstasy/eViewPlat.hpp" #include "Speed/Indep/Src/Camera/Camera.hpp" #include "Speed/Indep/Src/Ecstasy/Ecstasy.hpp" +#include "Speed/Indep/Src/Ecstasy/EmitterSystem.h" #include "Speed/Indep/Src/Ecstasy/Texture.hpp" #include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" +#include +#include #include bVector4 BillboardedParticleBasisX; @@ -56,15 +59,70 @@ bool PlatStartParticleRender(eView *view, TextureInfo *mTextureInfo, unsigned in return 1; } -void afxEndBillboardedParticleBatch(TextureInfo *texture_info, float f, int i) {} +int afxEndBillboardedParticleBatch(TextureInfo *texture_info, float f, int i) { + return 1; +} -void afxEndBillboardedParticles() {} +int afxEndBillboardedParticles() { + return 1; +} void PlatEndParticleRender() { afxEndBillboardedParticleBatch(0, 0.0f, 0); afxEndBillboardedParticles(); } +void PlatAddParticle(const EmitterParticle &particle, const UMath::Vector3 &upVec, const UMath::Vector3 &rightVec, + unsigned int hack_flags, bVector4 *x_constrain_basis, bVector4 *y_constrain_basis) { + unsigned int uv_start_u = particle.mUVStart >> 16; + unsigned int uv_start_v = particle.mUVStart & 0xFFFF; + unsigned int uv_end_u = particle.mUVEnd >> 16; + unsigned int uv_end_v = particle.mUVEnd & 0xFFFF; + + float size = particle.mSize; + UMath::Vector3 bx; + bx.x = BillboardedParticleBasisX.x * size; + bx.y = BillboardedParticleBasisX.y * size; + bx.z = BillboardedParticleBasisX.z * size; + UMath::Vector3 by; + by.x = BillboardedParticleBasisY.x * size; + by.y = BillboardedParticleBasisY.y * size; + by.z = BillboardedParticleBasisY.z * size; + + float u0 = static_cast(uv_start_u) * (1.0f / 65535.0f); + float v0 = static_cast(uv_start_v) * (1.0f / 65535.0f); + float u1 = static_cast(uv_end_u) * (1.0f / 65535.0f); + float v1 = static_cast(uv_end_v) * (1.0f / 65535.0f); + + unsigned int color = particle.mColour; + + GXBegin(GX_QUADS, static_cast(crtVtxFmt), 4); + + GXPosition3f32(particle.mPosX + bx.x + by.x, + particle.mPosY + bx.y + by.y, + particle.mPosZ + bx.z + by.z); + GXColor1u32(color); + GXTexCoord2f32(u1, v1); + + GXPosition3f32(particle.mPosX - bx.x + by.x, + particle.mPosY - bx.y + by.y, + particle.mPosZ - bx.z + by.z); + GXColor1u32(color); + GXTexCoord2f32(u0, v1); + + GXPosition3f32(particle.mPosX - bx.x - by.x, + particle.mPosY - bx.y - by.y, + particle.mPosZ - bx.z - by.z); + GXColor1u32(color); + GXTexCoord2f32(u0, v0); + + GXPosition3f32(particle.mPosX + bx.x - by.x, + particle.mPosY + bx.y - by.y, + particle.mPosZ + bx.z - by.z); + GXColor1u32(color); + GXTexCoord2f32(u1, v0); +} + void PlatGetViewVectors(eView *view, UMath::Vector3 &right, UMath::Vector3 &up, UMath::Vector3 &forward) { eViewPlatInfo *plat_info = view->GetPlatInfo(); Mtx44 local_matrix; diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index a6c862e95..d299524f7 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -13,6 +13,7 @@ struct XenonEffectDef { UMath::Matrix4 mat; // offset 0x10, size 0x40 Attrib::Collection *spec; // offset 0x50, size 0x4 EmitterGroup *piggyback_effect; // offset 0x54, size 0x4 + ~XenonEffectDef() {} }; struct XenonEffectVec { @@ -20,6 +21,15 @@ struct XenonEffectVec { XenonEffectDef *finish; // offset 0x4, size 0x4 void *unused; // offset 0x8, size 0x4 XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 + + void clear() { + XenonEffectDef *p = start; + while (p != finish) { + p->~XenonEffectDef(); + p++; + } + finish = start; + } }; struct XenonEffectLists { @@ -240,8 +250,8 @@ void ParticleList::GeneratePolys() { void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { - gNGEffectList.active.finish = gNGEffectList.active.start; - gNGEffectList.staging.finish = gNGEffectList.staging.start; + gNGEffectList.active.clear(); + gNGEffectList.staging.clear(); } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { From ae1fe919201d7076c19357ab175d73ac13ccdc37 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 09:37:59 +0100 Subject: [PATCH 048/172] 73.4%: add vector reserve implementation, improve Wheels::IsConnected - vector::reserve: 85.9% match (476B) Uses extern "C" with exact mangled name to emit correct symbol - Wheels::IsConnected: simplified bool return expression Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/Wheels.cpp | 2 +- src/Speed/GameCube/Src/xSparks.cpp | 69 ++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index 066ce78ad..83a6bc064 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -68,7 +68,7 @@ bool Wheels::ButtonIsPressed(long channel, unsigned long buttonMask) { bool Wheels::IsConnected(long channel) { const LGPosition *position = reinterpret_cast(this); - return position[channel].err != 0; + return position[channel].err; } bool Wheels::PedalsConnected(long channel) { diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index d299524f7..d9d085a47 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -79,8 +79,69 @@ extern ParticleList gParticleList; extern XSpriteManager NGSpriteManager; extern unsigned int randomSeed; -void reserveXenonEffectVec(void *vec, unsigned int count) - __asm__("reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi"); +extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( + XenonEffectVec *vec, unsigned int count); + +static inline void reserveXenonEffectVecImpl(XenonEffectVec *vec, unsigned int count) { + reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi(vec, count); +} + +extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( + XenonEffectVec *vec, unsigned int count) { + unsigned int capacity = vec->end_of_storage - vec->start; + if (capacity >= count) { + return; + } + + unsigned int size = vec->finish - vec->start; + XenonEffectDef *old_start = vec->start; + + XenonEffectDef *new_buf; + unsigned int new_bytes; + if (old_start != 0) { + if (count != 0) { + new_bytes = count * sizeof(XenonEffectDef); + new_buf = static_cast(gFastMem.Alloc(new_bytes, 0)); + } else { + new_buf = 0; + new_bytes = 0; + } + + XenonEffectDef *src = old_start; + XenonEffectDef *dst = new_buf; + while (src != vec->finish) { + if (dst != 0) { + *dst = *src; + } + src++; + dst++; + } + + XenonEffectDef *old_iter = vec->start; + XenonEffectDef *old_finish = vec->finish; + while (old_iter != old_finish) { + old_iter->~XenonEffectDef(); + old_iter++; + } + + unsigned int old_capacity = vec->end_of_storage - vec->start; + if (vec->start != 0) { + gFastMem.Free(vec->start, old_capacity * sizeof(XenonEffectDef), 0); + } + } else { + if (count != 0) { + new_bytes = count * sizeof(XenonEffectDef); + new_buf = static_cast(gFastMem.Alloc(new_bytes, 0)); + } else { + new_buf = 0; + new_bytes = 0; + } + } + + vec->end_of_storage = reinterpret_cast(reinterpret_cast(new_buf) + new_bytes); + vec->start = new_buf; + vec->finish = reinterpret_cast(reinterpret_cast(new_buf) + size * sizeof(XenonEffectDef)); +} float bRandom(float range, unsigned int *seed); unsigned int bStringHash(const char *str); TextureInfo *GetTextureInfo(unsigned int name_hash, int allow_default, int force_local); @@ -259,12 +320,12 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, if (size < 20) { unsigned int active_capacity = gNGEffectList.active.end_of_storage - gNGEffectList.active.start; if (active_capacity < 20) { - reserveXenonEffectVec(&gNGEffectList.active, 20); + reserveXenonEffectVecImpl(&gNGEffectList.active, 20); } unsigned int staging_capacity = gNGEffectList.staging.end_of_storage - gNGEffectList.staging.start; if (staging_capacity < 20) { - reserveXenonEffectVec(&gNGEffectList.staging, 20); + reserveXenonEffectVecImpl(&gNGEffectList.staging, 20); } XenonEffectDef effect; From daab1302f433a8738bbe57f851fe21f21b7d9411 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 09:44:09 +0100 Subject: [PATCH 049/172] 73.7%: improve GCHW_VD::iDraw VP6 path, fix GXSetTevOp arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GCHW_VD::iDraw: 92.7% → 97.8% by restructuring VP6 border calculation to add/subtract h in-place and swapping if/else branches - Fix GXSetTevOp second argument: GX_PASSCLR → GX_MODULATE Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Movie_GC.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 002ecb44b..8c31f3412 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -123,19 +123,19 @@ void GCHW_VD::iDraw() { float m_b; float m_z; - if (!mIsVP6) { - cb = y + w * h; - cr = cb + (w / 2) * (h / 2); - } else { - const int vp6Border = 0x60; - int dataOfs = w + vp6Border; - int uvOfs = (dataOfs / 2) * 0x18; + if (mIsVP6) { + w += 0x60; + h += 0x60; + int uvOfs = (w / 2) * 0x18; - cb = y + dataOfs * (h + vp6Border); - y += dataOfs * 0x30; - cr = cb + (dataOfs / 2) * ((h + vp6Border) / 2) + uvOfs; + cb = y + w * h; + y += w * 0x30; + cr = cb + (w / 2) * (h / 2) + uvOfs; cb += uvOfs; - w = dataOfs; + h -= 0x60; + } else { + cb = y + w * h; + cr = cb + (w / 2) * (h / 2); } GXSetCullMode(GX_CULL_NONE); @@ -152,7 +152,7 @@ void GCHW_VD::iDraw() { GXSetChanCtrl(static_cast(4), GX_FALSE, static_cast(0), static_cast(1), GX_LIGHT_NULL, static_cast(2), static_cast(2)); GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); - GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR); + GXSetTevOp(GX_TEVSTAGE0, GX_MODULATE); DCFlushRangeNoSync(y, w * h); DCFlushRangeNoSync(cr, size); DCFlushRangeNoSync(cb, size); From 95e3e6d7803c790c92b4a63357b18806a5134f50 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 10:43:41 +0100 Subject: [PATCH 050/172] 74.0%: add VMStatsManager globals, fix gNGEffectList, remove STL template bloat - Define gVMStatsManager_FE/LS/IG with constructors in Platform_G.cpp to match target static initialization - Change gNGEffectList from extern to definition with XenonEffectVec constructors (zero-init + reserve(20)) - Convert XenonEffectLists to use array[2] to match target's loop-based array construction in static init - Move DatabasePrivate inline methods out of AttribPrivate.h into AttribDatabase.cpp to eliminate ~1488B of extra STL template instantiations (find, _Rb_tree, _List_base, vector::reserve) - Restructure BootupCheckDone to call GetMemcard() in each branch Co-Authored-By: Claude Opus 4.6 --- .../GameCube/Src/MemoryCard/MemoryCardImp.cpp | 9 ++- src/Speed/GameCube/Src/Platform_G.cpp | 10 +++ src/Speed/GameCube/Src/xSparks.cpp | 52 +++++++------- .../Runtime/Common/AttribDatabase.cpp | 63 +++++++++++++++++ .../AttribSys/Runtime/Common/AttribPrivate.h | 67 ++----------------- 5 files changed, 112 insertions(+), 89 deletions(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index e5a0c1d00..08a7b5df8 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -142,14 +142,13 @@ void MemoryCardImp::BootupCheckDone(RealmcIface::CardStatus status, RealmcIface: MemoryCard *memcard = GetMemcard(); if (*reinterpret_cast(reinterpret_cast(memcard) + 0x30)) { - void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); - const char *package_name = *reinterpret_cast(reinterpret_cast(screen) + 0xC); - if (status == RealmcIface::STATUS_NO_CARD || status == RealmcIface::STATUS_CARD_DAMAGED || status == RealmcIface::STATUS_WRONG_DEVICE || status == RealmcIface::STATUS_CARD_FULL) { - cFEng::mInstance->QueuePackageMessage(0x8867412D, package_name, 0); + void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); + cFEng::mInstance->QueuePackageMessage(0x8867412D, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); } else { - cFEng::mInstance->QueuePackageMessage(0x3A2BE557, package_name, 0); + void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); + cFEng::mInstance->QueuePackageMessage(0x3A2BE557, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); } } } diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 3e0fe7cf6..140ed9668 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -595,9 +595,19 @@ struct VMStatsManager { float mMaxFrameTime; const char *DebugName; + VMStatsManager(const char *name) { + mFrameStats.Init(); + Init(name); + mInitialized = false; + } + void Init(const char *name); }; +VMStatsManager gVMStatsManager_FE("Frontend"); +VMStatsManager gVMStatsManager_LS("LoadScreen Streamer"); +VMStatsManager gVMStatsManager_IG("InGame"); + void VMStats::Init() { mServiceTimeMax = 0; mServiceTimeAvg = 0.0f; diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index d9d085a47..96e5f0523 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -16,12 +16,21 @@ struct XenonEffectDef { ~XenonEffectDef() {} }; +struct XenonEffectVec; + +extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( + XenonEffectVec *vec, unsigned int count); + struct XenonEffectVec { XenonEffectDef *start; // offset 0x0, size 0x4 XenonEffectDef *finish; // offset 0x4, size 0x4 void *unused; // offset 0x8, size 0x4 XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 + XenonEffectVec() : start(0), finish(0), end_of_storage(0) { + reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi(this, 20); + } + void clear() { XenonEffectDef *p = start; while (p != finish) { @@ -33,8 +42,8 @@ struct XenonEffectVec { }; struct XenonEffectLists { - XenonEffectVec active; // offset 0x0, size 0x10 - XenonEffectVec staging; // offset 0x10, size 0x10 + enum { ACTIVE = 0, STAGING = 1 }; + XenonEffectVec lists[2]; // [0]=active, [1]=staging }; struct CGEmitter { @@ -74,14 +83,11 @@ class ParticleList { uint32 GetNumParticles(); }; -extern XenonEffectLists gNGEffectList; +XenonEffectLists gNGEffectList; extern ParticleList gParticleList; extern XSpriteManager NGSpriteManager; extern unsigned int randomSeed; -extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( - XenonEffectVec *vec, unsigned int count); - static inline void reserveXenonEffectVecImpl(XenonEffectVec *vec, unsigned int count) { reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi(vec, count); } @@ -311,21 +317,21 @@ void ParticleList::GeneratePolys() { void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { - gNGEffectList.active.clear(); - gNGEffectList.staging.clear(); + gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); + gNGEffectList.lists[XenonEffectLists::STAGING].clear(); } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { - unsigned int size = gNGEffectList.active.finish - gNGEffectList.active.start; + unsigned int size = gNGEffectList.lists[XenonEffectLists::ACTIVE].finish - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; if (size < 20) { - unsigned int active_capacity = gNGEffectList.active.end_of_storage - gNGEffectList.active.start; + unsigned int active_capacity = gNGEffectList.lists[XenonEffectLists::ACTIVE].end_of_storage - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; if (active_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.active, 20); + reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::ACTIVE], 20); } - unsigned int staging_capacity = gNGEffectList.staging.end_of_storage - gNGEffectList.staging.start; + unsigned int staging_capacity = gNGEffectList.lists[XenonEffectLists::STAGING].end_of_storage - gNGEffectList.lists[XenonEffectLists::STAGING].start; if (staging_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.staging, 20); + reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::STAGING], 20); } XenonEffectDef effect; @@ -335,29 +341,29 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, effect.piggyback_effect = piggyback_fx; effect.vel = *vel; - *gNGEffectList.active.finish = effect; - ++gNGEffectList.active.finish; + *gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = effect; + ++gNGEffectList.lists[XenonEffectLists::ACTIVE].finish; } } void UpdateXenonEmitters(float dt) { gParticleList.AgeParticles(dt); - XenonEffectDef *effect = gNGEffectList.active.start; - while (effect != gNGEffectList.active.finish) { - *gNGEffectList.staging.finish = *effect; - ++gNGEffectList.staging.finish; + XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { + *gNGEffectList.lists[XenonEffectLists::STAGING].finish = *effect; + ++gNGEffectList.lists[XenonEffectLists::STAGING].finish; ++effect; } - gNGEffectList.active.finish = gNGEffectList.active.start; + gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; - effect = gNGEffectList.staging.start; - while (effect != gNGEffectList.staging.finish) { + effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; + while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { NGEffect ng_effect(*effect); ++effect; } - gNGEffectList.staging.finish = gNGEffectList.staging.start; + gNGEffectList.lists[XenonEffectLists::STAGING].finish = gNGEffectList.lists[XenonEffectLists::STAGING].start; gParticleList.GeneratePolys(); } diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp index 6f9d1d145..f9cce1288 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribDatabase.cpp @@ -135,6 +135,69 @@ class CollectionExportPolicy : public IExportPolicy { } }; +void DatabasePrivate::QueueForDelete(const Collection *obj, std::list &bag) { + obj->IsReferenced(); + if (std::find(bag.begin(), bag.end(), obj) == bag.end()) { + bag.push_back(obj); + } +} + +void DatabasePrivate::QueueForDelete(const Class *obj, std::list &bag) { + obj->IsReferenced(); + if (std::find(bag.begin(), bag.end(), obj) == bag.end()) { + bag.push_back(obj); + } +} + +void DatabasePrivate::CollectGarbageBag(std::list &bag) { + std::list::iterator iter = bag.begin(); + + while (iter != bag.end()) { + const Collection *obj = *iter; + if (!obj->IsReferenced()) { + obj->Delete(); + } + bag.pop_front(); + iter = bag.begin(); + } +} + +void DatabasePrivate::CollectGarbageBag(std::list &bag) { + std::list::iterator iter = bag.begin(); + + while (iter != bag.end()) { + const Class *obj = *iter; + if (!obj->IsReferenced()) { + obj->Delete(); + } + bag.pop_front(); + iter = bag.begin(); + } +} + +DatabasePrivate::DatabasePrivate(const DatabaseLoadData &loadData) : Database(*this), mClasses(loadData.mNumClasses) { + mClasses.Reserve(loadData.mNumClasses); + mNumCompiledTypes = loadData.mNumTypes + 1; + mCompiledTypes.reserve(mNumCompiledTypes); + DefaultDataArea(loadData.mDefaultDataSize); + mCompiledTypes.push_back(&*mTypes.insert(TypeDesc()).first); + + const unsigned int *sizes = loadData.GetTypeSizes(); + const char *name = loadData.mTypenames; + + for (unsigned int i = 0; i < loadData.mNumTypes; i++) { + TypeTable::iterator iter = mTypes.insert(TypeDesc(name, sizes[i], mCompiledTypes.size())).first; + mCompiledTypes.push_back(&*iter); + name += strlen(name) + 1; + } +} + +DatabasePrivate::~DatabasePrivate() { + mClasses.Size(); + mTypes.clear(); + mCompiledTypes.clear(); +} + Database *Database::sThis = nullptr; static unsigned int gDatabaseType = StringToTypeID("Attrib::DatabaseLoadData"); diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h index d27bcd498..8fff34657 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/Common/AttribPrivate.h @@ -264,45 +264,10 @@ class DatabaseLoadData { // total size: 0x4C class DatabasePrivate : public Database { public: - static void QueueForDelete(const Collection *obj, std::list &bag) { - obj->IsReferenced(); - if (std::find(bag.begin(), bag.end(), obj) == bag.end()) { - bag.push_back(obj); - } - } - - static void QueueForDelete(const Class *obj, std::list &bag) { - obj->IsReferenced(); - if (std::find(bag.begin(), bag.end(), obj) == bag.end()) { - bag.push_back(obj); - } - } - - static void CollectGarbageBag(std::list &bag) { - std::list::iterator iter = bag.begin(); - - while (iter != bag.end()) { - const Collection *obj = *iter; - if (!obj->IsReferenced()) { - obj->Delete(); - } - bag.pop_front(); - iter = bag.begin(); - } - } - - static void CollectGarbageBag(std::list &bag) { - std::list::iterator iter = bag.begin(); - - while (iter != bag.end()) { - const Class *obj = *iter; - if (!obj->IsReferenced()) { - obj->Delete(); - } - bag.pop_front(); - iter = bag.begin(); - } - } + static void QueueForDelete(const Collection *obj, std::list &bag); + static void QueueForDelete(const Class *obj, std::list &bag); + static void CollectGarbageBag(std::list &bag); + static void CollectGarbageBag(std::list &bag); void *operator new(std::size_t bytes) { return Alloc(bytes, "Attrib::DatabasePrivate"); @@ -312,28 +277,8 @@ class DatabasePrivate : public Database { Free(ptr, bytes, "Attrib::DatabasePrivate"); } - DatabasePrivate(const DatabaseLoadData &loadData) : Database(*this), mClasses(loadData.mNumClasses) { - mClasses.Reserve(loadData.mNumClasses); - mNumCompiledTypes = loadData.mNumTypes + 1; - mCompiledTypes.reserve(mNumCompiledTypes); - DefaultDataArea(loadData.mDefaultDataSize); - mCompiledTypes.push_back(&*mTypes.insert(TypeDesc()).first); - - const unsigned int *sizes = loadData.GetTypeSizes(); - const char *name = loadData.mTypenames; - - for (unsigned int i = 0; i < loadData.mNumTypes; i++) { - TypeTable::iterator iter = mTypes.insert(TypeDesc(name, sizes[i], mCompiledTypes.size())).first; - mCompiledTypes.push_back(&*iter); - name += strlen(name) + 1; - } - } - - ~DatabasePrivate() { - mClasses.Size(); - mTypes.clear(); - mCompiledTypes.clear(); - } + DatabasePrivate(const DatabaseLoadData &loadData); + ~DatabasePrivate(); ClassTable mClasses; // offset 0x8, size 0x10 unsigned int mNumCompiledTypes; // offset 0x18, size 0x4 From 2e2af0fd7de8f32fd091ade2813a25924b09fbca Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 10:50:57 +0100 Subject: [PATCH 051/172] 74.2%: fix eSetTexture to call GetPlatInfo() each time instead of caching Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index 65ce39b07..af31c1b45 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -185,17 +185,16 @@ unsigned char TextureInfoPlatInfo::SetImage(int width, int height, int mip, int int eSetTexture(TextureInfo *texture_info, int stage) { static int stagePrev; - TextureInfoPlatInfo *plat_info = texture_info->GetPlatInfo(); if (texture_info == pTexPrev && stage == stagePrev) { return 0; } - if (plat_info->HasClut()) { - GXLoadTlut(&plat_info->ImageInfos.objClut, 0); + if (texture_info->GetPlatInfo()->HasClut()) { + GXLoadTlut(&texture_info->GetPlatInfo()->ImageInfos.objClut, 0); } - GXLoadTexObj(&plat_info->ImageInfos.obj, static_cast(stage)); + GXLoadTexObj(&texture_info->GetPlatInfo()->ImageInfos.obj, static_cast(stage)); pTexPrev = texture_info; stagePrev = stage; From ed210ea550bd824d572d040f66d4600d5fa3da96 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 11:13:18 +0100 Subject: [PATCH 052/172] 75.9%: improve xenon effect vector flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 84 ++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 96e5f0523..ddcefb212 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -39,6 +39,54 @@ struct XenonEffectVec { } finish = start; } + + void push_back(const XenonEffectDef &value) { + if (finish == end_of_storage) { + unsigned int size = finish - start; + unsigned int old_capacity = end_of_storage - start; + unsigned int new_capacity = size + (size == 0 ? 1 : size); + unsigned int new_bytes; + XenonEffectDef *new_start; + XenonEffectDef *src; + XenonEffectDef *dst; + + if (new_capacity != 0) { + new_bytes = new_capacity * sizeof(XenonEffectDef); + new_start = static_cast(gFastMem.Alloc(new_bytes, 0)); + } else { + new_start = 0; + new_bytes = 0; + } + + src = start; + dst = new_start; + while (src != finish) { + if (dst != 0) { + *dst = *src; + } + src++; + dst++; + } + + if (dst != 0) { + *dst = value; + } + dst++; + + if (start != 0) { + gFastMem.Free(start, old_capacity * sizeof(XenonEffectDef), 0); + } + + start = new_start; + finish = dst; + end_of_storage = reinterpret_cast(reinterpret_cast(new_start) + new_bytes); + } else { + if (finish != 0) { + *finish = value; + } + finish++; + } + } }; struct XenonEffectLists { @@ -322,48 +370,52 @@ void ClearXenonEmitters() { } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { - unsigned int size = gNGEffectList.lists[XenonEffectLists::ACTIVE].finish - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + XenonEffectVec &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; + XenonEffectVec &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; + unsigned int size = active.finish - active.start; + if (size < 20) { - unsigned int active_capacity = gNGEffectList.lists[XenonEffectLists::ACTIVE].end_of_storage - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + unsigned int active_capacity = active.end_of_storage - active.start; if (active_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::ACTIVE], 20); + reserveXenonEffectVecImpl(&active, 20); } - unsigned int staging_capacity = gNGEffectList.lists[XenonEffectLists::STAGING].end_of_storage - gNGEffectList.lists[XenonEffectLists::STAGING].start; + unsigned int staging_capacity = staging.end_of_storage - staging.start; if (staging_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::STAGING], 20); + reserveXenonEffectVecImpl(&staging, 20); } XenonEffectDef effect; + effect.vel = *vel; effect.mat = UMath::Matrix4::kIdentity; effect.mat.v3 = mat->v3; effect.spec = const_cast(spec); effect.piggyback_effect = piggyback_fx; - effect.vel = *vel; - *gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = effect; - ++gNGEffectList.lists[XenonEffectLists::ACTIVE].finish; + active.push_back(effect); } } void UpdateXenonEmitters(float dt) { + XenonEffectVec &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; + XenonEffectVec &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; + gParticleList.AgeParticles(dt); - XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; - while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { - *gNGEffectList.lists[XenonEffectLists::STAGING].finish = *effect; - ++gNGEffectList.lists[XenonEffectLists::STAGING].finish; + XenonEffectDef *effect = active.start; + while (effect != active.finish) { + staging.push_back(*effect); ++effect; } - gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + active.finish = active.start; - effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; - while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { + effect = staging.start; + while (effect != staging.finish) { NGEffect ng_effect(*effect); ++effect; } - gNGEffectList.lists[XenonEffectLists::STAGING].finish = gNGEffectList.lists[XenonEffectLists::STAGING].start; + staging.finish = staging.start; gParticleList.GeneratePolys(); } From 8fa458952bb994dd0fcd7be59748d88f5d830d9f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 11:18:30 +0100 Subject: [PATCH 053/172] 76.2%: improve AddXenonEffect construction order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 33 +++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index ddcefb212..be88d45bb 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -370,52 +370,47 @@ void ClearXenonEmitters() { } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { - XenonEffectVec &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; - XenonEffectVec &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; - unsigned int size = active.finish - active.start; + unsigned int size = gNGEffectList.lists[XenonEffectLists::ACTIVE].finish - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; if (size < 20) { - unsigned int active_capacity = active.end_of_storage - active.start; + unsigned int active_capacity = gNGEffectList.lists[XenonEffectLists::ACTIVE].end_of_storage - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; if (active_capacity < 20) { - reserveXenonEffectVecImpl(&active, 20); + reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::ACTIVE], 20); } - unsigned int staging_capacity = staging.end_of_storage - staging.start; + unsigned int staging_capacity = gNGEffectList.lists[XenonEffectLists::STAGING].end_of_storage - gNGEffectList.lists[XenonEffectLists::STAGING].start; if (staging_capacity < 20) { - reserveXenonEffectVecImpl(&staging, 20); + reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::STAGING], 20); } XenonEffectDef effect; - effect.vel = *vel; effect.mat = UMath::Matrix4::kIdentity; effect.mat.v3 = mat->v3; effect.spec = const_cast(spec); effect.piggyback_effect = piggyback_fx; + effect.vel = *vel; - active.push_back(effect); + gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(effect); } } void UpdateXenonEmitters(float dt) { - XenonEffectVec &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; - XenonEffectVec &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; - gParticleList.AgeParticles(dt); - XenonEffectDef *effect = active.start; - while (effect != active.finish) { - staging.push_back(*effect); + XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { + gNGEffectList.lists[XenonEffectLists::STAGING].push_back(*effect); ++effect; } - active.finish = active.start; + gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; - effect = staging.start; - while (effect != staging.finish) { + effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; + while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { NGEffect ng_effect(*effect); ++effect; } - staging.finish = staging.start; + gNGEffectList.lists[XenonEffectLists::STAGING].finish = gNGEffectList.lists[XenonEffectLists::STAGING].start; gParticleList.GeneratePolys(); } From 8fa418f89b2897a392f8d5955c9fd91c7521790a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 11:21:00 +0100 Subject: [PATCH 054/172] 76.8%: improve UpdateXenonEmitters staging temps Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index be88d45bb..9a6320d07 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -399,7 +399,8 @@ void UpdateXenonEmitters(float dt) { XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { - gNGEffectList.lists[XenonEffectLists::STAGING].push_back(*effect); + XenonEffectDef staged_effect = *effect; + gNGEffectList.lists[XenonEffectLists::STAGING].push_back(staged_effect); ++effect; } @@ -407,7 +408,8 @@ void UpdateXenonEmitters(float dt) { effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { - NGEffect ng_effect(*effect); + XenonEffectDef staged_effect = *effect; + NGEffect ng_effect(staged_effect); ++effect; } From 0aa9634b1d8292c462724e7eb5ee29289e4c2ff0 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 14:11:29 +0100 Subject: [PATCH 055/172] 77.3%: match GeneratePolys and CGEmitter constructor Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 51 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 9a6320d07..3071d4c37 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -203,8 +203,9 @@ TextureInfo *GetTextureInfo(unsigned int name_hash, int allow_default, int force CGEmitter::CGEmitter(const Attrib::Collection *spec, const XenonEffectDef &eDef) : mEmitterDef(spec, 0, nullptr) // , mTextureUVs(mEmitterDef.emitteruv(), 0, nullptr) // - , mVel(eDef.vel) // - , mLocalWorld(eDef.mat) {} + , mLocalWorld(eDef.mat) { + mVel = eDef.vel; +} CGEmitter::~CGEmitter() {} @@ -264,14 +265,14 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { length_clamped = length_start; } - volume_extent.x = 1.0f - (mEmitterDef.VolumeExtent().x - bRandom(mEmitterDef.VolumeExtent().x, &seed) * 2.0f); - volume_extent.y = 1.0f - (mEmitterDef.VolumeExtent().y - bRandom(mEmitterDef.VolumeExtent().y, &seed) * 2.0f); - volume_extent.z = 1.0f - (mEmitterDef.VolumeExtent().z - bRandom(mEmitterDef.VolumeExtent().z, &seed) * 2.0f); + volume_extent.x = 1.0f - (mEmitterDef.VelocityDelta().x - bRandom(mEmitterDef.VelocityDelta().x, &seed) * 2.0f); + volume_extent.y = 1.0f - (mEmitterDef.VelocityDelta().y - bRandom(mEmitterDef.VelocityDelta().y, &seed) * 2.0f); + volume_extent.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &seed) * 2.0f); volume_extent.w = 1.0f; - spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeCenter().x, &seed) - mEmitterDef.VolumeCenter().x * 0.5f); - spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeCenter().y, &seed) - mEmitterDef.VolumeCenter().y * 0.5f); - spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeCenter().z, &seed) - mEmitterDef.VolumeCenter().z * 0.5f); + spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeExtent().x, &seed) - mEmitterDef.VolumeExtent().x * 0.5f); + spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeExtent().y, &seed) - mEmitterDef.VolumeExtent().y * 0.5f); + spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeExtent().z, &seed) - mEmitterDef.VolumeExtent().z * 0.5f); spawn_point.w = 1.0f; UMath::RotateTranslate(spawn_point, local_world, world_spawn_point); @@ -336,28 +337,22 @@ void ParticleList::AgeParticles(float dt) { } void ParticleList::GeneratePolys() { - NGParticle *particle = mParticles; - if (mNumParticles != 0) { if (!mContrail_tex) { mContrail_tex = GetTextureInfo(bStringHash("PS2_CONTRAIL"), 0, 0); mSparks_tex = GetTextureInfo(bStringHash("PS2_SPARKS"), 0, 0); } - { - unsigned int i = 0; - - do { - if (particle->uv[0] == 0x7f) { - mCurrentTexture = mContrail_tex; - } else { - mCurrentTexture = mSparks_tex; - } + NGParticle *particle = mParticles; + for (unsigned int i = 0; i < mNumParticles; i++) { + if (particle->uv[0] == 0x7f) { + mCurrentTexture = mContrail_tex; + } else { + mCurrentTexture = mSparks_tex; + } - i++; - NGSpriteManager.AddSpark(*particle, mCurrentTexture); - particle++; - } while (i < mNumParticles); + NGSpriteManager.AddSpark(*particle, mCurrentTexture); + particle++; } } } @@ -395,24 +390,26 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, } void UpdateXenonEmitters(float dt) { + XenonEffectDef staged_effect; + gParticleList.AgeParticles(dt); XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { - XenonEffectDef staged_effect = *effect; + staged_effect = *effect; gNGEffectList.lists[XenonEffectLists::STAGING].push_back(staged_effect); ++effect; } - gNGEffectList.lists[XenonEffectLists::ACTIVE].finish = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { - XenonEffectDef staged_effect = *effect; + staged_effect = *effect; NGEffect ng_effect(staged_effect); ++effect; } - gNGEffectList.lists[XenonEffectLists::STAGING].finish = gNGEffectList.lists[XenonEffectLists::STAGING].start; + gNGEffectList.lists[XenonEffectLists::STAGING].clear(); gParticleList.GeneratePolys(); } From 600165bbbae6d10ff1d416a1b76fa236b621551a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 14:26:48 +0100 Subject: [PATCH 056/172] 77.6%: improve eBuildSunPoly and eBuildSunPolyFix register flow Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Render/SunE.cpp | 114 ++++++++++++++----------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 8f108b5be..430545375 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -52,7 +52,7 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float eGetScreenHeight(); - if (layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { max_size = layer->Size; } @@ -69,42 +69,49 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float poly->Vertices[2].z = 1.0f; poly->Vertices[3].z = 1.0f; - diagonal0 = half_size * cos_angle; diagonal1 = half_size * sin_angle; + diagonal0 = half_size * cos_angle; intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; center_x = x + layer->OffsetX; center_y = y + layer->OffsetY; - if (intensity >= 28.0f) { - alpha = static_cast(static_cast(intensity - 28.0f)); - } else { + if (intensity < 28.0f) { alpha = static_cast(static_cast(intensity)); + } else { + alpha = static_cast(static_cast(intensity - 28.0f)); } - poly->Vertices[3].x = center_x - (diagonal0 - diagonal1); - poly->Vertices[0].x = center_x - (diagonal1 + diagonal0); - poly->Vertices[3].y = center_y + (diagonal1 + diagonal0); - poly->Vertices[0].y = center_y - (diagonal0 - diagonal1); - poly->Vertices[1].y = center_y - (diagonal1 + diagonal0); - poly->Vertices[1].x = center_x + (diagonal0 - diagonal1); - poly->Vertices[2].x = center_x + (diagonal1 + diagonal0); - poly->Vertices[2].y = center_y + (diagonal0 - diagonal1); + float sum = diagonal1 + diagonal0; + float diff = diagonal0 - diagonal1; + + poly->Vertices[3].x = center_x - diff; + poly->Vertices[0].x = center_x - sum; + poly->Vertices[3].y = center_y + sum; + poly->Vertices[0].y = center_y - diff; + poly->Vertices[1].y = center_y - sum; + poly->Vertices[1].x = center_x + diff; + poly->Vertices[2].x = center_x + sum; + poly->Vertices[2].y = center_y + diff; + + unsigned char c0 = layer->Colour[0]; + unsigned char c1 = layer->Colour[1]; + unsigned char c2 = layer->Colour[2]; poly->Colours[3][3] = alpha; - poly->Colours[3][0] = layer->Colour[0]; - poly->Colours[3][1] = layer->Colour[1]; - poly->Colours[3][2] = layer->Colour[2]; - poly->Colours[0][0] = layer->Colour[0]; - poly->Colours[0][1] = layer->Colour[1]; - poly->Colours[0][2] = layer->Colour[2]; + poly->Colours[3][0] = c0; + poly->Colours[3][1] = c1; + poly->Colours[3][2] = c2; + poly->Colours[0][0] = c0; + poly->Colours[0][1] = c1; + poly->Colours[0][2] = c2; poly->Colours[0][3] = alpha; - poly->Colours[1][0] = layer->Colour[0]; - poly->Colours[1][1] = layer->Colour[1]; - poly->Colours[1][2] = layer->Colour[2]; + poly->Colours[1][0] = c0; + poly->Colours[1][1] = c1; + poly->Colours[1][2] = c2; poly->Colours[1][3] = alpha; - poly->Colours[2][0] = layer->Colour[0]; - poly->Colours[2][1] = layer->Colour[1]; - poly->Colours[2][2] = layer->Colour[2]; + poly->Colours[2][0] = c0; + poly->Colours[2][1] = c1; + poly->Colours[2][2] = c2; poly->Colours[2][3] = alpha; } @@ -123,7 +130,7 @@ void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, flo eGetScreenHeight(); - if (layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { max_size = layer->Size; } @@ -140,27 +147,30 @@ void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, flo poly->Vertices[2].z = sun_vis_poly_fix_ini[2]; poly->Vertices[3].z = sun_vis_poly_fix_ini[2]; - diagonal0 = half_size * cos_angle; diagonal1 = half_size * sin_angle; + diagonal0 = half_size * cos_angle; intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; center_x = x + layer->OffsetX; center_y = y + layer->OffsetY; - if (intensity >= 28.0f) { - alpha = static_cast(static_cast(intensity - 28.0f)); - } else { + if (intensity < 28.0f) { alpha = static_cast(static_cast(intensity)); + } else { + alpha = static_cast(static_cast(intensity - 28.0f)); } - poly->Vertices[3].x = center_x - (diagonal0 - diagonal1); - poly->Vertices[3].y = center_y + (diagonal1 + diagonal0); - poly->Vertices[0].y = center_y - (diagonal0 - diagonal1); - sun_vis_poly_fix_ini[0] = center_x - (diagonal1 + diagonal0); + float sum = diagonal1 + diagonal0; + float diff = diagonal0 - diagonal1; + + poly->Vertices[3].x = center_x - diff; + poly->Vertices[3].y = center_y + sum; + poly->Vertices[0].y = center_y - diff; + sun_vis_poly_fix_ini[0] = center_x - sum; poly->Vertices[0].x = sun_vis_poly_fix_ini[0]; - poly->Vertices[1].y = center_y - (diagonal1 + diagonal0); - poly->Vertices[1].x = center_x + (diagonal0 - diagonal1); - poly->Vertices[2].y = center_y + (diagonal0 - diagonal1); - poly->Vertices[2].x = center_x + (diagonal1 + diagonal0); + poly->Vertices[1].y = center_y - sum; + poly->Vertices[1].x = center_x + diff; + poly->Vertices[2].y = center_y + diff; + poly->Vertices[2].x = center_x + sum; sun_vis_poly_fix_ini[1] = poly->Vertices[0].y; sun_vis_poly_fix_ini[4] = poly->Vertices[1].x; @@ -170,21 +180,25 @@ void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, flo sun_vis_poly_fix_ini[12] = poly->Vertices[3].x; sun_vis_poly_fix_ini[13] = poly->Vertices[3].y; - poly->Colours[0][0] = layer->Colour[0]; - poly->Colours[0][1] = layer->Colour[1]; - poly->Colours[0][2] = layer->Colour[2]; + unsigned char c0 = layer->Colour[0]; + unsigned char c1 = layer->Colour[1]; + unsigned char c2 = layer->Colour[2]; + + poly->Colours[0][0] = c0; + poly->Colours[0][1] = c1; + poly->Colours[0][2] = c2; poly->Colours[0][3] = alpha; - poly->Colours[1][0] = layer->Colour[0]; - poly->Colours[1][1] = layer->Colour[1]; - poly->Colours[1][2] = layer->Colour[2]; + poly->Colours[1][0] = c0; + poly->Colours[1][1] = c1; + poly->Colours[1][2] = c2; poly->Colours[1][3] = alpha; - poly->Colours[2][0] = layer->Colour[0]; - poly->Colours[2][1] = layer->Colour[1]; - poly->Colours[2][2] = layer->Colour[2]; + poly->Colours[2][0] = c0; + poly->Colours[2][1] = c1; + poly->Colours[2][2] = c2; poly->Colours[2][3] = alpha; - poly->Colours[3][0] = layer->Colour[0]; - poly->Colours[3][1] = layer->Colour[1]; - poly->Colours[3][2] = layer->Colour[2]; + poly->Colours[3][0] = c0; + poly->Colours[3][1] = c1; + poly->Colours[3][2] = c2; poly->Colours[3][3] = alpha; } From d76c2f640a7b741902a5dbe13adf6f5423e89e46 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 17:35:17 +0100 Subject: [PATCH 057/172] 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 c23d422cd79b70b791635f070f17742689f53e57 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 19:38:23 +0100 Subject: [PATCH 058/172] 77.7%: improve DVDErrorTask control flow and indexing Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Platform_G.cpp | 47 ++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 140ed9668..3c3dc1f1a 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -87,7 +87,7 @@ extern int g_discErrorOccured; extern int g_discErrorNumber; extern EAXSound *g_pEAXSound; extern MoviePlayer *gMoviePlayer; -extern const char *s_OpenCover_ErrorText[]; +extern const char *s_OpenCover_ErrorText[][6]; extern const char FEngDiscErrorPackage[]; extern PADStatus HardwarePadStatus[4]; @@ -268,11 +268,15 @@ void CheckReset(int resetMode) { int DVDValidErrorState(int error) { switch (error) { case 5: + return 5; case 4: + return 4; case 6: + return 6; case 11: + return 11; case -1: - return error; + return -1; default: return 0; } @@ -388,15 +392,16 @@ void DVDErrorTask(void *, int) { if (OSGetResetSwitchState()) { resetButtonPressed = 1; } - } - if (num_queued_resets > 0 || resetButtonPressed) { + } else if (num_queued_resets > 0 || resetButtonPressed) { resetMode = 0; } driveStatus = DVDGetDriveStatus(); if (driveStatus != -1 && resetMode != -1) { - CheckReset(resetMode); + int mode = resetMode; + resetMode = -1; + CheckReset(mode); } /* Map drive status to error index */ @@ -427,25 +432,24 @@ void DVDErrorTask(void *, int) { /* New error detected */ language = GC_GetOSLanguage(); g_discErrorNumber = errorState; + g_discErrorOccured = 1; if (gMoviePlayer != 0) { gMoviePlayer->Stop(); } - g_discErrorOccured = 1; SoundPause(true, -1); SetSoundControlState(true, 0x10, "GC Error"); if (g_pEAXSound != 0) { g_pEAXSound->Update(0.1f); } - feng = cFEng::mInstance; pkgName = "DiscError.fng"; - if (!feng->IsPackagePushed(pkgName)) { - feng->PushErrorPackage(pkgName, 0, 0xff); + if (!cFEng::mInstance->IsPackagePushed(pkgName)) { + cFEng::mInstance->PushErrorPackage(pkgName, 0, 0xff); } nextFrame = frame + 1; FEPrintf(pkgName, 0xEEFFD04F, - s_OpenCover_ErrorText[language * 6 + errorIndex]); + s_OpenCover_ErrorText[language][errorIndex]); } else if (g_discErrorOccured == 0) { nextFrame = frame + 1; goto loop_end; @@ -453,8 +457,8 @@ void DVDErrorTask(void *, int) { /* Disc error was active, check if we should service streaming */ nextFrame = frame + 1; - if (TheTrackStreamer.UserMemoryAllocationSize <= 0 && - TheTrackStreamer.LoadingPhase != TrackStreamer::LOADING_IDLE) { + if (!(TheTrackStreamer.UserMemoryAllocationSize > 0) && + (TheTrackStreamer.LoadingPhase != TrackStreamer::LOADING_IDLE)) { ServiceResourceLoading(); driveStatus = 1; TheTrackStreamer.ServiceNonGameState(); @@ -464,7 +468,7 @@ void DVDErrorTask(void *, int) { if (driveStatus != 0) { /* Scrolling text display */ scrollLen = (signed char)bStrLen( - s_OpenCover_ErrorText[language * 6 + errorIndex]); + s_OpenCover_ErrorText[language][errorIndex]); bMemSet(textBuf, 0, 16); @@ -486,7 +490,7 @@ void DVDErrorTask(void *, int) { } bStrNCpy(textBuf, - s_OpenCover_ErrorText[language * 6 + errorIndex], + s_OpenCover_ErrorText[language][errorIndex], scrollLen - scrollOffset); nextFrame = frame + 1; @@ -519,14 +523,13 @@ void DVDErrorTask(void *, int) { gMoviePlayer->Stop(); } - feng = cFEng::mInstance; - feng->MakeLoadedPackagesDirty(); - if (feng->IsPackagePushed("DiscError.fng")) { - feng->PopErrorPackage(); + cFEng::mInstance->MakeLoadedPackagesDirty(); + if (cFEng::mInstance->IsPackagePushed("DiscError.fng")) { + cFEng::mInstance->PopErrorPackage(); } nextFrame = frame + 1; if (movieWasPlaying) { - feng->QueueGameMessage(0xC3960EB9, 0, 0xff); + cFEng::mInstance->QueueGameMessage(0xC3960EB9, 0, 0xff); } } } @@ -609,13 +612,13 @@ VMStatsManager gVMStatsManager_LS("LoadScreen Streamer"); VMStatsManager gVMStatsManager_IG("InGame"); void VMStats::Init() { + mServiceTimeMicroSecs = 0; + mServiceTimeMin = static_cast(-1); + mElapsedTime = 0.0f; mServiceTimeMax = 0; mServiceTimeAvg = 0.0f; - mServiceTimeMin = static_cast(-1); mNumPageFaults = 0; mNumWritebacks = 0; - mElapsedTime = 0.0f; - mServiceTimeMicroSecs = 0; } void VMStatsManager::Init(const char *name) { From 197ff53cfc59e88ad6c65d87fb67e9c3be13f70d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 19:41:21 +0100 Subject: [PATCH 059/172] 77.8%: add IsPlaying guard to StopForce case 9 Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 54 ++++++++++---------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index e655e2efd..775c07ce4 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -233,33 +233,35 @@ void LGWheels::StopForce(long channel, long forceType) { } break; case 9: - LGWheelsGetIsAirborne(this, channel) = 0; - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) == 1) { - PlaySpringForce(channel, LGWheelsGetSpringForceParams(this)[channel].offset, LGWheelsGetSpringForceParams(this)[channel].saturation, LGWheelsGetSpringForceParams(this)[channel].coefficient); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) == 1) { - PlayConstantForce(channel, LGWheelsGetConstantForceParams(this)[channel].magnitude, LGWheelsGetConstantForceParams(this)[channel].direction); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) == 1) { - PlayDamperForce(channel, LGWheelsGetDamperForceParams(this)[channel].coefficient); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) == 1) { - PlayDirtRoadEffect(channel, static_cast(LGWheelsGetDirtRoadParams(this)[channel].magnitude)); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) == 1) { - PlayBumpyRoadEffect(channel, static_cast(LGWheelsGetBumpyRoadParams(this)[channel].magnitude)); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) == 1) { - PlaySlipperyRoadEffect(channel, LGWheelsGetSlipperyRoadParams(this)[channel].magnitude); - } - if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) == 1) { - PlaySurfaceEffect(channel, LGWheelsGetSurfaceEffectParams(this)[channel].type, LGWheelsGetSurfaceEffectParams(this)[channel].magnitude, LGWheelsGetSurfaceEffectParams(this)[channel].period); - } - { - int jj; + if (IsPlaying(channel, 9)) { + LGWheelsGetIsAirborne(this, channel) = 0; + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 0) == 1) { + PlaySpringForce(channel, LGWheelsGetSpringForceParams(this)[channel].offset, LGWheelsGetSpringForceParams(this)[channel].saturation, LGWheelsGetSpringForceParams(this)[channel].coefficient); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 1) == 1) { + PlayConstantForce(channel, LGWheelsGetConstantForceParams(this)[channel].magnitude, LGWheelsGetConstantForceParams(this)[channel].direction); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 2) == 1) { + PlayDamperForce(channel, LGWheelsGetDamperForceParams(this)[channel].coefficient); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 5) == 1) { + PlayDirtRoadEffect(channel, static_cast(LGWheelsGetDirtRoadParams(this)[channel].magnitude)); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 6) == 1) { + PlayBumpyRoadEffect(channel, static_cast(LGWheelsGetBumpyRoadParams(this)[channel].magnitude)); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 7) == 1) { + PlaySlipperyRoadEffect(channel, LGWheelsGetSlipperyRoadParams(this)[channel].magnitude); + } + if (LGWheelsGetWasPlayingBeforeAirborne(this, channel, 8) == 1) { + PlaySurfaceEffect(channel, LGWheelsGetSurfaceEffectParams(this)[channel].type, LGWheelsGetSurfaceEffectParams(this)[channel].magnitude, LGWheelsGetSurfaceEffectParams(this)[channel].period); + } + { + int jj; - for (jj = 0; jj < 10; jj++) { - LGWheelsGetWasPlayingBeforeAirborne(this, channel, jj) = 0; + for (jj = 0; jj < 10; jj++) { + LGWheelsGetWasPlayingBeforeAirborne(this, channel, jj) = 0; + } } } break; From 050206051835fd61bd8940116e69b4d044209f60 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 19:45:06 +0100 Subject: [PATCH 060/172] 78.6%: improve AddSpark ScaleAdd argument order and computation flow Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index 958618820..a5e1560fd 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -41,26 +41,27 @@ void RenderViewPolyEx(eView *view, ePoly *poly, TextureInfo *texture_info, bMatr void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTexture) { if (this->position < 300) { - { - UMath::Vector3 startPos; - UMath::Vector3 endPos; - float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; - float width = static_cast(particle.width) * (1.0f / 2048.0f); - SpriteDef *XSpriteBufferP = &this->XSpriteBuffer[this->position]; - - UMath::ScaleAdd(particle.initialPos, particle.age, particle.vel, startPos); - startPos.z += particle.gravity * particle.age * particle.age; - UMath::ScaleAdd(particle.initialPos, endAge, particle.vel, endPos); - endPos.z += particle.gravity * endAge * endAge; - - XSpriteBufferP->texture_info = CurrentTexture; - XSpriteBufferP->color = - particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; - XSpriteBufferP->width = width; - XSpriteBufferP->startPos = startPos; - XSpriteBufferP->EndPosPos = endPos; - this->position++; - } + UMath::Vector3 startPos; + UMath::Vector3 endPos; + + UMath::ScaleAdd(particle.vel, particle.age, particle.initialPos, startPos); + float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; + startPos.z += particle.gravity * particle.age * particle.age; + + UMath::ScaleAdd(particle.vel, endAge, particle.initialPos, endPos); + + SpriteDef *XSpriteBufferP = &this->XSpriteBuffer[this->position]; + endPos.z += particle.gravity * endAge * endAge; + + XSpriteBufferP->texture_info = CurrentTexture; + XSpriteBufferP->color = + particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; + XSpriteBufferP->startPos = startPos; + XSpriteBufferP->EndPosPos = endPos; + + float width = static_cast(particle.width) * (1.0f / 2048.0f); + XSpriteBufferP->width = width; + this->position++; } } From 830002c38761f3a4d942450899f3cbf2e8fcc222 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 19:53:16 +0100 Subject: [PATCH 061/172] 79.2%: swap push_back condition order and fix AddXenonEffect member stores Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 3071d4c37..522902b3d 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -41,7 +41,12 @@ struct XenonEffectVec { } void push_back(const XenonEffectDef &value) { - if (finish == end_of_storage) { + if (finish != end_of_storage) { + if (finish != 0) { + *finish = value; + } + finish++; + } else { unsigned int size = finish - start; unsigned int old_capacity = end_of_storage - start; unsigned int new_capacity = size + (size == 0 ? 1 : size); @@ -80,11 +85,6 @@ struct XenonEffectVec { start = new_start; finish = dst; end_of_storage = reinterpret_cast(reinterpret_cast(new_start) + new_bytes); - } else { - if (finish != 0) { - *finish = value; - } - finish++; } } }; @@ -381,8 +381,8 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, XenonEffectDef effect; effect.mat = UMath::Matrix4::kIdentity; effect.mat.v3 = mat->v3; - effect.spec = const_cast(spec); effect.piggyback_effect = piggyback_fx; + effect.spec = const_cast(spec); effect.vel = *vel; gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(effect); From d2b1774f90f7c3396fd9461f7e425ce24e4ccbab Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 20:16:26 +0100 Subject: [PATCH 062/172] 80.3%: restructure SpawnParticles velocity computation and color precompute Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 37 ++++++++++--------- .../Indep/Libs/Support/Utility/UVectorMath.h | 3 ++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 522902b3d..fe41186b6 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -221,25 +221,24 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { if (intensity > 0.0f) { UMath::Matrix4 local_world = mLocalWorld; + UMath::Matrix4 rotation_world = local_world; + rotation_world.v3.x = 0.0f; + rotation_world.v3.y = 0.0f; + rotation_world.v3.z = 0.0f; + rotation_world.v3.w = 1.0f; UMath::Vector4 velocity_base; UMath::Vector4 velocity_center; UMath::Vector4 volume_extent; UMath::Vector4 spawn_point; - UMath::Vector4 world_spawn_point; float age = 0.0f; float count = intensity * mEmitterDef.NumParticles(); float life = mEmitterDef.Life(); float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; - float colour_r = mEmitterDef.Colour1().x; - float colour_g = mEmitterDef.Colour1().y; - float colour_b = mEmitterDef.Colour1().z; unsigned int colour_a = static_cast(mEmitterDef.Colour1().w * 255.0f); - - VU0_v4scalexyz(mVel, mEmitterDef.VelocityInherit().x, velocity_base); - UMath::RotateTranslate(mEmitterDef.VolumeCenter(), local_world, velocity_center); - velocity_base.x += velocity_center.x; - velocity_base.y += velocity_center.y; - velocity_base.z += velocity_center.z; + int colour_r = static_cast(mEmitterDef.Colour1().x * 255.0f); + int colour_g = static_cast(mEmitterDef.Colour1().y * 255.0f); + int colour_b = static_cast(mEmitterDef.Colour1().z * 255.0f); + unsigned int precomputed_color = colour_a << 24 | colour_b << 16 | colour_g << 8 | colour_r; if (count_after_variance != 0.0f) { float particle_step = dt / count_after_variance; @@ -270,25 +269,27 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { volume_extent.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &seed) * 2.0f); volume_extent.w = 1.0f; + VU0_v4scalexyz(mEmitterDef.VelocityInherit(), mVel, velocity_base); + VU0_MATRIX3x4_vect4mult(mEmitterDef.VolumeCenter(), mLocalWorld, velocity_center); + VU0_v4add(velocity_base, velocity_center, velocity_base); + VU0_v4scalexyz(velocity_base, volume_extent, velocity_base); + spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeExtent().x, &seed) - mEmitterDef.VolumeExtent().x * 0.5f); spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeExtent().y, &seed) - mEmitterDef.VolumeExtent().y * 0.5f); spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeExtent().z, &seed) - mEmitterDef.VolumeExtent().z * 0.5f); spawn_point.w = 1.0f; - UMath::RotateTranslate(spawn_point, local_world, world_spawn_point); - VU0_v4scalexyz(velocity_base, volume_extent.x, velocity_center); - VU0_v3scaleadd(UMath::Vector4To3(velocity_center), age, UMath::Vector4To3(world_spawn_point), + UMath::RotateTranslate(spawn_point, local_world, spawn_point); + VU0_v3scaleadd(UMath::Vector4To3(velocity_base), age, UMath::Vector4To3(spawn_point), *reinterpret_cast(particle)); gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &seed) * 2.0f; - particle->initialPos = UMath::Vector4To3(world_spawn_point); - particle->vel = UMath::Vector4To3(velocity_center); + particle->initialPos = UMath::Vector4To3(spawn_point); + particle->vel = UMath::Vector4To3(velocity_base); particle->age = age; particle->gravity = gravity; particle->life = static_cast((life - life * mEmitterDef.LifeVariance()) * 65535.0f); - particle->color = - colour_a << 24 | static_cast(colour_b * 255.0f) << 16 | static_cast(colour_g * 255.0f) << 8 | - static_cast(colour_r * 255.0f); + particle->color = precomputed_color; particle->length = static_cast(length_clamped * 255.0f); particle->width = static_cast(mEmitterDef.HeightStart()); diff --git a/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h b/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h index ae44b2d95..c80663d67 100644 --- a/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h +++ b/src/Speed/Indep/Libs/Support/Utility/UVectorMath.h @@ -39,8 +39,11 @@ void VU0_v4subxyz(const UMath::Vector4 &a, const UMath::Vector4 &b, UMath::Vecto float VU0_v4dotprodxyz(const UMath::Vector4 &a, const UMath::Vector4 &b); void VU0_v4scale(const UMath::Vector4 &a, const float scaleby, UMath::Vector4 &result); void VU0_v4scalexyz(const UMath::Vector4 &a, const float scaleby, UMath::Vector4 &result); +void VU0_v4scalexyz(const UMath::Vector4 &a, const UMath::Vector4 &b, UMath::Vector4 &result); +void VU0_v4add(const UMath::Vector4 &a, const UMath::Vector4 &b, UMath::Vector4 &result); float VU0_v4distancesquarexyz(const UMath::Vector4 &p1, const UMath::Vector4 &p2); void VU0_MATRIX3x4_vect3mult(const UMath::Vector3 &v, const UMath::Matrix4 &m, UMath::Vector3 &result); +void VU0_MATRIX3x4_vect4mult(const UMath::Vector4 &v, const UMath::Matrix4 &m, UMath::Vector4 &result); void VU0_qmul(const UMath::Vector4 &b, const UMath::Vector4 &a, UMath::Vector4 &dest); void VU0_v3quatrotate(const UMath::Vector4 &q, const UMath::Vector3 &v, UMath::Vector3 &result); From 1a17263ab518aa677774c39798c6671c734f08ba Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 20:19:32 +0100 Subject: [PATCH 063/172] 80.8%: fix SpawnParticles colour casts to int and reorder computation Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index fe41186b6..26c50bcb6 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -231,13 +231,13 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { UMath::Vector4 volume_extent; UMath::Vector4 spawn_point; float age = 0.0f; - float count = intensity * mEmitterDef.NumParticles(); - float life = mEmitterDef.Life(); - float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; - unsigned int colour_a = static_cast(mEmitterDef.Colour1().w * 255.0f); int colour_r = static_cast(mEmitterDef.Colour1().x * 255.0f); int colour_g = static_cast(mEmitterDef.Colour1().y * 255.0f); int colour_b = static_cast(mEmitterDef.Colour1().z * 255.0f); + int colour_a = static_cast(mEmitterDef.Colour1().w * 255.0f); + float count = intensity * mEmitterDef.NumParticles(); + float life = mEmitterDef.Life(); + float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; unsigned int precomputed_color = colour_a << 24 | colour_b << 16 | colour_g << 8 | colour_r; if (count_after_variance != 0.0f) { From 0f5264ca8e5b3ef3f892573cd1ee9e97181a21b7 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 20:28:16 +0100 Subject: [PATCH 064/172] 81.0%: fix RenderAll ePoly constructor call and unsigned loop compare Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 4 ++-- src/Speed/Indep/Src/Ecstasy/Ecstasy.hpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index a5e1560fd..c182969b5 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -70,10 +70,10 @@ void XSpriteManager::RenderAll(eView *view) { SpriteDef *XSpriteBufferP = this->XSpriteBuffer; { - int i; + uint32 i; bMatrix4 *identity = eGetIdentityMatrix(); - for (i = 0; i < static_cast(this->position); i++) { + for (i = 0; i < this->position; i++) { pPoly.Vertices[0] = XSpriteBufferP->startPos; pPoly.Vertices[1] = XSpriteBufferP->startPos; pPoly.Vertices[1].z += XSpriteBufferP->width; diff --git a/src/Speed/Indep/Src/Ecstasy/Ecstasy.hpp b/src/Speed/Indep/Src/Ecstasy/Ecstasy.hpp index fc73ad0fc..e8a4bc71b 100644 --- a/src/Speed/Indep/Src/Ecstasy/Ecstasy.hpp +++ b/src/Speed/Indep/Src/Ecstasy/Ecstasy.hpp @@ -168,6 +168,8 @@ struct ePoly { unsigned char flags; // offset 0x90, size 0x1 unsigned char Flailer; // offset 0x91, size 0x1 + ePoly(); + void *operator new(size_t size) {} void operator delete(void *ptr) {} From 7981cccf5682fdd9b2af58ff44c986ed9b158367 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 20:32:00 +0100 Subject: [PATCH 065/172] 81.3%: SpawnParticles gravity order, remove initialPos overwrite, precompute life Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 26c50bcb6..1ecc09899 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -238,6 +238,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { float count = intensity * mEmitterDef.NumParticles(); float life = mEmitterDef.Life(); float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; + float life_factor = life - life * mEmitterDef.LifeVariance(); unsigned int precomputed_color = colour_a << 24 | colour_b << 16 | colour_g << 8 | colour_r; if (count_after_variance != 0.0f) { @@ -274,6 +275,8 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { VU0_v4add(velocity_base, velocity_center, velocity_base); VU0_v4scalexyz(velocity_base, volume_extent, velocity_base); + gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &seed) * 2.0f; + spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeExtent().x, &seed) - mEmitterDef.VolumeExtent().x * 0.5f); spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeExtent().y, &seed) - mEmitterDef.VolumeExtent().y * 0.5f); spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeExtent().z, &seed) - mEmitterDef.VolumeExtent().z * 0.5f); @@ -281,14 +284,13 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { UMath::RotateTranslate(spawn_point, local_world, spawn_point); VU0_v3scaleadd(UMath::Vector4To3(velocity_base), age, UMath::Vector4To3(spawn_point), - *reinterpret_cast(particle)); + particle->initialPos); - gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &seed) * 2.0f; - particle->initialPos = UMath::Vector4To3(spawn_point); + particle->initialPos.z += gravity * age * age; particle->vel = UMath::Vector4To3(velocity_base); particle->age = age; particle->gravity = gravity; - particle->life = static_cast((life - life * mEmitterDef.LifeVariance()) * 65535.0f); + particle->life = static_cast(life_factor * 65535.0f); particle->color = precomputed_color; particle->length = static_cast(length_clamped * 255.0f); particle->width = static_cast(mEmitterDef.HeightStart()); From 1e480014ad6cc4d4f71a62190e6b7c150c3c8959 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Wed, 18 Mar 2026 20:58:22 +0100 Subject: [PATCH 066/172] 81.6%: fix 2D array access in LGWheels/Periodic force helpers Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 6 ++++-- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 775c07ce4..7dcf4234a 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -90,11 +90,13 @@ static inline unsigned char &LGWheelsGetOverallGain(LGWheels *self) { } static inline int &LGWheelsGetPlaying(Force *self, int channel, int forceNumber) { - return reinterpret_cast(self)[channel * 8 + forceNumber]; + typedef int Row[8]; + return reinterpret_cast(self)[channel][forceNumber]; } static inline unsigned long &LGWheelsGetEffectID(Force *self, int channel, int forceNumber) { - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; + typedef unsigned long Row[8]; + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; } static inline SpringForceParams *LGWheelsGetSpringForceParams(LGWheels *self) { diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index 28c1d1063..dddf0e0e8 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -13,7 +13,8 @@ static const char kDownloadPeriodicForceInvalidWheel[] = "ERROR: Trying to downl static const char kUpdatePeriodicForceError[] = "ERROR: UpdateForce(periodic force) on channel %d returned %d\n"; static inline unsigned long &PeriodicGetEffectID(Force *self, int channel, int forceNumber) { - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; + typedef unsigned long Row[8]; + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; } Periodic::Periodic() : Force() {} From 3cecdf47f6d8273e26b15e73dc03e03f922044d5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 08:19:50 +0100 Subject: [PATCH 067/172] 82.5%: rewrite ConvertPadButtons to use bit manipulation and fix ClampAnalogValue Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/JoyE.cpp | 50 +++++++++------------------------ 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 3173f7ab9..583e514e5 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -43,47 +43,25 @@ float lastCalibTime[4]; int JoystickInitialized; static inline unsigned short ConvertPadButtons(unsigned short buttons) { - unsigned short converted = 0xFFFF; - - if (buttons & PAD_BUTTON_A) { - converted &= ~0x0001; - } - if (buttons & PAD_BUTTON_B) { - converted &= ~0x0002; - } - if (buttons & PAD_BUTTON_X) { - converted &= ~0x0004; - } - if (buttons & PAD_BUTTON_Y) { - converted &= ~0x0008; - } - if (buttons & PAD_TRIGGER_Z) { - converted &= ~0x0010; - } - if (buttons & PAD_BUTTON_START) { - converted &= ~0x0020; - } - if (buttons & PAD_BUTTON_UP) { - converted &= ~0x0100; - } - if (buttons & PAD_BUTTON_DOWN) { - converted &= ~0x0200; - } - if (buttons & PAD_BUTTON_LEFT) { - converted &= ~0x0400; - } - if (buttons & PAD_BUTTON_RIGHT) { - converted &= ~0x0800; - } - - return converted; + unsigned short result; + result = (buttons >> 8) & 1; + result |= (buttons >> 8) & 2; + result |= (buttons >> 8) & 4; + result |= (buttons >> 8) & 8; + result |= buttons & 0x10; + result |= (buttons >> 7) & 0x20; + result |= (buttons << 5) & 0x100; + result |= (buttons << 7) & 0x200; + result |= (buttons & 1) << 10; + result |= (buttons << 10) & 0x800; + return ~result; } static inline unsigned char ClampAnalogValue(int value) { - if (value < 0) { + if (value & 0x8000) { return 0; } - if (value > 0xFF) { + if (static_cast(value) > 0xFF) { return 0xFF; } return value; From 6ce32b55c5586d38ffb773acff34eddb81fcf271 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 08:44:06 +0100 Subject: [PATCH 068/172] 82.6%: use OS_BUS_CLOCK macro in DVDErrorTask, fix eInitSunPat store order Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Platform_G.cpp | 2 +- src/Speed/GameCube/Src/Render/SunE.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 3c3dc1f1a..58337253b 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -329,7 +329,7 @@ void DVDErrorTask(void *, int) { softwareResetCheckStarted = 1; } else { u32 currentTick = OSGetTick(); - u32 ticksPerMs = *(volatile u32 *)0x800000F8 / 4000; + u32 ticksPerMs = OS_BUS_CLOCK / 4000; u32 elapsed = currentTick - softwareResetStartTick; u32 msElapsed = elapsed / ticksPerMs; if (msElapsed > 500) { diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 430545375..8932b4405 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -325,9 +325,9 @@ void eInitSunPat() { vis_layer_fix.Texture = SUNTEX_CENTER; vis_layer_fix.IntensityScale = 32.0f; vis_layer_fix.Size = 1.0f; - vis_layer_fix.Angle = 0; + vis_layer_fix.SweepAngleAmount = 0.0f; vis_layer_fix.OffsetX = 0.0f; vis_layer_fix.OffsetY = 0.0f; - vis_layer_fix.SweepAngleAmount = 0.0f; + vis_layer_fix.Angle = 0; eBuildSunPolyFix(&sun_vis_poly_fix, &vis_layer_fix, 1.0f, 0.0f, 0.0f); } From 3991375c415ed16efe25fb6492ca59d2d1533d28 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 08:45:50 +0100 Subject: [PATCH 069/172] 82.8%: 100% match eRenderSun - fix SunInfo caching, condition order, ePoly ctor Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Render/SunE.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 8932b4405..a50cb9562 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -255,10 +255,11 @@ void eCalcSunVisibility(eView *view, float x, float y) { } void eRenderSun(eView *view) { + SunChunkInfo *sun_info = SunInfo; + SetCurrentSunInfo(); if (IsGameFlowInGame()) { - SunChunkInfo *sun_info = SunInfo; Camera *camera = view->GetCamera(); bMatrix4 *world_view = camera->GetCameraMatrix(); bVector4 position3d; @@ -280,12 +281,11 @@ void eRenderSun(eView *view) { screen_widthf = static_cast(eGetScreenWidth()); screen_heightf = static_cast(eGetScreenHeight()); + x = position2d.x; + y = position2d.y; if (SunPosX != 0.0f || SunPosY != 0.0f) { x = SunPosX; y = SunPosY; - } else { - x = position2d.x; - y = position2d.y; } max_size = 0.0f; @@ -293,12 +293,12 @@ void eRenderSun(eView *view) { for (int i = 0; i < 4; i++) { SunLayer *layer = &sun_info->SunLayers[i]; - if (0.0f < layer->IntensityScale && layer->Texture == SUNTEX_CENTER && max_size < layer->Size) { + if (layer->IntensityScale > 0.0f && layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { max_size = layer->Size; } } - if (0.0f <= view3d.z && -max_size <= x && x <= screen_widthf + max_size && -max_size <= y && y <= screen_heightf + max_size) { + if (view3d.z >= 0.0f && x >= -max_size && x <= screen_widthf + max_size && y >= -max_size && y <= screen_heightf + max_size) { eRecalculateOthographicProjection(1, 100000.0f); eSetOrthographicMatrixToHW(); eCalcSunVisibility(eGetView(0, false), x, y); @@ -312,7 +312,6 @@ void eRenderSun(eView *view) { if (texture_info) { ePoly sun_poly; - ConstructePoly(&sun_poly); eBuildSunPoly(&sun_poly, layer, max_size, x, y); RenderViewPoly(view, &sun_poly, texture_info, 0); } From 86df37f2f0ab0749e5ac2bde395b71ac1cc8286f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 08:54:44 +0100 Subject: [PATCH 070/172] 82.9%: 100% match NGEffect constructor, use IsValid() for inline field access Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/xSparks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 1ecc09899..1b495755f 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -305,7 +305,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { NGEffect::NGEffect(const XenonEffectDef &eDef) : mEffectDef(eDef.spec, 0, nullptr) { - if (mEffectDef.GetCollection() != 0) { + if (mEffectDef.IsValid()) { int i = 0; int length = mEffectDef.Num_NGEmitter(); while (i < length) { From c907e7a2a62ea454dbac75f3ee144d663ba106e7 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 08:59:00 +0100 Subject: [PATCH 071/172] 83.0%: 100% match AddSpark and NGEffect, fix int cast for xoris pattern Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index c182969b5..b619c4a29 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -45,8 +45,8 @@ void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTe UMath::Vector3 endPos; UMath::ScaleAdd(particle.vel, particle.age, particle.initialPos, startPos); - float endAge = static_cast(particle.length) * (1.0f / 2048.0f) + particle.age; startPos.z += particle.gravity * particle.age * particle.age; + float endAge = static_cast(static_cast(particle.length)) * (1.0f / 2048.0f) + particle.age; UMath::ScaleAdd(particle.vel, endAge, particle.initialPos, endPos); @@ -59,7 +59,7 @@ void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTe XSpriteBufferP->startPos = startPos; XSpriteBufferP->EndPosPos = endPos; - float width = static_cast(particle.width) * (1.0f / 2048.0f); + float width = static_cast(static_cast(particle.width)) * (1.0f / 2048.0f); XSpriteBufferP->width = width; this->position++; } From 1a54bf300cac21f922f60417ef1928a37f49e777 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:03:54 +0100 Subject: [PATCH 072/172] 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 7629e9ef8ea4b4ea21ce5ee6a31900c028893465 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:28:28 +0100 Subject: [PATCH 073/172] 83.3%: fix kFloatScale init order by including UVectorMath.h in Platform_G.cpp Moves kFloatScaleUp/kFloatScaleDown initialization before VMStatsManager constructors in __static_init, matching original binary order. Also reorder VMStats::Init stores to better match original. Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Platform_G.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 58337253b..1944ebd1e 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -1,3 +1,4 @@ +#include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" #include "Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp" #include "Speed/Indep/Src/Misc/BuildRegion.hpp" #include "Speed/Indep/Src/Misc/GameFlow.hpp" @@ -612,13 +613,13 @@ VMStatsManager gVMStatsManager_LS("LoadScreen Streamer"); VMStatsManager gVMStatsManager_IG("InGame"); void VMStats::Init() { - mServiceTimeMicroSecs = 0; - mServiceTimeMin = static_cast(-1); - mElapsedTime = 0.0f; mServiceTimeMax = 0; mServiceTimeAvg = 0.0f; + mServiceTimeMin = static_cast(-1); mNumPageFaults = 0; mNumWritebacks = 0; + mElapsedTime = 0.0f; + mServiceTimeMicroSecs = 0; } void VMStatsManager::Init(const char *name) { From fc2f05f5fadee72880d5f9b82730c89ebf182793 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:42:54 +0100 Subject: [PATCH 074/172] 83.4%: improve RenderAll layout and force EffectID 2D array accessors - Reorder XSpriteManager::RenderAll vertex/colour stores for better match - Change Condition and Constant EffectID accessors to 2D array typedef to match original's distributed multiplication pattern Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 7 ++++--- src/Speed/GameCube/Src/Logitech/Condition.cpp | 3 ++- src/Speed/GameCube/Src/Logitech/Constant.cpp | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index b619c4a29..d881d9d82 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -76,15 +76,16 @@ void XSpriteManager::RenderAll(eView *view) { for (i = 0; i < this->position; i++) { pPoly.Vertices[0] = XSpriteBufferP->startPos; pPoly.Vertices[1] = XSpriteBufferP->startPos; - pPoly.Vertices[1].z += XSpriteBufferP->width; pPoly.Vertices[2] = XSpriteBufferP->EndPosPos; - pPoly.Vertices[2].z += XSpriteBufferP->width; pPoly.Vertices[3] = XSpriteBufferP->EndPosPos; + *reinterpret_cast(pPoly.Colours[3]) = XSpriteBufferP->color; *reinterpret_cast(pPoly.Colours[0]) = XSpriteBufferP->color; *reinterpret_cast(pPoly.Colours[1]) = XSpriteBufferP->color; *reinterpret_cast(pPoly.Colours[2]) = XSpriteBufferP->color; - *reinterpret_cast(pPoly.Colours[3]) = XSpriteBufferP->color; + + pPoly.Vertices[1].z += XSpriteBufferP->width; + pPoly.Vertices[2].z += XSpriteBufferP->width; RenderViewPolyEx(view, &pPoly, XSpriteBufferP->texture_info, identity, 0, 0.0f); XSpriteBufferP++; diff --git a/src/Speed/GameCube/Src/Logitech/Condition.cpp b/src/Speed/GameCube/Src/Logitech/Condition.cpp index 5069675a4..1f5ed49f5 100644 --- a/src/Speed/GameCube/Src/Logitech/Condition.cpp +++ b/src/Speed/GameCube/Src/Logitech/Condition.cpp @@ -13,7 +13,8 @@ static const char kDownloadConditionForceInvalidWheel[] = "ERROR: Trying to down static const char kUpdateConditionForceError[] = "ERROR: UpdateForce(condition force) on channel %d returned %d\n"; static inline unsigned long &ConditionGetEffectID(Force *self, int channel, int forceNumber) { - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; + typedef unsigned long Row[8]; + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; } Condition::Condition() : Force() {} diff --git a/src/Speed/GameCube/Src/Logitech/Constant.cpp b/src/Speed/GameCube/Src/Logitech/Constant.cpp index 685b7e1d5..a2fa3eab7 100644 --- a/src/Speed/GameCube/Src/Logitech/Constant.cpp +++ b/src/Speed/GameCube/Src/Logitech/Constant.cpp @@ -13,7 +13,8 @@ static const char kDownloadConstantForceInvalidWheel[] = "ERROR: Trying to downl static const char kUpdateConstantForceError[] = "ERROR: UpdateForce(constant force) on channel %d returned %d\n"; static inline unsigned long &ConstantGetEffectID(Force *self, int channel, int forceNumber) { - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel * 8 + forceNumber]; + typedef unsigned long Row[8]; + return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; } Constant::Constant() : Force() {} From 1b8ec13c1bf0453e18f17d12875cc9b1cd599d24 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 09:51:52 +0100 Subject: [PATCH 075/172] 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 c3d7ab9ff546ce4862d6ddf31be36900d5c3b436 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 16:33:15 +0100 Subject: [PATCH 076/172] commit semantics --- AGENTS.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fc4f7af24..7b4321485 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -354,25 +354,35 @@ This is a **C++98** codebase compiled with ProDG GC 3.9.3 (GCC 2.95 under the ho ## Committing Progress -After each meaningful percentage-point improvement in objdiff match score, commit your changes. Check the current unit match percentage with: +After each meaningful improvement in objdiff match score or DWARF progress, commit your changes. Check the current unit match percentage with: ```sh 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: +Commit whenever the match percentage increases or you achieve a milestone (e.g. you matched a new function or improved an existing one). Use this format for the commit message: ``` -n.n[n]%: short description of what was matched or changed +n.n[n]%: [action] [Subject]::[Function] ``` +- **match+**: used when a function or object achieves 100% byte-match status AND 100% DWARF match. +- **match**: used when a function achieves 100% byte-match status but DWARF is still missing/mismatched. +- **improve**: used when the instruction match percentage increases. +- **dwarf match**: used when the normalized DWARF achieves 100% match. +- **dwarf improve**: used when DWARF issues are resolved but it's not yet 100% DWARF-matched. + Examples: -- `42.1%: match UpdateCamera` -- `78.56%: match PlayerController constructor and destructor` +- `42.1%: match+ UpdateCamera` +- `76.7%: match+ TrackStreamer::HibernateStreamingSections` +- `76.7%: match TrackStreamer::WillUnloadBlock` +- `76.5%: dwarf match TrackStreamer::HandleLoading` +- `76.5%: improve TrackStreamer::LoadSection` +- `76.5%: dwarf improve TrackStreamer::CountUserAllocations` - `100.0%: full match for zAnim` -Do not batch up multiple percentage milestones into one commit — commit as each improvement lands. +Do not batch up multiple unrelated improvements into one commit — commit as each logical piece of work lands. ## Parallel Sub-Agent Matching From e5ef59c2dccf00f9849218dac2e5f8e796ce361c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 17:49:59 +0100 Subject: [PATCH 077/172] skill --- .github/skills/code_style/SKILL.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/skills/code_style/SKILL.md b/.github/skills/code_style/SKILL.md index d5b2cbbb3..28db08c3c 100644 --- a/.github/skills/code_style/SKILL.md +++ b/.github/skills/code_style/SKILL.md @@ -19,6 +19,15 @@ In this repo, style cleanup must preserve decomp progress. - If a style tweak changes codegen or match status, revert it. - Extend this skill only from patterns you actually verified in the repo. +### Authenticity Over Hacks + +A 100% match is the goal, but **how** we get there matters just as much. + +- Do not use "any means necessary" to force a match if it results in unreadable or unnatural code. +- Always think about what the original code probably looked like and write it that way. +- Even if a function matches 100% binary-wise, it is not "correct" if the source is unreadable or contains logic that no human developer would have written. +- If you find a stubborn mismatch, look for a more natural C++ expression or a different architectural pattern instead of resorting to opaque hacks. + ## Quick Tooling Use the repo-local helper before doing a style pass: From 4611a9872abcc74082acdd6637b0b155d0f4632e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 19:58:08 +0100 Subject: [PATCH 078/172] 83.7%: improve eUpdateSunPolyFix, Wheels::IsConnected Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/Wheels.cpp | 2 +- src/Speed/GameCube/Src/Render/SunE.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index 83a6bc064..d5839ed71 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -68,7 +68,7 @@ bool Wheels::ButtonIsPressed(long channel, unsigned long buttonMask) { bool Wheels::IsConnected(long channel) { const LGPosition *position = reinterpret_cast(this); - return position[channel].err; + return !position[channel].err; } bool Wheels::PedalsConnected(long channel) { diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index a50cb9562..611dc70d6 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -206,10 +206,10 @@ void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, fl float intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; unsigned char alpha; - if (intensity >= 28.0f) { - alpha = static_cast(static_cast(intensity - 28.0f)); - } else { + if (intensity < 28.0) { alpha = static_cast(static_cast(intensity)); + } else { + alpha = static_cast(static_cast(intensity - 28.0)); } poly->Vertices[0].x = sun_vis_poly_fix_ini[0] + x; From db330cde66518457c35b35835973c6416fedad1d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Thu, 19 Mar 2026 20:05:04 +0100 Subject: [PATCH 079/172] 83.9%: improve LGWheels inline accessors Use flat index pattern for LGWheelsGetPlaying/EffectID. Improves IsPlaying tail merging and Play* functions. Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 7dcf4234a..567193e49 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -90,13 +90,11 @@ static inline unsigned char &LGWheelsGetOverallGain(LGWheels *self) { } static inline int &LGWheelsGetPlaying(Force *self, int channel, int forceNumber) { - typedef int Row[8]; - return reinterpret_cast(self)[channel][forceNumber]; + return *(reinterpret_cast(self) + forceNumber + channel * 8); } static inline unsigned long &LGWheelsGetEffectID(Force *self, int channel, int forceNumber) { - typedef unsigned long Row[8]; - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; + return *(reinterpret_cast(reinterpret_cast(self) + 0x80) + forceNumber + channel * 8); } static inline SpringForceParams *LGWheelsGetSpringForceParams(LGWheels *self) { From 9ddb9e0c58166de02560bc15108b34221a080ef1 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 00:46:58 +0100 Subject: [PATCH 080/172] 83.7%: improve Periodic/Constant/Condition EffectID accessors Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Logitech/Condition.cpp | 4 ++-- src/Speed/GameCube/Src/Logitech/Constant.cpp | 4 ++-- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Condition.cpp b/src/Speed/GameCube/Src/Logitech/Condition.cpp index 1f5ed49f5..d569238c1 100644 --- a/src/Speed/GameCube/Src/Logitech/Condition.cpp +++ b/src/Speed/GameCube/Src/Logitech/Condition.cpp @@ -13,8 +13,8 @@ static const char kDownloadConditionForceInvalidWheel[] = "ERROR: Trying to down static const char kUpdateConditionForceError[] = "ERROR: UpdateForce(condition force) on channel %d returned %d\n"; static inline unsigned long &ConditionGetEffectID(Force *self, int channel, int forceNumber) { - typedef unsigned long Row[8]; - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; + char *base = reinterpret_cast(self) + 0x80; + return *reinterpret_cast(base + channel * 32 + forceNumber * 4); } Condition::Condition() : Force() {} diff --git a/src/Speed/GameCube/Src/Logitech/Constant.cpp b/src/Speed/GameCube/Src/Logitech/Constant.cpp index a2fa3eab7..2688b8cab 100644 --- a/src/Speed/GameCube/Src/Logitech/Constant.cpp +++ b/src/Speed/GameCube/Src/Logitech/Constant.cpp @@ -13,8 +13,8 @@ static const char kDownloadConstantForceInvalidWheel[] = "ERROR: Trying to downl static const char kUpdateConstantForceError[] = "ERROR: UpdateForce(constant force) on channel %d returned %d\n"; static inline unsigned long &ConstantGetEffectID(Force *self, int channel, int forceNumber) { - typedef unsigned long Row[8]; - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; + char *base = reinterpret_cast(self) + 0x80; + return *reinterpret_cast(base + channel * 32 + forceNumber * 4); } Constant::Constant() : Force() {} diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index dddf0e0e8..567aa0acb 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -13,8 +13,8 @@ static const char kDownloadPeriodicForceInvalidWheel[] = "ERROR: Trying to downl static const char kUpdatePeriodicForceError[] = "ERROR: UpdateForce(periodic force) on channel %d returned %d\n"; static inline unsigned long &PeriodicGetEffectID(Force *self, int channel, int forceNumber) { - typedef unsigned long Row[8]; - return reinterpret_cast(reinterpret_cast(self) + 0x80)[channel][forceNumber]; + char *base = reinterpret_cast(self) + 0x80; + return *reinterpret_cast(base + channel * 32 + forceNumber * 4); } Periodic::Periodic() : Force() {} From 097c1a71f787c4f0735f800740eaa5cb325bfba9 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 00:55:43 +0100 Subject: [PATCH 081/172] 84.0%: match MemoryCardImp::BootupCheckDone Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index 08a7b5df8..d10d1adb7 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -142,13 +142,15 @@ void MemoryCardImp::BootupCheckDone(RealmcIface::CardStatus status, RealmcIface: MemoryCard *memcard = GetMemcard(); if (*reinterpret_cast(reinterpret_cast(memcard) + 0x30)) { - if (status == RealmcIface::STATUS_NO_CARD || status == RealmcIface::STATUS_CARD_DAMAGED || - status == RealmcIface::STATUS_WRONG_DEVICE || status == RealmcIface::STATUS_CARD_FULL) { + if (status == RealmcIface::STATUS_CARD_DAMAGED || status == RealmcIface::STATUS_WRONG_DEVICE || + status == RealmcIface::STATUS_CARD_FULL || status == RealmcIface::STATUS_NO_CARD) { + cFEng *fe = cFEng::mInstance; void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); - cFEng::mInstance->QueuePackageMessage(0x8867412D, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); + fe->QueuePackageMessage(0x8867412D, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); } else { + cFEng *fe = cFEng::mInstance; void *screen = *reinterpret_cast(reinterpret_cast(GetMemcard()) + 0x190); - cFEng::mInstance->QueuePackageMessage(0x3A2BE557, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); + fe->QueuePackageMessage(0x3A2BE557, *reinterpret_cast(reinterpret_cast(screen) + 0xC), 0); } } } From 3e691853c5aee42bf39573f423a6e9ab13c846bd Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 00:59:00 +0100 Subject: [PATCH 082/172] 84.0%: improve TextureInfoPlatInfo::SetImage power-of-2 check Co-Authored-By: Claude Opus 4.6 --- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index af31c1b45..fbf819c1f 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -142,7 +142,7 @@ unsigned char TextureInfoPlatInfo::SetImage(int width, int height, int mip, int GXTlutFmt tlut_format = static_cast(format >= 0 ? GX_TL_RGB5A3 : GX_TL_IA8); if (clamp & 1) { - int width_lsb = width & -width; + int width_lsb = width & (~width + 1); if (width == width_lsb) { wrap_s = 1; @@ -150,7 +150,7 @@ unsigned char TextureInfoPlatInfo::SetImage(int width, int height, int mip, int } if (clamp & 2) { - int height_lsb = height & -height; + int height_lsb = height & (~height + 1); if (height == height_lsb) { wrap_t = 1; From 0bab6b8da55355215734602b5ec9057edf02f82f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 16:22:06 +0100 Subject: [PATCH 083/172] 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 084/172] 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 085/172] 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 086/172] 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 5434d2a8c53ee681138377bf0d1c620fcffdbf45 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 21:17:04 +0100 Subject: [PATCH 087/172] 84.4%: improve LGWheels::InitVars Restore C linkage for bDoWithStack and byte-match LGWheels::InitVars while pushing zPlatform forward. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 39 ++++++++++++++------ src/Speed/GameCube/Src/Platform_G.cpp | 2 +- src/Speed/Indep/Src/Misc/Platform.h | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 567193e49..d40f0e8b0 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -148,20 +148,35 @@ LGWheels::LGWheels() { void LGWheels::InitVars(long channel) { int ii; - - LGWheelsGetIsAirborne(this, channel) = 0; - LGWheelsGetDamperWasPlaying(this, channel) = 0; - LGWheelsGetSpringWasPlaying(this, channel) = 0; + int channelOffset = channel * 4; + int forceOffset = channel * 0x20; + char *isAirborne = reinterpret_cast(this) + 0x166C; + char *damperWasPlaying = reinterpret_cast(this) + 0x15AC; + char *springWasPlaying = reinterpret_cast(this) + 0x15BC; + char *conditionEffectID = reinterpret_cast(this) + 0x1228; + char *conditionPlaying = reinterpret_cast(this) + 0x11A8; + char *constantEffectID = reinterpret_cast(this) + 0x1328; + char *constantPlaying = reinterpret_cast(this) + 0x12A8; + char *periodicEffectID = reinterpret_cast(this) + 0x1428; + char *periodicPlaying = reinterpret_cast(this) + 0x13A8; + char *rampEffectID = reinterpret_cast(this) + 0x1528; + char *rampPlaying = reinterpret_cast(this) + 0x14A8; + + *reinterpret_cast(isAirborne + channelOffset) = 0; + *reinterpret_cast(damperWasPlaying + channelOffset) = 0; + *reinterpret_cast(springWasPlaying + channelOffset) = 0; for (ii = 0; ii < 8; ii++) { - LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, ii) = static_cast(-1); - LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, ii) = 0; - LGWheelsGetEffectID(LGWheelsGetConstant(this), channel, ii) = static_cast(-1); - LGWheelsGetPlaying(LGWheelsGetConstant(this), channel, ii) = 0; - LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, ii) = static_cast(-1); - LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, ii) = 0; - LGWheelsGetEffectID(LGWheelsGetRamp(this), channel, ii) = static_cast(-1); - LGWheelsGetPlaying(LGWheelsGetRamp(this), channel, ii) = 0; + int offset = ii * 4 + forceOffset; + + *reinterpret_cast(conditionEffectID + offset) = static_cast(-1); + *reinterpret_cast(conditionPlaying + offset) = 0; + *reinterpret_cast(constantEffectID + offset) = static_cast(-1); + *reinterpret_cast(constantPlaying + offset) = 0; + *reinterpret_cast(periodicEffectID + offset) = static_cast(-1); + *reinterpret_cast(periodicPlaying + offset) = 0; + *reinterpret_cast(rampEffectID + offset) = static_cast(-1); + *reinterpret_cast(rampPlaying + offset) = 0; } } diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 1944ebd1e..99bf2d6d8 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -237,7 +237,7 @@ void InitDisplaySystem() { void FinishedRenderingFEngLayer() {} -int bDoWithStack(void *function, void *stack_pointer, int arg1, int arg2) { +extern "C" int bDoWithStack(void *function, void *stack_pointer, int arg1, int arg2) { return 0; } diff --git a/src/Speed/Indep/Src/Misc/Platform.h b/src/Speed/Indep/Src/Misc/Platform.h index 727f618b7..c70749ed3 100644 --- a/src/Speed/Indep/Src/Misc/Platform.h +++ b/src/Speed/Indep/Src/Misc/Platform.h @@ -10,7 +10,7 @@ void InitPlatform(); void InitDisplaySystem(); void ServicePlatform(); -int bDoWithStack(void *function, void *stack_pointer, int arg1, int arg2); +extern "C" int bDoWithStack(void *function, void *stack_pointer, int arg1, int arg2); void EnableInterrupts(); void DVDErrorTask(void *, int); From 15977383b6e03724c02661891e325542ad4ad56d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 21:39:56 +0100 Subject: [PATCH 088/172] 84.6%: improve snProfilerEnable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 99bf2d6d8..d4e7c5a09 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -156,7 +156,7 @@ extern EA::Allocator::IAllocator &gMemoryAllocator; void *arenaLo; unsigned int g_GC_Disk_GameName; -int snProfilerEnable; +int snProfilerEnable = 0; void InitPlatform() { static char profdata[0x2000]; From 3eeb171e1aea6349834701455f4d3b3e4d094125 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 21:52:07 +0100 Subject: [PATCH 089/172] 84.6%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 160 ++++++++++++++++---------------- 1 file changed, 79 insertions(+), 81 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 583e514e5..1a1b17dcf 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -119,90 +119,88 @@ int ActualReadJoystickData() { int nNewTop; int port; - if (!JoystickInitialized) { - return 0; - } - - nNewTop = (JoystickRingBufferTop + 1) & 0x1F; - if (nNewTop == JoystickRingBufferBottom) { - return 0; - } - - PADRead(HardwarePadStatus); - PADClamp(HardwarePadStatus); - plat_lgwheels->ReadAll(); - - for (port = 0; port <= 3; port++) { - JoyData *joy_data = &PadRingData[port][JoystickRingBufferTop]; - PadData *pad_data = &joy_data->ThePadData[0]; - - bMemSet(joy_data, 0xFF, sizeof(JoyData)); - - if (HardwarePadStatus[port].err == PAD_ERR_NONE) { - joy_data->padSTATUS = HardwarePadStatus[port]; - pad_data->Type = 0x41; - pad_data->Error = 0; - pad_data->DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); - pad_data->AnalogRightX = ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); - pad_data->AnalogRightY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); - pad_data->AnalogLeftX = ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); - pad_data->AnalogLeftY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); - pad_data->LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); - pad_data->RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); - } else if (plat_lgwheels->IsConnected(port)) { - const LGPosition *wheel_position; - int wheel_connected; - int pedals_connected; - - wheel_connected = wasWheelConnected[port]; - if (!wheel_connected) { - wasWheelConnected[port] = 1; - calibrationTimer[port] = 7.0f; - lastCalibTime[port] = RealTimer.GetSeconds(); - } - - if (calibrationTimer[port] < 5.0f && notYetCalibrating[port]) { - AutoCalibrateWheel(port); - notYetCalibrating[port] = 0; - } - - if (calibrationTimer[port] > 0.0f) { - float now = RealTimer.GetSeconds(); - float elapsed = now - lastCalibTime[port]; - lastCalibTime[port] = now; - calibrationTimer[port] -= elapsed; - } - - wheel_position = &reinterpret_cast(plat_lgwheels)[port]; - joy_data->padSTATUS.button = wheel_position->button; - HardwarePadStatus[port].button = wheel_position->button; - pad_data->Error = 0; - pedals_connected = plat_lgwheels->PedalsConnected(port); - pad_data->Type = pedals_connected ? 0x51 : 0x50; - pad_data->DigitalButtons = ConvertPadButtons(wheel_position->button); - pad_data->AnalogRightX = 0; - pad_data->AnalogLeftX = wheel_position->wheel + 0x80; - - if (pedals_connected) { - pad_data->AnalogRightY = wheel_position->accelerator; - pad_data->AnalogLeftY = wheel_position->brake; - } else { - pad_data->AnalogLeftY = 0; - pad_data->AnalogRightY = 0; + if (JoystickInitialized) { + nNewTop = (JoystickRingBufferTop + 1) & 0x1F; + if (nNewTop != JoystickRingBufferBottom) { + PADRead(HardwarePadStatus); + PADClamp(HardwarePadStatus); + plat_lgwheels->ReadAll(); + + for (port = 0; port <= 3; port++) { + JoyData *joy_data = &PadRingData[port][JoystickRingBufferTop]; + PadData *pad_data = &joy_data->ThePadData[0]; + + bMemSet(joy_data, 0xFF, sizeof(JoyData)); + + if (HardwarePadStatus[port].err == PAD_ERR_NONE) { + joy_data->padSTATUS = HardwarePadStatus[port]; + pad_data->Type = 0x41; + pad_data->Error = 0; + pad_data->DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); + pad_data->AnalogRightX = ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); + pad_data->AnalogRightY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); + pad_data->AnalogLeftX = ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); + pad_data->AnalogLeftY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); + pad_data->LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); + pad_data->RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); + } else if (plat_lgwheels->IsConnected(port)) { + const LGPosition *wheel_position; + int wheel_connected; + int pedals_connected; + + wheel_connected = wasWheelConnected[port]; + if (!wheel_connected) { + wasWheelConnected[port] = 1; + calibrationTimer[port] = 7.0f; + lastCalibTime[port] = RealTimer.GetSeconds(); + } + + if (calibrationTimer[port] < 5.0f && notYetCalibrating[port]) { + AutoCalibrateWheel(port); + notYetCalibrating[port] = 0; + } + + if (calibrationTimer[port] > 0.0f) { + float now = RealTimer.GetSeconds(); + float elapsed = now - lastCalibTime[port]; + lastCalibTime[port] = now; + calibrationTimer[port] -= elapsed; + } + + wheel_position = &reinterpret_cast(plat_lgwheels)[port]; + joy_data->padSTATUS.button = wheel_position->button; + HardwarePadStatus[port].button = wheel_position->button; + pad_data->Error = 0; + pedals_connected = plat_lgwheels->PedalsConnected(port); + pad_data->Type = pedals_connected ? 0x51 : 0x50; + pad_data->DigitalButtons = ConvertPadButtons(wheel_position->button); + pad_data->AnalogRightX = 0; + pad_data->AnalogLeftX = wheel_position->wheel + 0x80; + + if (pedals_connected) { + pad_data->AnalogRightY = wheel_position->accelerator; + pad_data->AnalogLeftY = wheel_position->brake; + } else { + pad_data->AnalogLeftY = 0; + pad_data->AnalogRightY = 0; + } + + pad_data->LTrigger = wheel_position->triggerLeft; + pad_data->RTrigger = wheel_position->triggerRight; + } else { + pad_data->Type = 0xFF; + wasWheelConnected[port] = 0; + notYetCalibrating[port] = 1; + pad_data->Error = 1; + PADReset(PADMASKS[port]); + HardwarePadStatus[port].button = 0; + } } - pad_data->LTrigger = wheel_position->triggerLeft; - pad_data->RTrigger = wheel_position->triggerRight; - } else { - pad_data->Type = 0xFF; - wasWheelConnected[port] = 0; - notYetCalibrating[port] = 1; - pad_data->Error = 1; - PADReset(PADMASKS[port]); - HardwarePadStatus[port].button = 0; + JoystickRingBufferTop = nNewTop; + return 1; } } - JoystickRingBufferTop = nNewTop; - return 1; + return 0; } From b77c35e2f7f132eb87e932bd134558ab873f80ee Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 21:56:32 +0100 Subject: [PATCH 090/172] 84.6%: match+ MemoryCardImp::DestructSaveInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index d10d1adb7..db28a7ce7 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -132,9 +132,13 @@ RealmcIface::SaveInfo *MemoryCardImp::ConstructSaveInfo(MemoryCard::SaveType typ } void MemoryCardImp::DestructSaveInfo() { - if (this->m_SaveReq.mSaveInfo) { - delete this->m_SaveReq.mSaveInfo; - this->m_SaveReq.mSaveInfo = 0; + RealmcIface::SaveInfo *pInfo; + + pInfo = this->m_SaveReq.mSaveInfo; + if (pInfo) { + delete pInfo; + pInfo = 0; + this->m_SaveReq.mSaveInfo = pInfo; } } From 73d1c19286f8f48101152946bdcef5b8a985d262 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:05:19 +0100 Subject: [PATCH 091/172] 84.7%: match+ TextureInfoPlatInfo::SetImage(TextureInfo *) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 10 +++++----- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index fbf819c1f..c25779946 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -125,13 +125,13 @@ unsigned char TextureInfoPlatInfo::HasClut() { unsigned char TextureInfoPlatInfo::SetImage(TextureInfo *texture_info) { TextureInfoPlatInfo *plat_info = texture_info->GetPlatInfo(); - if (!plat_info) { - return 0; + if (plat_info) { + plat_info->SetImage(texture_info->Width, texture_info->Height, texture_info->NumMipMapLevels, plat_info->Format, + texture_info->ImageData, texture_info->PaletteData, texture_info->AlphaUsageType, texture_info->TilableUV); + return 1; } - plat_info->SetImage(texture_info->Width, texture_info->Height, texture_info->NumMipMapLevels, plat_info->Format, texture_info->ImageData, - texture_info->PaletteData, texture_info->AlphaUsageType, texture_info->TilableUV); - return 1; + return 0; } unsigned char TextureInfoPlatInfo::SetImage(int width, int height, int mip, int format, void *imageData, void *imagePal, diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp index 577dca5cf..634ca096e 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.hpp @@ -42,7 +42,7 @@ class TextureInfoPlatInterface { void *LockPalette(TextureLockType lock); void UnlockPalette(void *palette_lock); - TextureInfoPlatInfo *GetPlatInfo() { + TextureInfoPlatInfo *GetPlatInfo() const { return this->PlatInfo; } }; From 293c648ac2b416c3656b836687582062b10d92c3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:17:13 +0100 Subject: [PATCH 092/172] 84.7%: improve InitPlatform Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index d4e7c5a09..d758b2a8f 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -175,6 +175,7 @@ void InitPlatform() { opts.size = 0x38; FILE_getopts(&opts); + opts.DiscType = 1; opts.allocator = &gMemoryAllocator; opts.MaxOpenFiles = 0x20; opts.MaxFileOps = 0x40; From 09c2a65980a27e23154f1520cec08a329f7cf23d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:26:03 +0100 Subject: [PATCH 093/172] 84.8%: match MemoryCardImp::ConstructSaveInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index db28a7ce7..70578dec6 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -124,8 +124,8 @@ RealmcIface::SaveInfo *MemoryCardImp::ConstructSaveInfo(MemoryCard::SaveType typ save_info->mGcInfo.mBannerDataInfo = *reinterpret_cast(reinterpret_cast(MemoryCard::s_pThis) + 0x14); this->m_SaveReq.mSaveInfo = save_info; - save_info->mContentName = MemoryCardImp::gContentName; save_info->mTypeName = MemoryCardImp::gEntryType[type]; + save_info->mContentName = MemoryCardImp::gContentName; save_info->mHeaderSize = 8; save_info->mBodySize = aSize; return save_info; From 6e10cd6dc7d44205a2575dadd0da89e1ef663eb5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:39:18 +0100 Subject: [PATCH 094/172] 84.9%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 59 ++++++++++++++++----------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 1a1b17dcf..12af494ab 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -128,28 +128,28 @@ int ActualReadJoystickData() { for (port = 0; port <= 3; port++) { JoyData *joy_data = &PadRingData[port][JoystickRingBufferTop]; - PadData *pad_data = &joy_data->ThePadData[0]; bMemSet(joy_data, 0xFF, sizeof(JoyData)); if (HardwarePadStatus[port].err == PAD_ERR_NONE) { joy_data->padSTATUS = HardwarePadStatus[port]; - pad_data->Type = 0x41; - pad_data->Error = 0; - pad_data->DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); - pad_data->AnalogRightX = ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); - pad_data->AnalogRightY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); - pad_data->AnalogLeftX = ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); - pad_data->AnalogLeftY = ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); - pad_data->LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); - pad_data->RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); + joy_data->ThePadData[0].Type = 0x41; + joy_data->ThePadData[0].Error = 0; + joy_data->ThePadData[0].DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); + joy_data->ThePadData[0].AnalogRightX = + ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); + joy_data->ThePadData[0].AnalogRightY = + ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); + joy_data->ThePadData[0].AnalogLeftX = + ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); + joy_data->ThePadData[0].AnalogLeftY = + ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); + joy_data->ThePadData[0].LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); + joy_data->ThePadData[0].RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); } else if (plat_lgwheels->IsConnected(port)) { const LGPosition *wheel_position; - int wheel_connected; - int pedals_connected; - wheel_connected = wasWheelConnected[port]; - if (!wheel_connected) { + if (!wasWheelConnected[port]) { wasWheelConnected[port] = 1; calibrationTimer[port] = 7.0f; lastCalibTime[port] = RealTimer.GetSeconds(); @@ -170,28 +170,27 @@ int ActualReadJoystickData() { wheel_position = &reinterpret_cast(plat_lgwheels)[port]; joy_data->padSTATUS.button = wheel_position->button; HardwarePadStatus[port].button = wheel_position->button; - pad_data->Error = 0; - pedals_connected = plat_lgwheels->PedalsConnected(port); - pad_data->Type = pedals_connected ? 0x51 : 0x50; - pad_data->DigitalButtons = ConvertPadButtons(wheel_position->button); - pad_data->AnalogRightX = 0; - pad_data->AnalogLeftX = wheel_position->wheel + 0x80; - - if (pedals_connected) { - pad_data->AnalogRightY = wheel_position->accelerator; - pad_data->AnalogLeftY = wheel_position->brake; + joy_data->ThePadData[0].Error = 0; + joy_data->ThePadData[0].Type = plat_lgwheels->PedalsConnected(port) ? 0x51 : 0x50; + joy_data->ThePadData[0].DigitalButtons = ConvertPadButtons(wheel_position->button); + joy_data->ThePadData[0].AnalogRightX = 0; + joy_data->ThePadData[0].AnalogLeftX = wheel_position->wheel + 0x80; + + if (plat_lgwheels->PedalsConnected(port)) { + joy_data->ThePadData[0].AnalogRightY = wheel_position->accelerator; + joy_data->ThePadData[0].AnalogLeftY = wheel_position->brake; } else { - pad_data->AnalogLeftY = 0; - pad_data->AnalogRightY = 0; + joy_data->ThePadData[0].AnalogLeftY = 0; + joy_data->ThePadData[0].AnalogRightY = 0; } - pad_data->LTrigger = wheel_position->triggerLeft; - pad_data->RTrigger = wheel_position->triggerRight; + joy_data->ThePadData[0].LTrigger = wheel_position->triggerLeft; + joy_data->ThePadData[0].RTrigger = wheel_position->triggerRight; } else { - pad_data->Type = 0xFF; + joy_data->ThePadData[0].Type = 0xFF; wasWheelConnected[port] = 0; notYetCalibrating[port] = 1; - pad_data->Error = 1; + joy_data->ThePadData[0].Error = 1; PADReset(PADMASKS[port]); HardwarePadStatus[port].button = 0; } From 19a0f837de575842c6dc33255f1ea8aee3def281 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:43:53 +0100 Subject: [PATCH 095/172] 86.3%: improve UpdateXenonEmitters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 35 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 1b495755f..0402e4d6a 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -4,14 +4,17 @@ #include "Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h" #include "Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h" +#include "Speed/Indep/Libs/Support/Utility/UStandard.h" #include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" #include "Speed/Indep/Libs/Support/Utility/UTypes.h" +DECLARE_CONTAINER_TYPE(XenonEffectDef); + struct XenonEffectDef { // total size: 0x58 UMath::Vector4 vel; // offset 0x0, size 0x10 UMath::Matrix4 mat; // offset 0x10, size 0x40 - Attrib::Collection *spec; // offset 0x50, size 0x4 + const Attrib::Collection *spec; // offset 0x50, size 0x4 EmitterGroup *piggyback_effect; // offset 0x54, size 0x4 ~XenonEffectDef() {} }; @@ -385,7 +388,7 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, effect.mat = UMath::Matrix4::kIdentity; effect.mat.v3 = mat->v3; effect.piggyback_effect = piggyback_fx; - effect.spec = const_cast(spec); + effect.spec = spec; effect.vel = *vel; gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(effect); @@ -393,26 +396,28 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, } void UpdateXenonEmitters(float dt) { - XenonEffectDef staged_effect; + typedef UTL::Std::vector XenonEffectStdVector; + XenonEffectDef *iter; + XenonEffectDef eDef; gParticleList.AgeParticles(dt); - XenonEffectDef *effect = gNGEffectList.lists[XenonEffectLists::ACTIVE].start; - while (effect != gNGEffectList.lists[XenonEffectLists::ACTIVE].finish) { - staged_effect = *effect; - gNGEffectList.lists[XenonEffectLists::STAGING].push_back(staged_effect); - ++effect; + iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).begin(); + while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).end()) { + eDef = *iter; + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); + ++iter; } - gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); - effect = gNGEffectList.lists[XenonEffectLists::STAGING].start; - while (effect != gNGEffectList.lists[XenonEffectLists::STAGING].finish) { - staged_effect = *effect; - NGEffect ng_effect(staged_effect); - ++effect; + iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).begin(); + while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).end()) { + eDef = *iter; + NGEffect anEffect(eDef); + ++iter; } - gNGEffectList.lists[XenonEffectLists::STAGING].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); gParticleList.GeneratePolys(); } From 0c8b1466311b02a00a386276fc824d1717782d57 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:51:22 +0100 Subject: [PATCH 096/172] 86.4%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 111 ++++++++++++++--------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 0402e4d6a..92b858e56 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -7,6 +7,7 @@ #include "Speed/Indep/Libs/Support/Utility/UStandard.h" #include "Speed/Indep/Libs/Support/Utility/UVectorMath.h" #include "Speed/Indep/Libs/Support/Utility/UTypes.h" +#include "Speed/Indep/bWare/Inc/bMath.hpp" DECLARE_CONTAINER_TYPE(XenonEffectDef); @@ -19,6 +20,8 @@ struct XenonEffectDef { ~XenonEffectDef() {} }; +typedef UTL::Std::vector XenonEffectStdVector; + struct XenonEffectVec; extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( @@ -220,89 +223,86 @@ NGParticle *ParticleList::GetNextParticle() { } void CGEmitter::SpawnParticles(float dt, float intensity) { - unsigned int seed = randomSeed; + unsigned int random_seed = randomSeed; if (intensity > 0.0f) { UMath::Matrix4 local_world = mLocalWorld; - UMath::Matrix4 rotation_world = local_world; - rotation_world.v3.x = 0.0f; - rotation_world.v3.y = 0.0f; - rotation_world.v3.z = 0.0f; - rotation_world.v3.w = 1.0f; - UMath::Vector4 velocity_base; - UMath::Vector4 velocity_center; - UMath::Vector4 volume_extent; - UMath::Vector4 spawn_point; - float age = 0.0f; - int colour_r = static_cast(mEmitterDef.Colour1().x * 255.0f); - int colour_g = static_cast(mEmitterDef.Colour1().y * 255.0f); - int colour_b = static_cast(mEmitterDef.Colour1().z * 255.0f); - int colour_a = static_cast(mEmitterDef.Colour1().w * 255.0f); - float count = intensity * mEmitterDef.NumParticles(); + UMath::Matrix4 local_orientation = local_world; + local_orientation.v3.x = 0.0f; + local_orientation.v3.y = 0.0f; + local_orientation.v3.z = 0.0f; + local_orientation.v3.w = 1.0f; + int r = static_cast(mEmitterDef.Colour1().x * 255.0f); + int g = static_cast(mEmitterDef.Colour1().y * 255.0f); + int b = static_cast(mEmitterDef.Colour1().z * 255.0f); + int a = static_cast(mEmitterDef.Colour1().w * 255.0f); + float num_particles = intensity * mEmitterDef.NumParticles(); float life = mEmitterDef.Life(); - float count_after_variance = count - count * mEmitterDef.NumParticlesVariance() * 100.0f; - float life_factor = life - life * mEmitterDef.LifeVariance(); - unsigned int precomputed_color = colour_a << 24 | colour_b << 16 | colour_g << 8 | colour_r; - - if (count_after_variance != 0.0f) { - float particle_step = dt / count_after_variance; - while (count_after_variance != 0.0f) { + float num_particles_variance = num_particles - num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; + float particle_age_factor = life - life * mEmitterDef.LifeVariance(); + float current_particle_age = 0.0f; + unsigned int particleColor = a << 24 | b << 16 | g << 8 | r; + + if (num_particles_variance != 0.0f) { + float particle_step = dt / num_particles_variance; + while (num_particles_variance != 0.0f) { NGParticle *particle; - float length_start; - float length_clamped; + float sparkLength; + float ld; + UMath::Vector4 pvel; + UMath::Vector4 rand; + UMath::Vector4 rotatedVel; float gravity; + UMath::Vector4 ppos; - count_after_variance -= 1.0f; + num_particles_variance -= 1.0f; particle = gParticleList.GetNextParticle(); if (!particle) { break; } - length_start = mEmitterDef.LengthStart() + bRandom(mEmitterDef.LengthDelta(), &seed); - if (length_start < 0.0f) { + sparkLength = mEmitterDef.LengthStart() + bRandom(mEmitterDef.LengthDelta(), &random_seed); + if (sparkLength < 0.0f) { break; } - length_clamped = 1.0f; - if (length_start < 1.0f) { - length_clamped = length_start; - } + ld = bMin(sparkLength, 1.0f); - volume_extent.x = 1.0f - (mEmitterDef.VelocityDelta().x - bRandom(mEmitterDef.VelocityDelta().x, &seed) * 2.0f); - volume_extent.y = 1.0f - (mEmitterDef.VelocityDelta().y - bRandom(mEmitterDef.VelocityDelta().y, &seed) * 2.0f); - volume_extent.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &seed) * 2.0f); - volume_extent.w = 1.0f; + rand.x = 1.0f - (mEmitterDef.VelocityDelta().x - bRandom(mEmitterDef.VelocityDelta().x, &random_seed) * 2.0f); + rand.y = 1.0f - (mEmitterDef.VelocityDelta().y - bRandom(mEmitterDef.VelocityDelta().y, &random_seed) * 2.0f); + rand.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &random_seed) * 2.0f); + rand.w = 1.0f; - VU0_v4scalexyz(mEmitterDef.VelocityInherit(), mVel, velocity_base); - VU0_MATRIX3x4_vect4mult(mEmitterDef.VolumeCenter(), mLocalWorld, velocity_center); - VU0_v4add(velocity_base, velocity_center, velocity_base); - VU0_v4scalexyz(velocity_base, volume_extent, velocity_base); + VU0_v4scalexyz(mEmitterDef.VelocityInherit(), mVel, pvel); + VU0_MATRIX3x4_vect4mult(mEmitterDef.VelocityStart(), local_orientation, rotatedVel); + VU0_v4add(pvel, rotatedVel, pvel); + VU0_v4scalexyz(pvel, rand, pvel); - gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &seed) * 2.0f; + gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &random_seed) * 2.0f; - spawn_point.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeExtent().x, &seed) - mEmitterDef.VolumeExtent().x * 0.5f); - spawn_point.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeExtent().y, &seed) - mEmitterDef.VolumeExtent().y * 0.5f); - spawn_point.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeExtent().z, &seed) - mEmitterDef.VolumeExtent().z * 0.5f); - spawn_point.w = 1.0f; + ppos.x = mEmitterDef.VolumeCenter().x + (bRandom(mEmitterDef.VolumeExtent().x, &random_seed) - mEmitterDef.VolumeExtent().x * 0.5f); + ppos.y = mEmitterDef.VolumeCenter().y + (bRandom(mEmitterDef.VolumeExtent().y, &random_seed) - mEmitterDef.VolumeExtent().y * 0.5f); + ppos.z = mEmitterDef.VolumeCenter().z + (bRandom(mEmitterDef.VolumeExtent().z, &random_seed) - mEmitterDef.VolumeExtent().z * 0.5f); + ppos.w = 1.0f; - UMath::RotateTranslate(spawn_point, local_world, spawn_point); - VU0_v3scaleadd(UMath::Vector4To3(velocity_base), age, UMath::Vector4To3(spawn_point), + UMath::RotateTranslate(ppos, local_world, ppos); + VU0_v3scaleadd(UMath::Vector4To3(pvel), current_particle_age, UMath::Vector4To3(ppos), particle->initialPos); - particle->initialPos.z += gravity * age * age; - particle->vel = UMath::Vector4To3(velocity_base); - particle->age = age; + particle->initialPos.z += gravity * current_particle_age * current_particle_age; + particle->vel = UMath::Vector4To3(pvel); + particle->age = current_particle_age; particle->gravity = gravity; - particle->life = static_cast(life_factor * 65535.0f); - particle->color = precomputed_color; - particle->length = static_cast(length_clamped * 255.0f); + particle->life = static_cast(particle_age_factor * 65535.0f); + particle->color = particleColor; + particle->length = static_cast(ld * 255.0f); particle->width = static_cast(mEmitterDef.HeightStart()); - age += particle_step; + current_particle_age += particle_step; } } - randomSeed = seed; + randomSeed = random_seed; } } @@ -396,7 +396,6 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, } void UpdateXenonEmitters(float dt) { - typedef UTL::Std::vector XenonEffectStdVector; XenonEffectDef *iter; XenonEffectDef eDef; From 5c3277633df86fb243dc042b3dffe0144e2a3e74 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Fri, 20 Mar 2026 22:54:51 +0100 Subject: [PATCH 097/172] 86.4%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 92b858e56..26558cab5 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -236,16 +236,19 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { int g = static_cast(mEmitterDef.Colour1().y * 255.0f); int b = static_cast(mEmitterDef.Colour1().z * 255.0f); int a = static_cast(mEmitterDef.Colour1().w * 255.0f); - float num_particles = intensity * mEmitterDef.NumParticles(); float life = mEmitterDef.Life(); - float num_particles_variance = num_particles - num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; - float particle_age_factor = life - life * mEmitterDef.LifeVariance(); + float life_variance = life * mEmitterDef.LifeVariance(); + float num_particles = intensity * mEmitterDef.NumParticles(); + float num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; + float particle_age_factor; float current_particle_age = 0.0f; unsigned int particleColor = a << 24 | b << 16 | g << 8 | r; + life -= life_variance; + num_particles -= num_particles_variance; - if (num_particles_variance != 0.0f) { - float particle_step = dt / num_particles_variance; - while (num_particles_variance != 0.0f) { + if (num_particles != 0.0f) { + particle_age_factor = dt / num_particles; + while (num_particles != 0.0f) { NGParticle *particle; float sparkLength; float ld; @@ -255,7 +258,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { float gravity; UMath::Vector4 ppos; - num_particles_variance -= 1.0f; + num_particles -= 1.0f; particle = gParticleList.GetNextParticle(); if (!particle) { break; @@ -286,19 +289,18 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { ppos.w = 1.0f; UMath::RotateTranslate(ppos, local_world, ppos); - VU0_v3scaleadd(UMath::Vector4To3(pvel), current_particle_age, UMath::Vector4To3(ppos), - particle->initialPos); + UMath::ScaleAdd(UMath::Vector4To3(pvel), current_particle_age, UMath::Vector4To3(ppos), particle->initialPos); particle->initialPos.z += gravity * current_particle_age * current_particle_age; particle->vel = UMath::Vector4To3(pvel); particle->age = current_particle_age; particle->gravity = gravity; - particle->life = static_cast(particle_age_factor * 65535.0f); + particle->life = static_cast(life * 65535.0f); particle->color = particleColor; particle->length = static_cast(ld * 255.0f); particle->width = static_cast(mEmitterDef.HeightStart()); - current_particle_age += particle_step; + current_particle_age += particle_age_factor; } } From dce867c54bf47d7bc605bd781de03216b3f50a1f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 01:54:38 +0100 Subject: [PATCH 098/172] 87.6%: improve AddXenonEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 233 ++++++++--------------------- 1 file changed, 59 insertions(+), 174 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 26558cab5..cc4417f85 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -22,82 +22,9 @@ struct XenonEffectDef { typedef UTL::Std::vector XenonEffectStdVector; -struct XenonEffectVec; - -extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( - XenonEffectVec *vec, unsigned int count); - -struct XenonEffectVec { - XenonEffectDef *start; // offset 0x0, size 0x4 - XenonEffectDef *finish; // offset 0x4, size 0x4 - void *unused; // offset 0x8, size 0x4 - XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 - - XenonEffectVec() : start(0), finish(0), end_of_storage(0) { - reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi(this, 20); - } - - void clear() { - XenonEffectDef *p = start; - while (p != finish) { - p->~XenonEffectDef(); - p++; - } - finish = start; - } - - void push_back(const XenonEffectDef &value) { - if (finish != end_of_storage) { - if (finish != 0) { - *finish = value; - } - finish++; - } else { - unsigned int size = finish - start; - unsigned int old_capacity = end_of_storage - start; - unsigned int new_capacity = size + (size == 0 ? 1 : size); - unsigned int new_bytes; - XenonEffectDef *new_start; - XenonEffectDef *src; - XenonEffectDef *dst; - - if (new_capacity != 0) { - new_bytes = new_capacity * sizeof(XenonEffectDef); - new_start = static_cast(gFastMem.Alloc(new_bytes, 0)); - } else { - new_start = 0; - new_bytes = 0; - } - - src = start; - dst = new_start; - while (src != finish) { - if (dst != 0) { - *dst = *src; - } - src++; - dst++; - } - - if (dst != 0) { - *dst = value; - } - dst++; - - if (start != 0) { - gFastMem.Free(start, old_capacity * sizeof(XenonEffectDef), 0); - } - - start = new_start; - finish = dst; - end_of_storage = reinterpret_cast(reinterpret_cast(new_start) + new_bytes); - } - } -}; - struct XenonEffectLists { enum { ACTIVE = 0, STAGING = 1 }; - XenonEffectVec lists[2]; // [0]=active, [1]=staging + XenonEffectStdVector lists[2]; // [0]=active, [1]=staging }; struct CGEmitter { @@ -141,67 +68,6 @@ XenonEffectLists gNGEffectList; extern ParticleList gParticleList; extern XSpriteManager NGSpriteManager; extern unsigned int randomSeed; - -static inline void reserveXenonEffectVecImpl(XenonEffectVec *vec, unsigned int count) { - reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi(vec, count); -} - -extern "C" void reserve__Q24_STLt6vector2Z14XenonEffectDefZQ33UTL3Stdt9Allocator2Z14XenonEffectDefZ20_type_XenonEffectDefUi( - XenonEffectVec *vec, unsigned int count) { - unsigned int capacity = vec->end_of_storage - vec->start; - if (capacity >= count) { - return; - } - - unsigned int size = vec->finish - vec->start; - XenonEffectDef *old_start = vec->start; - - XenonEffectDef *new_buf; - unsigned int new_bytes; - if (old_start != 0) { - if (count != 0) { - new_bytes = count * sizeof(XenonEffectDef); - new_buf = static_cast(gFastMem.Alloc(new_bytes, 0)); - } else { - new_buf = 0; - new_bytes = 0; - } - - XenonEffectDef *src = old_start; - XenonEffectDef *dst = new_buf; - while (src != vec->finish) { - if (dst != 0) { - *dst = *src; - } - src++; - dst++; - } - - XenonEffectDef *old_iter = vec->start; - XenonEffectDef *old_finish = vec->finish; - while (old_iter != old_finish) { - old_iter->~XenonEffectDef(); - old_iter++; - } - - unsigned int old_capacity = vec->end_of_storage - vec->start; - if (vec->start != 0) { - gFastMem.Free(vec->start, old_capacity * sizeof(XenonEffectDef), 0); - } - } else { - if (count != 0) { - new_bytes = count * sizeof(XenonEffectDef); - new_buf = static_cast(gFastMem.Alloc(new_bytes, 0)); - } else { - new_buf = 0; - new_bytes = 0; - } - } - - vec->end_of_storage = reinterpret_cast(reinterpret_cast(new_buf) + new_bytes); - vec->start = new_buf; - vec->finish = reinterpret_cast(reinterpret_cast(new_buf) + size * sizeof(XenonEffectDef)); -} float bRandom(float range, unsigned int *seed); unsigned int bStringHash(const char *str); TextureInfo *GetTextureInfo(unsigned int name_hash, int allow_default, int force_local); @@ -223,28 +89,42 @@ NGParticle *ParticleList::GetNextParticle() { } void CGEmitter::SpawnParticles(float dt, float intensity) { - unsigned int random_seed = randomSeed; + UMath::Matrix4 local_world; + UMath::Matrix4 local_orientation; + unsigned int random_seed; + float life_variance; + float life; + int r; + int g; + int b; + int a; + unsigned int particleColor; + float num_particles_variance; + float num_particles; + float particle_age_factor; + float current_particle_age; + + random_seed = randomSeed; if (intensity > 0.0f) { - UMath::Matrix4 local_world = mLocalWorld; - UMath::Matrix4 local_orientation = local_world; + local_world = mLocalWorld; + local_orientation = local_world; local_orientation.v3.x = 0.0f; local_orientation.v3.y = 0.0f; local_orientation.v3.z = 0.0f; local_orientation.v3.w = 1.0f; - int r = static_cast(mEmitterDef.Colour1().x * 255.0f); - int g = static_cast(mEmitterDef.Colour1().y * 255.0f); - int b = static_cast(mEmitterDef.Colour1().z * 255.0f); - int a = static_cast(mEmitterDef.Colour1().w * 255.0f); - float life = mEmitterDef.Life(); - float life_variance = life * mEmitterDef.LifeVariance(); - float num_particles = intensity * mEmitterDef.NumParticles(); - float num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; - float particle_age_factor; - float current_particle_age = 0.0f; - unsigned int particleColor = a << 24 | b << 16 | g << 8 | r; - life -= life_variance; + r = static_cast(mEmitterDef.Colour1().x * 255.0f); + g = static_cast(mEmitterDef.Colour1().y * 255.0f); + b = static_cast(mEmitterDef.Colour1().z * 255.0f); + a = static_cast(mEmitterDef.Colour1().w * 255.0f); + life = mEmitterDef.Life(); + life_variance = life * mEmitterDef.LifeVariance(); + num_particles = intensity * mEmitterDef.NumParticles(); + num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; + current_particle_age = 0.0f; + particleColor = a << 24 | b << 16 | g << 8 | r; num_particles -= num_particles_variance; + life -= life_variance; if (num_particles != 0.0f) { particle_age_factor = dt / num_particles; @@ -289,10 +169,16 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { ppos.w = 1.0f; UMath::RotateTranslate(ppos, local_world, ppos); - UMath::ScaleAdd(UMath::Vector4To3(pvel), current_particle_age, UMath::Vector4To3(ppos), particle->initialPos); + UMath::ScaleAdd( + reinterpret_cast(pvel), + current_particle_age, + reinterpret_cast(ppos), + particle->initialPos); particle->initialPos.z += gravity * current_particle_age * current_particle_age; - particle->vel = UMath::Vector4To3(pvel); + particle->vel.x = pvel.x; + particle->vel.y = pvel.y; + particle->vel.z = pvel.z; particle->age = current_particle_age; particle->gravity = gravity; particle->life = static_cast(life * 65535.0f); @@ -373,27 +259,26 @@ void ClearXenonEmitters() { } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { - unsigned int size = gNGEffectList.lists[XenonEffectLists::ACTIVE].finish - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; + XenonEffectDef eDef; + XenonEffectStdVector &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; + XenonEffectStdVector &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; - if (size < 20) { - unsigned int active_capacity = gNGEffectList.lists[XenonEffectLists::ACTIVE].end_of_storage - gNGEffectList.lists[XenonEffectLists::ACTIVE].start; - if (active_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::ACTIVE], 20); + if (active.size() < 20) { + if (active.capacity() < 20) { + active.reserve(20); } - unsigned int staging_capacity = gNGEffectList.lists[XenonEffectLists::STAGING].end_of_storage - gNGEffectList.lists[XenonEffectLists::STAGING].start; - if (staging_capacity < 20) { - reserveXenonEffectVecImpl(&gNGEffectList.lists[XenonEffectLists::STAGING], 20); + if (staging.capacity() < 20) { + staging.reserve(20); } - XenonEffectDef effect; - effect.mat = UMath::Matrix4::kIdentity; - effect.mat.v3 = mat->v3; - effect.piggyback_effect = piggyback_fx; - effect.spec = spec; - effect.vel = *vel; + eDef.mat = UMath::Matrix4::kIdentity; + eDef.mat.v3 = mat->v3; + eDef.piggyback_effect = piggyback_fx; + eDef.spec = spec; + eDef.vel = *vel; - gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(effect); + active.push_back(eDef); } } @@ -403,22 +288,22 @@ void UpdateXenonEmitters(float dt) { gParticleList.AgeParticles(dt); - iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).begin(); - while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).end()) { + iter = gNGEffectList.lists[XenonEffectLists::ACTIVE].begin(); + while (iter != gNGEffectList.lists[XenonEffectLists::ACTIVE].end()) { eDef = *iter; - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); + gNGEffectList.lists[XenonEffectLists::STAGING].push_back(eDef); ++iter; } - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); + gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); - iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).begin(); - while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).end()) { + iter = gNGEffectList.lists[XenonEffectLists::STAGING].begin(); + while (iter != gNGEffectList.lists[XenonEffectLists::STAGING].end()) { eDef = *iter; NGEffect anEffect(eDef); ++iter; } - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); + gNGEffectList.lists[XenonEffectLists::STAGING].clear(); gParticleList.GeneratePolys(); } From ff69ca5f9b9f380e8202286a69f09a9e5171d377 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 01:57:09 +0100 Subject: [PATCH 099/172] 87.6%: match AddXenonEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index cc4417f85..5e727b7dc 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -260,16 +260,14 @@ void ClearXenonEmitters() { void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { XenonEffectDef eDef; - XenonEffectStdVector &active = gNGEffectList.lists[XenonEffectLists::ACTIVE]; - XenonEffectStdVector &staging = gNGEffectList.lists[XenonEffectLists::STAGING]; - if (active.size() < 20) { - if (active.capacity() < 20) { - active.reserve(20); + if (gNGEffectList.lists[XenonEffectLists::ACTIVE].size() < 20) { + if (gNGEffectList.lists[XenonEffectLists::ACTIVE].capacity() < 20) { + gNGEffectList.lists[XenonEffectLists::ACTIVE].reserve(20); } - if (staging.capacity() < 20) { - staging.reserve(20); + if (gNGEffectList.lists[XenonEffectLists::STAGING].capacity() < 20) { + gNGEffectList.lists[XenonEffectLists::STAGING].reserve(20); } eDef.mat = UMath::Matrix4::kIdentity; @@ -278,7 +276,7 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, eDef.spec = spec; eDef.vel = *vel; - active.push_back(eDef); + gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(eDef); } } From 4e77d2fb1dda377b438ef8bfc956cf1d143b8693 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 01:59:19 +0100 Subject: [PATCH 100/172] 87.9%: match+ AddXenonEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 5e727b7dc..aa0534c5c 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -17,7 +17,6 @@ struct XenonEffectDef { UMath::Matrix4 mat; // offset 0x10, size 0x40 const Attrib::Collection *spec; // offset 0x50, size 0x4 EmitterGroup *piggyback_effect; // offset 0x54, size 0x4 - ~XenonEffectDef() {} }; typedef UTL::Std::vector XenonEffectStdVector; From f283496136f6a2b8aff2b1d2d591e82df709b4a0 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 02:06:16 +0100 Subject: [PATCH 101/172] 87.8%: improve __static_initialization_and_destruction_0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 48 ++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index aa0534c5c..54358a61e 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -21,9 +21,25 @@ struct XenonEffectDef { typedef UTL::Std::vector XenonEffectStdVector; +struct XenonEffectVec { + XenonEffectDef *start; // offset 0x0, size 0x4 + XenonEffectDef *finish; // offset 0x4, size 0x4 + void *unused; // offset 0x8, size 0x4 + XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 +}; + struct XenonEffectLists { enum { ACTIVE = 0, STAGING = 1 }; - XenonEffectStdVector lists[2]; // [0]=active, [1]=staging + XenonEffectVec lists[2]; // [0]=active, [1]=staging + + XenonEffectLists() { + for (int i = ACTIVE; i <= STAGING; i++) { + lists[i].start = 0; + lists[i].finish = 0; + lists[i].end_of_storage = 0; + reinterpret_cast(lists[i]).reserve(20); + } + } }; struct CGEmitter { @@ -253,20 +269,20 @@ void ParticleList::GeneratePolys() { void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { - gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); - gNGEffectList.lists[XenonEffectLists::STAGING].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { XenonEffectDef eDef; - if (gNGEffectList.lists[XenonEffectLists::ACTIVE].size() < 20) { - if (gNGEffectList.lists[XenonEffectLists::ACTIVE].capacity() < 20) { - gNGEffectList.lists[XenonEffectLists::ACTIVE].reserve(20); + if (reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).size() < 20) { + if (reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).capacity() < 20) { + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).reserve(20); } - if (gNGEffectList.lists[XenonEffectLists::STAGING].capacity() < 20) { - gNGEffectList.lists[XenonEffectLists::STAGING].reserve(20); + if (reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).capacity() < 20) { + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).reserve(20); } eDef.mat = UMath::Matrix4::kIdentity; @@ -275,7 +291,7 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, eDef.spec = spec; eDef.vel = *vel; - gNGEffectList.lists[XenonEffectLists::ACTIVE].push_back(eDef); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).push_back(eDef); } } @@ -285,22 +301,22 @@ void UpdateXenonEmitters(float dt) { gParticleList.AgeParticles(dt); - iter = gNGEffectList.lists[XenonEffectLists::ACTIVE].begin(); - while (iter != gNGEffectList.lists[XenonEffectLists::ACTIVE].end()) { + iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).begin(); + while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).end()) { eDef = *iter; - gNGEffectList.lists[XenonEffectLists::STAGING].push_back(eDef); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); ++iter; } - gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); - iter = gNGEffectList.lists[XenonEffectLists::STAGING].begin(); - while (iter != gNGEffectList.lists[XenonEffectLists::STAGING].end()) { + iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).begin(); + while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).end()) { eDef = *iter; NGEffect anEffect(eDef); ++iter; } - gNGEffectList.lists[XenonEffectLists::STAGING].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); gParticleList.GeneratePolys(); } From 5a423e013d2e2b6cee8210f379121fb949ca538d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 02:15:46 +0100 Subject: [PATCH 102/172] 88.0%: improve UpdateXenonEmitters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 54358a61e..391673575 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -304,8 +304,8 @@ void UpdateXenonEmitters(float dt) { iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).begin(); while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).end()) { eDef = *iter; - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); ++iter; + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); } reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); @@ -313,8 +313,10 @@ void UpdateXenonEmitters(float dt) { iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).begin(); while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).end()) { eDef = *iter; - NGEffect anEffect(eDef); ++iter; + if (!eDef.piggyback_effect || eDef.piggyback_effect->IsEnabled()) { + NGEffect anEffect(eDef); + } } reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); From 05dce88e5df9af36e1b07536774d9ba79eca34b6 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 02:23:05 +0100 Subject: [PATCH 103/172] 88.0%: dwarf improve UpdateXenonEmitters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 59 +++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 391673575..14b45778e 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -26,6 +26,22 @@ struct XenonEffectVec { XenonEffectDef *finish; // offset 0x4, size 0x4 void *unused; // offset 0x8, size 0x4 XenonEffectDef *end_of_storage; // offset 0xC, size 0x4 + + XenonEffectDef *begin() { + return start; + } + + XenonEffectDef *end() { + return finish; + } + + void push_back(const XenonEffectDef &eDef) { + reinterpret_cast(*this).push_back(eDef); + } + + void clear() { + reinterpret_cast(*this).clear(); + } }; struct XenonEffectLists { @@ -269,8 +285,8 @@ void ParticleList::GeneratePolys() { void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); + gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); + gNGEffectList.lists[XenonEffectLists::STAGING].clear(); } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { @@ -296,29 +312,36 @@ void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, } void UpdateXenonEmitters(float dt) { - XenonEffectDef *iter; - XenonEffectDef eDef; - gParticleList.AgeParticles(dt); - iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).begin(); - while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).end()) { - eDef = *iter; - ++iter; - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).push_back(eDef); + { + XenonEffectDef *iter; + XenonEffectDef eDef; + + iter = gNGEffectList.lists[XenonEffectLists::ACTIVE].begin(); + while (iter != gNGEffectList.lists[XenonEffectLists::ACTIVE].end()) { + eDef = *iter; + gNGEffectList.lists[XenonEffectLists::STAGING].push_back(eDef); + ++iter; + } } - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); + gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); + + { + XenonEffectDef *iter; + XenonEffectDef eDef; - iter = reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).begin(); - while (iter != reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).end()) { - eDef = *iter; - ++iter; - if (!eDef.piggyback_effect || eDef.piggyback_effect->IsEnabled()) { - NGEffect anEffect(eDef); + iter = gNGEffectList.lists[XenonEffectLists::STAGING].begin(); + while (iter != gNGEffectList.lists[XenonEffectLists::STAGING].end()) { + eDef = *iter; + ++iter; + if (!eDef.piggyback_effect || eDef.piggyback_effect->IsEnabled()) { + NGEffect anEffect(eDef); + } } } - reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); + gNGEffectList.lists[XenonEffectLists::STAGING].clear(); gParticleList.GeneratePolys(); } From aa1ecda701788ce527948920a80f9eb977a4d41d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 02:42:36 +0100 Subject: [PATCH 104/172] 88.3%: improve Periodic::UpdateForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index 567aa0acb..f03b6f72a 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -57,6 +57,7 @@ int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & hand int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; + char *effectIDBase; int ret; memset(&force, 0, sizeof(force)); @@ -73,10 +74,12 @@ int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, un force.p.periodic.envelope.attackLevel = attackLevel; force.p.periodic.envelope.fadeLevel = fadeLevel; - ret = LGUpdateForceEffect(PeriodicGetEffectID(this, channel, forceNumber), &force); + effectIDBase = reinterpret_cast(this) + 0x80; + ret = LGUpdateForceEffect(*reinterpret_cast(effectIDBase + channel * 32 + forceNumber * 4), &force); if (ret < 0) { OSReport(kUpdatePeriodicForceError, channel, ret); - PeriodicGetEffectID(this, channel, forceNumber) = static_cast(-1); + *reinterpret_cast(effectIDBase + channel * 32 + forceNumber * 4) = + static_cast(-1); } return ret; From 1b0e7268b616efed398c74d6bf932ef1b2f2d259 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 02:57:08 +0100 Subject: [PATCH 105/172] 87.8%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 14b45778e..264366247 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -9,6 +9,25 @@ #include "Speed/Indep/Libs/Support/Utility/UTypes.h" #include "Speed/Indep/bWare/Inc/bMath.hpp" +inline void Scalexyz(const UMath::Vector4 &a, const UMath::Vector4 &b, UMath::Vector4 &r) { + VU0_v4scalexyz(a, b, r); +} + +namespace UMath { +inline void Rotate(const Vector4 &a, const Matrix4 &m, Vector4 &r) { + VU0_MATRIX3x4_vect4mult(a, m, r); +} + +inline void Add(Vector4 &r, const Vector4 &b) { + VU0_v4add(r, b, r); +} +} // namespace UMath + +inline void Scalexyz(UMath::Vector4 &r, const UMath::Vector4 &b) { + VU0_v4scalexyz(r, b, r); +} + + DECLARE_CONTAINER_TYPE(XenonEffectDef); struct XenonEffectDef { @@ -135,8 +154,6 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { float particle_age_factor; float current_particle_age; - random_seed = randomSeed; - if (intensity > 0.0f) { local_world = mLocalWorld; local_orientation = local_world; @@ -144,6 +161,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { local_orientation.v3.y = 0.0f; local_orientation.v3.z = 0.0f; local_orientation.v3.w = 1.0f; + random_seed = randomSeed; r = static_cast(mEmitterDef.Colour1().x * 255.0f); g = static_cast(mEmitterDef.Colour1().y * 255.0f); b = static_cast(mEmitterDef.Colour1().z * 255.0f); @@ -187,10 +205,10 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { rand.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &random_seed) * 2.0f); rand.w = 1.0f; - VU0_v4scalexyz(mEmitterDef.VelocityInherit(), mVel, pvel); - VU0_MATRIX3x4_vect4mult(mEmitterDef.VelocityStart(), local_orientation, rotatedVel); - VU0_v4add(pvel, rotatedVel, pvel); - VU0_v4scalexyz(pvel, rand, pvel); + Scalexyz(mEmitterDef.VelocityInherit(), mVel, pvel); + UMath::Rotate(mEmitterDef.VelocityStart(), local_orientation, rotatedVel); + UMath::Add(pvel, rotatedVel); + Scalexyz(pvel, rand); gravity = (mEmitterDef.GravityStart() - mEmitterDef.GravityDelta()) + bRandom(mEmitterDef.GravityDelta(), &random_seed) * 2.0f; From e7db202f8ffcbf6e03ac5b7c82da78065f32ca98 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:00:22 +0100 Subject: [PATCH 106/172] 88.1%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 264366247..970ac0d5a 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -162,18 +162,18 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { local_orientation.v3.z = 0.0f; local_orientation.v3.w = 1.0f; random_seed = randomSeed; - r = static_cast(mEmitterDef.Colour1().x * 255.0f); - g = static_cast(mEmitterDef.Colour1().y * 255.0f); - b = static_cast(mEmitterDef.Colour1().z * 255.0f); - a = static_cast(mEmitterDef.Colour1().w * 255.0f); life = mEmitterDef.Life(); life_variance = life * mEmitterDef.LifeVariance(); num_particles = intensity * mEmitterDef.NumParticles(); num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; current_particle_age = 0.0f; + r = static_cast(mEmitterDef.Colour1().x * 255.0f); + g = static_cast(mEmitterDef.Colour1().y * 255.0f); + b = static_cast(mEmitterDef.Colour1().z * 255.0f); + a = static_cast(mEmitterDef.Colour1().w * 255.0f); particleColor = a << 24 | b << 16 | g << 8 | r; - num_particles -= num_particles_variance; life -= life_variance; + num_particles -= num_particles_variance; if (num_particles != 0.0f) { particle_age_factor = dt / num_particles; From be48310aff6f9ace75444cd19099c19f0da0d595 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:02:16 +0100 Subject: [PATCH 107/172] 88.2%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 970ac0d5a..2b874f315 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -203,7 +203,6 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { rand.x = 1.0f - (mEmitterDef.VelocityDelta().x - bRandom(mEmitterDef.VelocityDelta().x, &random_seed) * 2.0f); rand.y = 1.0f - (mEmitterDef.VelocityDelta().y - bRandom(mEmitterDef.VelocityDelta().y, &random_seed) * 2.0f); rand.z = 1.0f - (mEmitterDef.VelocityDelta().z - bRandom(mEmitterDef.VelocityDelta().z, &random_seed) * 2.0f); - rand.w = 1.0f; Scalexyz(mEmitterDef.VelocityInherit(), mVel, pvel); UMath::Rotate(mEmitterDef.VelocityStart(), local_orientation, rotatedVel); From f33cc434919cafb6c298855ac8f92125d82a950e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:08:18 +0100 Subject: [PATCH 108/172] 88.3%: improve Periodic::UpdateForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index f03b6f72a..076d218d5 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -75,10 +75,10 @@ int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, un force.p.periodic.envelope.fadeLevel = fadeLevel; effectIDBase = reinterpret_cast(this) + 0x80; - ret = LGUpdateForceEffect(*reinterpret_cast(effectIDBase + channel * 32 + forceNumber * 4), &force); + ret = LGUpdateForceEffect(*reinterpret_cast(effectIDBase + forceNumber * 4 + channel * 32), &force); if (ret < 0) { OSReport(kUpdatePeriodicForceError, channel, ret); - *reinterpret_cast(effectIDBase + channel * 32 + forceNumber * 4) = + *reinterpret_cast(effectIDBase + forceNumber * 4 + channel * 32) = static_cast(-1); } From d81e9542808cbdedceef574e458ef49c5f0ce522 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:14:05 +0100 Subject: [PATCH 109/172] 88.5%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 2b874f315..253e37c5a 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -233,6 +233,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { particle->color = particleColor; particle->length = static_cast(ld * 255.0f); particle->width = static_cast(mEmitterDef.HeightStart()); + particle->uv[0] = static_cast(mTextureUVs.StartU() * 255.0f); current_particle_age += particle_age_factor; } From 1797bc41a6bdd9cf0ac0b4c350c60ce29ee4b39a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:19:58 +0100 Subject: [PATCH 110/172] 88.6%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 253e37c5a..7c32a0b7c 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -232,7 +232,7 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { particle->life = static_cast(life * 65535.0f); particle->color = particleColor; particle->length = static_cast(ld * 255.0f); - particle->width = static_cast(mEmitterDef.HeightStart()); + particle->width = static_cast(mEmitterDef.HeightStart() * 255.0f); particle->uv[0] = static_cast(mTextureUVs.StartU() * 255.0f); current_particle_age += particle_age_factor; From 06cba6e126f39109874a7751a5fa1635ba13a935 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 03:39:13 +0100 Subject: [PATCH 111/172] 88.4%: improve Periodic::UpdateForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index 076d218d5..bfd8f3fbe 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -57,7 +57,6 @@ int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & hand int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; - char *effectIDBase; int ret; memset(&force, 0, sizeof(force)); @@ -74,11 +73,10 @@ int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, un force.p.periodic.envelope.attackLevel = attackLevel; force.p.periodic.envelope.fadeLevel = fadeLevel; - effectIDBase = reinterpret_cast(this) + 0x80; - ret = LGUpdateForceEffect(*reinterpret_cast(effectIDBase + forceNumber * 4 + channel * 32), &force); + ret = LGUpdateForceEffect(*reinterpret_cast(reinterpret_cast(this) + 0x80 + forceNumber * 4 + channel * 32), &force); if (ret < 0) { OSReport(kUpdatePeriodicForceError, channel, ret); - *reinterpret_cast(effectIDBase + forceNumber * 4 + channel * 32) = + *reinterpret_cast(reinterpret_cast(this) + 0x80 + forceNumber * 4 + channel * 32) = static_cast(-1); } From 30fbea72aff454c72c833b08810c52f3d5d0baf2 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 04:00:03 +0100 Subject: [PATCH 112/172] 88.3%: improve eBuildSunPolyFix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 141 +++++++++++++------------ 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 611dc70d6..bb6d1dc17 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -116,90 +116,101 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float } void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { - float screen_width = static_cast(eGetScreenWidth()); - float half_size; - float sin_angle; - float cos_angle; - float diagonal0; - float diagonal1; - float intensity; - float center_x; - float center_y; - unsigned char alpha; + const float PixelFadeAmount = 28.0f; + const float PixelFadeLimit = 28.0f; + float screen_widthf = static_cast(eGetScreenWidth()); + float screen_heightf = static_cast(eGetScreenHeight()); + float layer_intensity = layer->Size; + float main_intensity; + float delta_center_x; + float delta_center_y; unsigned short angle; - - eGetScreenHeight(); - - if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { - max_size = layer->Size; + float max_sweep_angle = 65536.0f; + float scale_x = (x + max_size) / ((screen_widthf + max_size) + max_size); + float sweep_angle; + float rx; + float ry; + float angle_sin; + float angle_cos; + float dx; + float dy; + int a; + float lx; + float ly; + int r; + int g; + int b; + + (void)screen_heightf; + if (layer->Texture == SUNTEX_CENTER && max_size < layer_intensity) { + max_size = layer_intensity; } - half_size = layer->Size * 0.5f; - angle = static_cast( - layer->Angle + - static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) - ); - sin_angle = bSin(angle); - cos_angle = bCos(angle); + rx = layer_intensity * 0.5f; + sweep_angle = layer->SweepAngleAmount * (scale_x * max_sweep_angle); + angle = static_cast(layer->Angle + static_cast(sweep_angle)); + angle_sin = bSin(angle); + angle_cos = bCos(angle); sun_vis_poly_fix_ini[2] = 1.0f; poly->Vertices[1].z = 1.0f; poly->Vertices[2].z = sun_vis_poly_fix_ini[2]; poly->Vertices[3].z = sun_vis_poly_fix_ini[2]; - diagonal1 = half_size * sin_angle; - diagonal0 = half_size * cos_angle; - intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; - center_x = x + layer->OffsetX; - center_y = y + layer->OffsetY; - - if (intensity < 28.0f) { - alpha = static_cast(static_cast(intensity)); + ry = rx * angle_cos; + poly->Vertices[0].z = sun_vis_poly_fix_ini[2]; + rx *= angle_sin; + dx = ry - rx; + dy = rx + ry; + sun_vis_poly_fix_ini[6] = poly->Vertices[1].z; + sun_vis_poly_fix_ini[10] = poly->Vertices[2].z; + main_intensity = layer_intensity * SunVisibility * SunMaxIntensity; + sun_vis_poly_fix_ini[14] = poly->Vertices[3].z; + r = layer->Colour[0]; + delta_center_x = x + layer->OffsetX; + g = layer->Colour[1]; + delta_center_y = y + layer->OffsetY; + b = layer->Colour[2]; + if (PixelFadeLimit <= main_intensity) { + a = static_cast(main_intensity - PixelFadeAmount); } else { - alpha = static_cast(static_cast(intensity - 28.0f)); + a = static_cast(main_intensity); } - float sum = diagonal1 + diagonal0; - float diff = diagonal0 - diagonal1; - - poly->Vertices[3].x = center_x - diff; - poly->Vertices[3].y = center_y + sum; - poly->Vertices[0].y = center_y - diff; - sun_vis_poly_fix_ini[0] = center_x - sum; + poly->Vertices[3].x = delta_center_x - dx; + poly->Vertices[3].y = delta_center_y + dy; + poly->Vertices[0].y = delta_center_y - dx; + sun_vis_poly_fix_ini[0] = delta_center_x - dy; poly->Vertices[0].x = sun_vis_poly_fix_ini[0]; - poly->Vertices[1].y = center_y - sum; - poly->Vertices[1].x = center_x + diff; - poly->Vertices[2].y = center_y + diff; - poly->Vertices[2].x = center_x + sum; + poly->Vertices[1].y = delta_center_y - dy; + poly->Vertices[1].x = delta_center_x + dx; + poly->Vertices[2].y = delta_center_y + dx; + poly->Vertices[2].x = delta_center_x + dy; - sun_vis_poly_fix_ini[1] = poly->Vertices[0].y; sun_vis_poly_fix_ini[4] = poly->Vertices[1].x; - sun_vis_poly_fix_ini[5] = poly->Vertices[1].y; sun_vis_poly_fix_ini[8] = poly->Vertices[2].x; - sun_vis_poly_fix_ini[9] = poly->Vertices[2].y; sun_vis_poly_fix_ini[12] = poly->Vertices[3].x; + sun_vis_poly_fix_ini[1] = poly->Vertices[0].y; + sun_vis_poly_fix_ini[5] = poly->Vertices[1].y; + sun_vis_poly_fix_ini[9] = poly->Vertices[2].y; sun_vis_poly_fix_ini[13] = poly->Vertices[3].y; - unsigned char c0 = layer->Colour[0]; - unsigned char c1 = layer->Colour[1]; - unsigned char c2 = layer->Colour[2]; - - poly->Colours[0][0] = c0; - poly->Colours[0][1] = c1; - poly->Colours[0][2] = c2; - poly->Colours[0][3] = alpha; - poly->Colours[1][0] = c0; - poly->Colours[1][1] = c1; - poly->Colours[1][2] = c2; - poly->Colours[1][3] = alpha; - poly->Colours[2][0] = c0; - poly->Colours[2][1] = c1; - poly->Colours[2][2] = c2; - poly->Colours[2][3] = alpha; - poly->Colours[3][0] = c0; - poly->Colours[3][1] = c1; - poly->Colours[3][2] = c2; - poly->Colours[3][3] = alpha; + poly->Colours[0][0] = r; + poly->Colours[0][1] = g; + poly->Colours[0][2] = b; + poly->Colours[0][3] = a; + poly->Colours[1][0] = r; + poly->Colours[1][1] = g; + poly->Colours[1][2] = b; + poly->Colours[1][3] = a; + poly->Colours[2][0] = r; + poly->Colours[2][1] = g; + poly->Colours[2][2] = b; + poly->Colours[3][3] = a; + poly->Colours[3][0] = r; + poly->Colours[3][1] = g; + poly->Colours[3][2] = b; + poly->Colours[2][3] = a; } void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { From f3e0d9f698185b68a4600be7540630a1991d2d2d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 04:44:10 +0100 Subject: [PATCH 113/172] 88.2%: improve LGWheels::PlaySpringForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 33 ++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index d40f0e8b0..59c9d570f 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -339,47 +339,48 @@ void LGWheels::PlayAutoCalibAndSpringForce(long channel) { void LGWheels::PlaySpringForce(long channel, signed char offset, unsigned char saturation, short coefficient) { int ret; - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11B0) != 0) { return; } - if (LGWheelsGetWheels(this)->IsConnected(channel)) { - if (LGWheelsGetIsAirborne(this, channel)) { + if (reinterpret_cast(reinterpret_cast(this) + 0x828)->IsConnected(channel)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) != 0) { return; } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) != 0) { + if (reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Playing[channel][0] != 0) { if (SameSpringForceParams(channel, offset, saturation, coefficient)) { return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); if (ret < 0) { return; } - LGWheelsGetSpringForceParams(this)[channel].offset = offset; - LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; - LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].offset = offset; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; return; } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + if (reinterpret_cast(reinterpret_cast(this) + 0x11A8)->EffectID[channel][0] == static_cast(-1)) { + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8) + ->DownloadForce(channel, 0, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); } else if (SameSpringForceParams(channel, offset, saturation, coefficient)) { - LGWheelsGetCondition(this)->Start(channel, 0); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 0); return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 0, 7, static_cast(-1), 0, offset, 0, saturation, saturation, coefficient, coefficient); } if (ret >= 0) { - LGWheelsGetSpringForceParams(this)[channel].offset = offset; - LGWheelsGetSpringForceParams(this)[channel].saturation = saturation; - LGWheelsGetSpringForceParams(this)[channel].coefficient = coefficient; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].offset = offset; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; + reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; } - LGWheelsGetCondition(this)->Start(channel, 0); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 0); } else { OSReport(kPlayForceError, channel); } From 2ea73d122402cf92850d8f68ba10c1fa6a0447b7 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 04:46:43 +0100 Subject: [PATCH 114/172] 88.3%: improve LGWheels::PlayDamperForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 59c9d570f..446e422a9 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -450,43 +450,44 @@ bool LGWheels::SameConstantForceParams(long channel, short magnitude, unsigned s void LGWheels::PlayDamperForce(long channel, short coefficient) { int ret; - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11B0) != 0) { return; } - if (LGWheelsGetWheels(this)->IsConnected(channel)) { - if (LGWheelsGetIsAirborne(this, channel)) { + if (reinterpret_cast(reinterpret_cast(this) + 0x828)->IsConnected(channel)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) != 0) { return; } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) != 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11AC) != 0) { if (SameDamperForceParams(channel, coefficient)) { return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); if (ret < 0) { return; } - LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + *reinterpret_cast(reinterpret_cast(this) + channel * 2 + 0x169C) = coefficient; return; } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 1) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x122C) == static_cast(-1)) { + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8) + ->DownloadForce(channel, 1, *reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x1050), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); } else if (SameDamperForceParams(channel, coefficient)) { - LGWheelsGetCondition(this)->Start(channel, 1); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 1); return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 1, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, coefficient, coefficient); } if (ret >= 0) { - LGWheelsGetDamperForceParams(this)[channel].coefficient = coefficient; + *reinterpret_cast(reinterpret_cast(this) + channel * 2 + 0x169C) = coefficient; } - LGWheelsGetCondition(this)->Start(channel, 1); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 1); } else { OSReport(kPlayForceError, channel); } From 547bc0792c62b45dac90ddb153fbf3368e218bd0 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 04:52:47 +0100 Subject: [PATCH 115/172] 88.3%: improve LGWheels::PlaySlipperyRoadEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 31 ++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 446e422a9..528c4e56d 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -641,49 +641,50 @@ void LGWheels::PlaySlipperyRoadEffect(long channel, short magnitude) { if (IsPlaying(channel, 2)) { StopDamperForce(channel); - LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 1) = 0; - LGWheelsGetDamperWasPlaying(this, channel) = 1; + *reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11AC) = 0; + *reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x15AC) = 1; } if (IsPlaying(channel, 0)) { StopSpringForce(channel); - LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 0) = 0; - LGWheelsGetSpringWasPlaying(this, channel) = 1; + *reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11A8) = 0; + *reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x15BC) = 1; } - if (LGWheelsGetWheels(this)->IsConnected(channel)) { - if (LGWheelsGetIsAirborne(this, channel)) { + if (reinterpret_cast(reinterpret_cast(this) + 0x828)->IsConnected(channel)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) != 0) { return; } - if (LGWheelsGetPlaying(LGWheelsGetCondition(this), channel, 2) != 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x11B0) != 0) { if (SameSlipperyRoadEffectParams(channel, magnitude)) { return; } - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0, 0xFF, 0xFF, -magnitude, -magnitude); if (ret < 0) { return; } - LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + *reinterpret_cast(reinterpret_cast(this) + channel * 2 + 0x16CC) = magnitude; return; } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 2) == static_cast(-1)) { - ret = LGWheelsGetCondition(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x1230) == static_cast(-1)) { + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8) + ->DownloadForce(channel, 2, *reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x1050), 8, static_cast(-1), 0, 0, 0, 0xFF, 0xFF, -magnitude, -magnitude); } else if (SameSlipperyRoadEffectParams(channel, magnitude)) { - LGWheelsGetCondition(this)->Start(channel, 2); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 2); return; } else { - ret = LGWheelsGetCondition(this)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0xFF, 0xFF, 0xFF, -magnitude, -magnitude); + ret = reinterpret_cast(reinterpret_cast(this) + 0x11A8)->UpdateForce(channel, 2, 8, static_cast(-1), 0, 0, 0, 0xFF, 0xFF, -magnitude, -magnitude); } if (ret >= 0) { - LGWheelsGetSlipperyRoadParams(this)[channel].magnitude = magnitude; + *reinterpret_cast(reinterpret_cast(this) + channel * 2 + 0x16CC) = magnitude; } - LGWheelsGetCondition(this)->Start(channel, 2); + reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 2); } else { OSReport(kPlayForceError, channel); } From 8c4ea74c9b5e5cbc77d88cd3a4cbeeb7cd71c5e9 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:05:54 +0100 Subject: [PATCH 116/172] 88.4%: improve LGWheels::PlayAutoCalibAndSpringForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 528c4e56d..152a40742 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -323,13 +323,13 @@ bool LGWheels::PedalsConnected(long channel) { } void LGWheels::PlayAutoCalibAndSpringForce(long channel) { - if (LGWheelsGetWheels(this)->IsConnected(channel) && !LGWheelsGetIsAirborne(this, channel)) { - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 4) == static_cast(-1)) { + if (LGWheelsGetWheels(this)->IsConnected(channel) && *reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) == 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x1438) == static_cast(-1)) { LGWheelsGetPeriodic(this)->DownloadForce(channel, 4, LGWheelsGetWheelHandle(this, channel), 3, 2200, 0, 180, 90, 2200, 0, 0, 0, 0, 0, 0); LGWheelsGetPeriodic(this)->Start(channel, 4); } - if (LGWheelsGetEffectID(LGWheelsGetCondition(this), channel, 0) == static_cast(-1)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x1228) == static_cast(-1)) { LGWheelsGetCondition(this)->DownloadForce(channel, 0, LGWheelsGetWheelHandle(this, channel), 7, static_cast(-1), 2200, 0, 0, 180, 180, 180, 180); LGWheelsGetCondition(this)->Start(channel, 0); } From 404b70939a8c5d50b5026988e11a93fc993f5dcb Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:24:24 +0100 Subject: [PATCH 117/172] 88.5%: improve DVDErrorTask Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 125 ++++++++++-------- .../Src/Frontend/MemoryCard/MemoryCard.hpp | 4 + src/Speed/Indep/Src/World/TrackStreamer.hpp | 8 ++ 3 files changed, 79 insertions(+), 58 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index d758b2a8f..48b3b6c48 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -38,6 +38,11 @@ class FEObject; class cFEng { public: static cFEng *mInstance; + + static cFEng *Get() { + return mInstance; + } + void MakeLoadedPackagesDirty(); int IsPackagePushed(const char *); void PushErrorPackage(const char *, int, unsigned long); @@ -302,15 +307,7 @@ void DVDErrorTask(void *, int) { int errorState = 0; unsigned int nextFrame; int driveStatus; - int movieWasPlaying; - long ch; - int textLen; - int scrollLen; - int buttonMask; - cFEng *feng; const char *pkgName; - char textBuf[16]; - PADStatus padBuf[4]; do { IOModule::GetIOModule().Update(); @@ -353,37 +350,40 @@ void DVDErrorTask(void *, int) { } nextFrame = frame + 1; - if (MemoryCard::s_pThis != 0) { - MemoryCard::s_pThis->Tick(16); + if (MemoryCard::GetInstance() != 0) { + MemoryCard::GetInstance()->Tick(16); } goto loop_end; } if (errorState != 0) { + unsigned long MotorRumble[4]; + long port; + /* Error state active - run sync tasks and handle input */ bSyncTaskRun(); - if (MemoryCard::s_pThis != 0) { - MemoryCard::s_pThis->Tick(16); + if (MemoryCard::GetInstance() != 0) { + MemoryCard::GetInstance()->Tick(16); } DVDCheckDisk(); - bMemSet(textBuf, 0, 16); - *(u32 *)&textBuf[0] = 2; - *(u32 *)&textBuf[4] = 2; - *(u32 *)&textBuf[8] = 2; - *(u32 *)&textBuf[12] = 2; - PADControlAllMotors((const u32 *)textBuf); + bMemSet(MotorRumble, 0, 16); + MotorRumble[0] = 2; + MotorRumble[1] = 2; + MotorRumble[2] = 2; + MotorRumble[3] = 2; + PADControlAllMotors(MotorRumble); LGWheels_ReadAll(plat_lgwheels); - for (ch = 0; ch <= 3; ch++) { - if (LGWheels_IsConnected(plat_lgwheels, ch)) { - LGWheels_StopConstantForce(plat_lgwheels, ch); - LGWheels_StopSurfaceEffect(plat_lgwheels, ch); - LGWheels_StopDamperForce(plat_lgwheels, ch); - LGWheels_StopCarAirborne(plat_lgwheels, ch); - LGWheels_StopSlipperyRoadEffect(plat_lgwheels, ch); - LGWheels_PlaySpringForce(plat_lgwheels, ch, - *(signed char *)((char *)plat_lgwheels + ch * 10 + 3), + for (port = 0; port <= 3; port++) { + if (LGWheels_IsConnected(plat_lgwheels, port)) { + LGWheels_StopConstantForce(plat_lgwheels, port); + LGWheels_StopSurfaceEffect(plat_lgwheels, port); + LGWheels_StopDamperForce(plat_lgwheels, port); + LGWheels_StopCarAirborne(plat_lgwheels, port); + LGWheels_StopSlipperyRoadEffect(plat_lgwheels, port); + LGWheels_PlaySpringForce(plat_lgwheels, port, + *(signed char *)((char *)plat_lgwheels + port * 10 + 3), 0xb4, 0xb4); } } @@ -401,9 +401,9 @@ void DVDErrorTask(void *, int) { driveStatus = DVDGetDriveStatus(); if (driveStatus != -1 && resetMode != -1) { - int mode = resetMode; + int reset_mode = resetMode; resetMode = -1; - CheckReset(mode); + CheckReset(reset_mode); } /* Map drive status to error index */ @@ -444,9 +444,10 @@ void DVDErrorTask(void *, int) { g_pEAXSound->Update(0.1f); } + cFEng *feng = cFEng::Get(); pkgName = "DiscError.fng"; - if (!cFEng::mInstance->IsPackagePushed(pkgName)) { - cFEng::mInstance->PushErrorPackage(pkgName, 0, 0xff); + if (!feng->IsPackagePushed(pkgName)) { + feng->PushErrorPackage(pkgName, 0, 0xff); } nextFrame = frame + 1; @@ -459,8 +460,7 @@ void DVDErrorTask(void *, int) { /* Disc error was active, check if we should service streaming */ nextFrame = frame + 1; - if (!(TheTrackStreamer.UserMemoryAllocationSize > 0) && - (TheTrackStreamer.LoadingPhase != TrackStreamer::LOADING_IDLE)) { + if (!TheTrackStreamer.HasUserMemoryAllocations() && TheTrackStreamer.IsLoadingInProgressNonRepeatable()) { ServiceResourceLoading(); driveStatus = 1; TheTrackStreamer.ServiceNonGameState(); @@ -469,13 +469,16 @@ void DVDErrorTask(void *, int) { if (driveStatus != 0) { /* Scrolling text display */ - scrollLen = (signed char)bStrLen( + char the_loading_text[16]; + int scrollLen = (signed char)bStrLen( s_OpenCover_ErrorText[language][errorIndex]); + int buttonMask = 0x10; + int to_copy; + char copy_length; - bMemSet(textBuf, 0, 16); + bMemSet(the_loading_text, 0, 16); - buttonMask = 0x10; - if (TheGameFlowManager.GetState() == GAMEFLOW_STATE_RACING) { + if (IsGameFlowInGame()) { buttonMask = 0x40; } @@ -491,21 +494,22 @@ void DVDErrorTask(void *, int) { scrollIndex = scrollIndex + 1; } - bStrNCpy(textBuf, + to_copy = scrollLen - scrollOffset; + bStrNCpy(the_loading_text, s_OpenCover_ErrorText[language][errorIndex], - scrollLen - scrollOffset); + to_copy); nextFrame = frame + 1; - textLen = bStrLen(textBuf); - while (textLen <= scrollLen) { - bStrCat(textBuf, textBuf, " "); - textLen = textLen + 1; + copy_length = static_cast(bStrLen(the_loading_text)); + while (copy_length <= scrollLen) { + bStrCat(the_loading_text, the_loading_text, " "); + copy_length = copy_length + 1; } - FEPrintf("DiscError.fng", 0xEEFFD04F, textBuf); + FEPrintf("DiscError.fng", 0xEEFFD04F, the_loading_text); - if (MemoryCard::s_pThis != 0) { - MemoryCard::s_pThis->Tick(16); + if (MemoryCard::GetInstance() != 0) { + MemoryCard::GetInstance()->Tick(16); } } else { /* Error resolved */ @@ -519,19 +523,20 @@ void DVDErrorTask(void *, int) { g_pEAXSound->Update(0.1f); } - movieWasPlaying = 0; + bool wasMovieActive = false; + cFEng *feng = cFEng::Get(); if (gMoviePlayer != 0) { - movieWasPlaying = 1; + wasMovieActive = true; gMoviePlayer->Stop(); } - cFEng::mInstance->MakeLoadedPackagesDirty(); - if (cFEng::mInstance->IsPackagePushed("DiscError.fng")) { - cFEng::mInstance->PopErrorPackage(); + feng->MakeLoadedPackagesDirty(); + if (feng->IsPackagePushed("DiscError.fng")) { + feng->PopErrorPackage(); } nextFrame = frame + 1; - if (movieWasPlaying) { - cFEng::mInstance->QueueGameMessage(0xC3960EB9, 0, 0xff); + if (wasMovieActive) { + feng->QueueGameMessage(0xC3960EB9, 0, 0xff); } } } @@ -550,12 +555,16 @@ void DVDErrorTask(void *, int) { HardwarePadStatus[0].button = *(u16 *)((char *)plat_lgwheels); HardwarePadStatus[1].button = *(u16 *)((char *)plat_lgwheels + 10); } else { - PADRead(padBuf); - if (padBuf[0].err == 0) { - bMemCpy(&HardwarePadStatus[0], &padBuf[0], 0xc); + PADStatus LocalHardwarePadStatus[4]; + int pad_state_0; + + PADRead(LocalHardwarePadStatus); + pad_state_0 = LocalHardwarePadStatus[0].err; + if (pad_state_0 == 0) { + bMemCpy(&HardwarePadStatus[0], &LocalHardwarePadStatus[0], 0xc); } - if (padBuf[1].err == 0) { - bMemCpy(&HardwarePadStatus[1], &padBuf[1], 0xc); + if (LocalHardwarePadStatus[1].err == 0) { + bMemCpy(&HardwarePadStatus[1], &LocalHardwarePadStatus[1], 0xc); } } } diff --git a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp index 8599f5122..5095431b4 100644 --- a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp +++ b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp @@ -16,6 +16,10 @@ class MemoryCard { void Tick(int); + static MemoryCard *GetInstance() { + return s_pThis; + } + static MemoryCard *s_pThis; static int IsCardBusy(); }; diff --git a/src/Speed/Indep/Src/World/TrackStreamer.hpp b/src/Speed/Indep/Src/World/TrackStreamer.hpp index 5edf1f2b6..f5787dcae 100644 --- a/src/Speed/Indep/Src/World/TrackStreamer.hpp +++ b/src/Speed/Indep/Src/World/TrackStreamer.hpp @@ -162,6 +162,14 @@ class TrackStreamer { return CurrentVisibleSectionTable.IsSet(section_number); } + bool HasUserMemoryAllocations() { + return UserMemoryAllocationSize > 0; + } + + bool IsLoadingInProgressNonRepeatable() { + return LoadingPhase != LOADING_IDLE; + } + TrackStreamingSection *pTrackStreamingSections; // offset 0x0, size 0x4 int NumTrackStreamingSections; // offset 0x4, size 0x4 DiscBundleSection *pDiscBundleSections; // offset 0x8, size 0x4 From 4174ce63f3bb19826ffcf54db94b367f896c3f3a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:28:33 +0100 Subject: [PATCH 118/172] 88.5%: improve DVDErrorTask Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 48b3b6c48..852797d6a 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -484,12 +484,13 @@ void DVDErrorTask(void *, int) { if ((frame & buttonMask) != (prevButtons & buttonMask)) { int rem; - prevButtons = frame; + rem = scrollIndex; if (scrollIndex < 0) { rem = scrollIndex + 3; } rem = rem & ~3; + prevButtons = frame; scrollOffset = (signed char)(3 - (scrollIndex - rem)); scrollIndex = scrollIndex + 1; } @@ -502,8 +503,8 @@ void DVDErrorTask(void *, int) { nextFrame = frame + 1; copy_length = static_cast(bStrLen(the_loading_text)); while (copy_length <= scrollLen) { - bStrCat(the_loading_text, the_loading_text, " "); copy_length = copy_length + 1; + bStrCat(the_loading_text, the_loading_text, " "); } FEPrintf("DiscError.fng", 0xEEFFD04F, the_loading_text); From 10886e027c50cafaf474576420fffb408454b8cb Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:29:37 +0100 Subject: [PATCH 119/172] 88.5%: improve DVDErrorTask Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 852797d6a..7677a3916 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -470,14 +470,16 @@ void DVDErrorTask(void *, int) { if (driveStatus != 0) { /* Scrolling text display */ char the_loading_text[16]; - int scrollLen = (signed char)bStrLen( - s_OpenCover_ErrorText[language][errorIndex]); - int buttonMask = 0x10; + int scrollLen; + int buttonMask; int to_copy; char copy_length; + scrollLen = (signed char)bStrLen( + s_OpenCover_ErrorText[language][errorIndex]); bMemSet(the_loading_text, 0, 16); + buttonMask = 0x10; if (IsGameFlowInGame()) { buttonMask = 0x40; } @@ -525,12 +527,12 @@ void DVDErrorTask(void *, int) { } bool wasMovieActive = false; - cFEng *feng = cFEng::Get(); if (gMoviePlayer != 0) { wasMovieActive = true; gMoviePlayer->Stop(); } + cFEng *feng = cFEng::Get(); feng->MakeLoadedPackagesDirty(); if (feng->IsPackagePushed("DiscError.fng")) { feng->PopErrorPackage(); From cafea00123358722dcf095fd8a2183f93be1e567 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:40:40 +0100 Subject: [PATCH 120/172] 88.5%: dwarf improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 12af494ab..b5a85b604 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -116,12 +116,12 @@ unsigned int IsWheelActiveForProgressiveMenu(int ix) { } int ActualReadJoystickData() { - int nNewTop; - int port; - if (JoystickInitialized) { - nNewTop = (JoystickRingBufferTop + 1) & 0x1F; + int nNewTop = (JoystickRingBufferTop + 1) & 0x1F; + if (nNewTop != JoystickRingBufferBottom) { + int port; + PADRead(HardwarePadStatus); PADClamp(HardwarePadStatus); plat_lgwheels->ReadAll(); From 236d2d09b7be1aa54ce5a565bf33bb2e6230be68 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:47:13 +0100 Subject: [PATCH 121/172] 88.6%: improve LGWheels::PlaySurfaceEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 57 +++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 152a40742..e61a66463 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -701,62 +701,67 @@ bool LGWheels::SameSlipperyRoadEffectParams(long channel, short magnitude) { void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { int ret; - if (LGWheelsGetWheels(this)->IsConnected(channel)) { - if (LGWheelsGetIsAirborne(this, channel)) { + if (reinterpret_cast(reinterpret_cast(this) + 0x828)->IsConnected(channel)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) != 0) { return; } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 3) != 0) { + if (reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Playing[channel][3] != 0) { if (SameSurfaceEffectParams(channel, type, magnitude, period)) { return; } - if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { - LGWheelsGetPeriodic(this)->Destroy(channel, 3); - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); - LGWheelsGetPeriodic(this)->Start(channel, 3); + if (type != reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type) { + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Destroy(channel, 3); + ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) + ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) + ->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); } if (ret >= 0) { - LGWheelsGetSurfaceEffectParams(this)[channel].type = type; - LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; - LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } return; } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 3) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + if (reinterpret_cast(reinterpret_cast(this) + 0x13A8)->EffectID[channel][3] == static_cast(-1)) { + ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) + ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); if (ret >= 0) { - LGWheelsGetSurfaceEffectParams(this)[channel].type = type; - LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; - LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } - LGWheelsGetPeriodic(this)->Start(channel, 3); + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); return; } if (SameSurfaceEffectParams(channel, type, magnitude, period)) { - LGWheelsGetPeriodic(this)->Start(channel, 3); + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); return; } - if (type != LGWheelsGetSurfaceEffectParams(this)[channel].type) { - LGWheelsGetPeriodic(this)->Destroy(channel, 3); - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 3, LGWheelsGetWheelHandle(this, channel), type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + if (type != reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type) { + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Destroy(channel, 3); + ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) + ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); + ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) + ->UpdateForce(channel, 3, type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); } if (ret >= 0) { - LGWheelsGetSurfaceEffectParams(this)[channel].type = type; - LGWheelsGetSurfaceEffectParams(this)[channel].magnitude = magnitude; - LGWheelsGetSurfaceEffectParams(this)[channel].period = period; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } - LGWheelsGetPeriodic(this)->Start(channel, 3); + reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); } else { OSReport(kPlayForceError, channel); } From 3318cfa709bccba573fb19110c5d3a9c3794eb0c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:50:20 +0100 Subject: [PATCH 122/172] 88.6%: improve LGWheels::PlaySurfaceEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index e61a66463..76171f806 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -722,8 +722,8 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char } if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } return; @@ -733,8 +733,8 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); @@ -756,8 +756,8 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char } if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; } From 75c3073c7b23ec4be0933957babf0470dcc9962e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:55:08 +0100 Subject: [PATCH 123/172] 88.8%: improve LGWheels::PlaySurfaceEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 76171f806..83bd3252c 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -706,7 +706,7 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char return; } - if (reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Playing[channel][3] != 0) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x13B4) != 0) { if (SameSurfaceEffectParams(channel, type, magnitude, period)) { return; } @@ -729,7 +729,7 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char return; } - if (reinterpret_cast(reinterpret_cast(this) + 0x13A8)->EffectID[channel][3] == static_cast(-1)) { + if (*reinterpret_cast(reinterpret_cast(this) + channel * 0x20 + 0x1434) == static_cast(-1)) { ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); if (ret >= 0) { From d31b357aeca5c0f57fd2a732e51a592394364d92 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 05:56:45 +0100 Subject: [PATCH 124/172] 88.8%: improve LGWheels::PlaySurfaceEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 83bd3252c..7d11a1287 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -699,7 +699,7 @@ bool LGWheels::SameSlipperyRoadEffectParams(long channel, short magnitude) { } void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { - int ret; + int ret = 0; if (reinterpret_cast(reinterpret_cast(this) + 0x828)->IsConnected(channel)) { if (*reinterpret_cast(reinterpret_cast(this) + channel * 4 + 0x166C) != 0) { From 91c4b274226bb40055ee94196a13142aa9aa4316 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:17:17 +0100 Subject: [PATCH 125/172] 88.8%: dwarf improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 7c32a0b7c..28930c1fb 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -232,8 +232,8 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { particle->life = static_cast(life * 65535.0f); particle->color = particleColor; particle->length = static_cast(ld * 255.0f); - particle->width = static_cast(mEmitterDef.HeightStart() * 255.0f); particle->uv[0] = static_cast(mTextureUVs.StartU() * 255.0f); + particle->width = static_cast(mEmitterDef.HeightStart() * 255.0f); current_particle_age += particle_age_factor; } From b2f0da6d070fc4dd1e539a14f1b7f8fdc671b240 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:18:28 +0100 Subject: [PATCH 126/172] 88.8%: dwarf improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 28930c1fb..d8f2d7f41 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -164,14 +164,14 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { random_seed = randomSeed; life = mEmitterDef.Life(); life_variance = life * mEmitterDef.LifeVariance(); - num_particles = intensity * mEmitterDef.NumParticles(); - num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; - current_particle_age = 0.0f; r = static_cast(mEmitterDef.Colour1().x * 255.0f); g = static_cast(mEmitterDef.Colour1().y * 255.0f); b = static_cast(mEmitterDef.Colour1().z * 255.0f); a = static_cast(mEmitterDef.Colour1().w * 255.0f); particleColor = a << 24 | b << 16 | g << 8 | r; + num_particles = intensity * mEmitterDef.NumParticles(); + num_particles_variance = num_particles * mEmitterDef.NumParticlesVariance() * 100.0f; + current_particle_age = 0.0f; life -= life_variance; num_particles -= num_particles_variance; From a10de59e36a1538543a88a8572bd5fa34a7c4d75 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:22:43 +0100 Subject: [PATCH 127/172] 88.8%: improve CGEmitter::SpawnParticles Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index d8f2d7f41..2c05cc4f2 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -193,7 +193,8 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { break; } - sparkLength = mEmitterDef.LengthStart() + bRandom(mEmitterDef.LengthDelta(), &random_seed); + sparkLength = mEmitterDef.LengthStart(); + sparkLength += bRandom(mEmitterDef.LengthDelta(), &random_seed); if (sparkLength < 0.0f) { break; } From 06aa8d5eb0717dabe019ba2c2316b035f0ce6011 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:43:46 +0100 Subject: [PATCH 128/172] 89.0%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index b5a85b604..634dd9d8e 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -50,10 +50,10 @@ static inline unsigned short ConvertPadButtons(unsigned short buttons) { result |= (buttons >> 8) & 8; result |= buttons & 0x10; result |= (buttons >> 7) & 0x20; - result |= (buttons << 5) & 0x100; - result |= (buttons << 7) & 0x200; + result |= (buttons & 8) << 5; + result |= (buttons & 4) << 7; result |= (buttons & 1) << 10; - result |= (buttons << 10) & 0x800; + result |= (buttons & 2) << 10; return ~result; } @@ -142,8 +142,7 @@ int ActualReadJoystickData() { ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); joy_data->ThePadData[0].AnalogLeftX = ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); - joy_data->ThePadData[0].AnalogLeftY = - ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f)); + joy_data->ThePadData[0].AnalogLeftY = 0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f); joy_data->ThePadData[0].LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); joy_data->ThePadData[0].RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); } else if (plat_lgwheels->IsConnected(port)) { From c609b6fcf820c9eebc18ef338a27fbb18dc85488 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:47:26 +0100 Subject: [PATCH 129/172] 89.1%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 38 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 634dd9d8e..f79bb94c1 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -128,6 +128,7 @@ int ActualReadJoystickData() { for (port = 0; port <= 3; port++) { JoyData *joy_data = &PadRingData[port][JoystickRingBufferTop]; + short data; bMemSet(joy_data, 0xFF, sizeof(JoyData)); @@ -135,7 +136,11 @@ int ActualReadJoystickData() { joy_data->padSTATUS = HardwarePadStatus[port]; joy_data->ThePadData[0].Type = 0x41; joy_data->ThePadData[0].Error = 0; - joy_data->ThePadData[0].DigitalButtons = ConvertPadButtons(joy_data->padSTATUS.button); + data = joy_data->padSTATUS.button; + joy_data->ThePadData[0].DigitalButtons = + ~((data >> 8) & 1 | (data >> 8) & 2 | (data >> 8) & 4 | (data >> 8) & 8 | data & 0x10 | + (data >> 7) & 0x20 | (data & 8) << 5 | (data & 4) << 7 | (data & 1) << 10 | + (data & 2) << 10); joy_data->ThePadData[0].AnalogRightX = ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); joy_data->ThePadData[0].AnalogRightY = @@ -146,8 +151,6 @@ int ActualReadJoystickData() { joy_data->ThePadData[0].LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); joy_data->ThePadData[0].RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); } else if (plat_lgwheels->IsConnected(port)) { - const LGPosition *wheel_position; - if (!wasWheelConnected[port]) { wasWheelConnected[port] = 1; calibrationTimer[port] = 7.0f; @@ -166,25 +169,34 @@ int ActualReadJoystickData() { calibrationTimer[port] -= elapsed; } - wheel_position = &reinterpret_cast(plat_lgwheels)[port]; - joy_data->padSTATUS.button = wheel_position->button; - HardwarePadStatus[port].button = wheel_position->button; + joy_data->padSTATUS.button = reinterpret_cast(plat_lgwheels)[port].button; + HardwarePadStatus[port].button = reinterpret_cast(plat_lgwheels)[port].button; joy_data->ThePadData[0].Error = 0; - joy_data->ThePadData[0].Type = plat_lgwheels->PedalsConnected(port) ? 0x51 : 0x50; - joy_data->ThePadData[0].DigitalButtons = ConvertPadButtons(wheel_position->button); + if (plat_lgwheels->PedalsConnected(port)) { + joy_data->ThePadData[0].Type = 0x51; + } else { + joy_data->ThePadData[0].Type = 0x50; + } + data = reinterpret_cast(plat_lgwheels)[port].button; + joy_data->ThePadData[0].DigitalButtons = + ~((data >> 8) & 1 | (data >> 8) & 2 | (data >> 8) & 4 | (data >> 8) & 8 | data & 0x10 | + (data >> 7) & 0x20 | (data & 8) << 5 | (data & 4) << 7 | (data & 1) << 10 | + (data & 2) << 10); joy_data->ThePadData[0].AnalogRightX = 0; - joy_data->ThePadData[0].AnalogLeftX = wheel_position->wheel + 0x80; + joy_data->ThePadData[0].AnalogLeftX = + reinterpret_cast(plat_lgwheels)[port].wheel + 0x80; if (plat_lgwheels->PedalsConnected(port)) { - joy_data->ThePadData[0].AnalogRightY = wheel_position->accelerator; - joy_data->ThePadData[0].AnalogLeftY = wheel_position->brake; + joy_data->ThePadData[0].AnalogRightY = + reinterpret_cast(plat_lgwheels)[port].accelerator; + joy_data->ThePadData[0].AnalogLeftY = reinterpret_cast(plat_lgwheels)[port].brake; } else { joy_data->ThePadData[0].AnalogLeftY = 0; joy_data->ThePadData[0].AnalogRightY = 0; } - joy_data->ThePadData[0].LTrigger = wheel_position->triggerLeft; - joy_data->ThePadData[0].RTrigger = wheel_position->triggerRight; + joy_data->ThePadData[0].LTrigger = reinterpret_cast(plat_lgwheels)[port].triggerLeft; + joy_data->ThePadData[0].RTrigger = reinterpret_cast(plat_lgwheels)[port].triggerRight; } else { joy_data->ThePadData[0].Type = 0xFF; wasWheelConnected[port] = 0; From d55099ed3f2cdd0bcdf82a66968ca5d8a858d002 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 06:52:09 +0100 Subject: [PATCH 130/172] 89.3%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index f79bb94c1..a12dda3ba 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -141,12 +141,30 @@ int ActualReadJoystickData() { ~((data >> 8) & 1 | (data >> 8) & 2 | (data >> 8) & 4 | (data >> 8) & 8 | data & 0x10 | (data >> 7) & 0x20 | (data & 8) << 5 | (data & 4) << 7 | (data & 1) << 10 | (data & 2) << 10); - joy_data->ThePadData[0].AnalogRightX = - ClampAnalogValue(static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80); - joy_data->ThePadData[0].AnalogRightY = - ClampAnalogValue(0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f)); - joy_data->ThePadData[0].AnalogLeftX = - ClampAnalogValue(static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80); + data = static_cast(joy_data->padSTATUS.substickX * 2.15f) + 0x80; + if (data & 0x8000) { + data = 0; + } + if (data > 0xFF) { + data = 0xFF; + } + joy_data->ThePadData[0].AnalogRightX = data; + data = 0x80 - static_cast(joy_data->padSTATUS.substickY * 2.15f); + if (data & 0x8000) { + data = 0; + } + if (data > 0xFF) { + data = 0xFF; + } + joy_data->ThePadData[0].AnalogRightY = data; + data = static_cast(joy_data->padSTATUS.stickX * 1.75f) + 0x80; + if (data & 0x8000) { + data = 0; + } + if (data > 0xFF) { + data = 0xFF; + } + joy_data->ThePadData[0].AnalogLeftX = data; joy_data->ThePadData[0].AnalogLeftY = 0x80 - static_cast(joy_data->padSTATUS.stickY * 1.75f); joy_data->ThePadData[0].LTrigger = static_cast(joy_data->padSTATUS.triggerL * 1.7f); joy_data->ThePadData[0].RTrigger = static_cast(joy_data->padSTATUS.triggerR * 1.7f); From d1a7ba70c5504ab17c5ac2a9f8695f38ff55c169 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 07:08:57 +0100 Subject: [PATCH 131/172] 89.3%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index a12dda3ba..410b6ec1a 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -202,7 +202,7 @@ int ActualReadJoystickData() { (data & 2) << 10); joy_data->ThePadData[0].AnalogRightX = 0; joy_data->ThePadData[0].AnalogLeftX = - reinterpret_cast(plat_lgwheels)[port].wheel + 0x80; + reinterpret_cast(plat_lgwheels)[port].wheel - 0x80; if (plat_lgwheels->PedalsConnected(port)) { joy_data->ThePadData[0].AnalogRightY = From f5b816a3a35ca28f2485ab15567d0e1f7eab7139 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 07:10:39 +0100 Subject: [PATCH 132/172] 89.3%: improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 410b6ec1a..64bcaf682 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -209,8 +209,8 @@ int ActualReadJoystickData() { reinterpret_cast(plat_lgwheels)[port].accelerator; joy_data->ThePadData[0].AnalogLeftY = reinterpret_cast(plat_lgwheels)[port].brake; } else { - joy_data->ThePadData[0].AnalogLeftY = 0; joy_data->ThePadData[0].AnalogRightY = 0; + joy_data->ThePadData[0].AnalogLeftY = 0; } joy_data->ThePadData[0].LTrigger = reinterpret_cast(plat_lgwheels)[port].triggerLeft; From 830f07a1e82057d473e53e773a65793834806f5d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 07:19:38 +0100 Subject: [PATCH 133/172] 89.4%: improve LGWheels::PlayDirtRoadEffect and LGWheels::PlayBumpyRoadEffect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 34 ++++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 7d11a1287..3eb1cb5e6 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -540,18 +540,19 @@ bool LGWheels::SameFrontalCollisionForceParams(long channel, short magnitude) { void LGWheels::PlayDirtRoadEffect(long channel, unsigned char magnitude) { int ret; + Periodic *periodic = LGWheelsGetPeriodic(this); if (LGWheelsGetWheels(this)->IsConnected(channel)) { if (LGWheelsGetIsAirborne(this, channel)) { return; } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 1) != 0) { + if (LGWheelsGetPlaying(periodic, channel, 1) != 0) { if (SameDirtRoadEffectParams(channel, magnitude)) { return; } - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + ret = periodic->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); if (ret < 0) { return; } @@ -560,20 +561,22 @@ void LGWheels::PlayDirtRoadEffect(long channel, unsigned char magnitude) { return; } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 1) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 1, LGWheelsGetWheelHandle(this, channel), 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + if (LGWheelsGetEffectID(periodic, channel, 1) == static_cast(-1)) { + ret = periodic->DownloadForce( + channel, 1, LGWheelsGetWheelHandle(this, channel), 2, static_cast(-1), 0, magnitude, 90, + 65, 0, 0, 0, 0, 0, 0); } else if (SameDirtRoadEffectParams(channel, magnitude)) { - LGWheelsGetPeriodic(this)->Start(channel, 1); + periodic->Start(channel, 1); return; } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); + ret = periodic->UpdateForce(channel, 1, 2, static_cast(-1), 0, magnitude, 90, 65, 0, 0, 0, 0, 0, 0); } if (ret >= 0) { LGWheelsGetDirtRoadParams(this)[channel].magnitude = magnitude; } - LGWheelsGetPeriodic(this)->Start(channel, 1); + periodic->Start(channel, 1); } else { OSReport(kPlayForceError, channel); } @@ -589,18 +592,19 @@ bool LGWheels::SameDirtRoadEffectParams(long channel, short magnitude) { void LGWheels::PlayBumpyRoadEffect(long channel, unsigned char magnitude) { int ret; + Periodic *periodic = LGWheelsGetPeriodic(this); if (LGWheelsGetWheels(this)->IsConnected(channel)) { if (LGWheelsGetIsAirborne(this, channel)) { return; } - if (LGWheelsGetPlaying(LGWheelsGetPeriodic(this), channel, 2) != 0) { + if (LGWheelsGetPlaying(periodic, channel, 2) != 0) { if (SameBumpyRoadEffectParams(channel, magnitude)) { return; } - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + ret = periodic->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); if (ret < 0) { return; } @@ -609,20 +613,22 @@ void LGWheels::PlayBumpyRoadEffect(long channel, unsigned char magnitude) { return; } - if (LGWheelsGetEffectID(LGWheelsGetPeriodic(this), channel, 2) == static_cast(-1)) { - ret = LGWheelsGetPeriodic(this)->DownloadForce(channel, 2, LGWheelsGetWheelHandle(this, channel), 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + if (LGWheelsGetEffectID(periodic, channel, 2) == static_cast(-1)) { + ret = periodic->DownloadForce( + channel, 2, LGWheelsGetWheelHandle(this, channel), 3, static_cast(-1), 0, magnitude, + 90, 100, 0, 0, 0, 0, 0, 0); } else if (SameBumpyRoadEffectParams(channel, magnitude)) { - LGWheelsGetPeriodic(this)->Start(channel, 2); + periodic->Start(channel, 2); return; } else { - ret = LGWheelsGetPeriodic(this)->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); + ret = periodic->UpdateForce(channel, 2, 3, static_cast(-1), 0, magnitude, 90, 100, 0, 0, 0, 0, 0, 0); } if (ret >= 0) { LGWheelsGetBumpyRoadParams(this)[channel].magnitude = magnitude; } - LGWheelsGetPeriodic(this)->Start(channel, 2); + periodic->Start(channel, 2); } else { OSReport(kPlayForceError, channel); } From 40483504886727e9e72c7147cd73b26b575db996 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 07:26:53 +0100 Subject: [PATCH 134/172] 89.8%: improve Periodic::UpdateForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index bfd8f3fbe..eee42373f 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -58,26 +58,29 @@ int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & hand int Periodic::UpdateForce(long channel, long forceNumber, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; + int slot; + unsigned long *base; memset(&force, 0, sizeof(force)); + slot = forceNumber * 4 + channel * 32; + base = reinterpret_cast(reinterpret_cast(this) + 0x80); force.type = type; + force.p.periodic.offset = offset; force.duration = duration; force.startDelay = startDelay; force.p.periodic.magnitude = magnitude; force.p.periodic.direction = direction; force.p.periodic.period = period; force.p.periodic.phase = phase; - force.p.periodic.offset = offset; force.p.periodic.envelope.attackTime = attackTime; force.p.periodic.envelope.fadeTime = fadeTime; force.p.periodic.envelope.attackLevel = attackLevel; force.p.periodic.envelope.fadeLevel = fadeLevel; - ret = LGUpdateForceEffect(*reinterpret_cast(reinterpret_cast(this) + 0x80 + forceNumber * 4 + channel * 32), &force); + ret = LGUpdateForceEffect(*reinterpret_cast(reinterpret_cast(base) + slot), &force); if (ret < 0) { OSReport(kUpdatePeriodicForceError, channel, ret); - *reinterpret_cast(reinterpret_cast(this) + 0x80 + forceNumber * 4 + channel * 32) = - static_cast(-1); + *reinterpret_cast(reinterpret_cast(base) + slot) = static_cast(-1); } return ret; From a20e40452103b08a466bd8ebe34852ce44cc8464 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 07:35:49 +0100 Subject: [PATCH 135/172] 89.9%: improve Periodic::DownloadForce Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Periodic.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Periodic.cpp b/src/Speed/GameCube/Src/Logitech/Periodic.cpp index eee42373f..4cd8b2812 100644 --- a/src/Speed/GameCube/Src/Logitech/Periodic.cpp +++ b/src/Speed/GameCube/Src/Logitech/Periodic.cpp @@ -22,9 +22,13 @@ Periodic::Periodic() : Force() {} int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & handle, unsigned char type, unsigned long duration, unsigned long startDelay, unsigned char magnitude, unsigned short direction, unsigned short period, unsigned short phase, short offset, unsigned long attackTime, unsigned long fadeTime, unsigned char attackLevel, unsigned char fadeLevel) { LGForceEffect force; int ret; + int slot; + unsigned long *effectId; + slot = forceNumber * 4 + channel * 32; + effectId = reinterpret_cast(reinterpret_cast(this) + 0x80 + slot); ret = 0; - if (PeriodicGetEffectID(this, channel, forceNumber) != static_cast(-1)) { + if (*effectId != static_cast(-1)) { Destroy(channel, forceNumber); } @@ -43,10 +47,10 @@ int Periodic::DownloadForce(long channel, long forceNumber, unsigned long & hand force.p.periodic.envelope.attackLevel = attackLevel; force.p.periodic.envelope.fadeLevel = fadeLevel; - ret = LGDownloadForceEffect(handle, &PeriodicGetEffectID(this, channel, forceNumber), &force); + ret = LGDownloadForceEffect(handle, effectId, &force); if (ret < 0) { OSReport(kDownloadPeriodicForceError, channel, ret); - PeriodicGetEffectID(this, channel, forceNumber) = static_cast(-1); + *effectId = static_cast(-1); } } else { OSReport(kDownloadPeriodicForceInvalidWheel, channel); From 888399cc42190bbf3d4c9dd0d7294cdb309cde1e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 08:09:03 +0100 Subject: [PATCH 136/172] 89.9%: improve DVDErrorTask Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 7677a3916..08751614e 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -450,9 +450,9 @@ void DVDErrorTask(void *, int) { feng->PushErrorPackage(pkgName, 0, 0xff); } - nextFrame = frame + 1; FEPrintf(pkgName, 0xEEFFD04F, s_OpenCover_ErrorText[language][errorIndex]); + nextFrame = frame + 1; } else if (g_discErrorOccured == 0) { nextFrame = frame + 1; goto loop_end; From c782934507bfc09fef74f3ccb902a31467273caa Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 08:45:12 +0100 Subject: [PATCH 137/172] 89.9%: improve Wheels helper readers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 3 ++- src/Speed/GameCube/Src/Logitech/Wheels.cpp | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 64bcaf682..a3fcec2c7 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -100,7 +100,8 @@ unsigned int ReadLGWheelButtonsForProgressiveMenu(int ix) { wheel_buttons = 0; if (plat_lgwheels && plat_lgwheels->IsConnected(ix)) { - wheel_buttons = reinterpret_cast(plat_lgwheels)[ix].button; + const LGPosition *wheel = &reinterpret_cast(plat_lgwheels)[ix]; + wheel_buttons = wheel->button; } return wheel_buttons; } diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index d5839ed71..2b7e8b5d7 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -22,8 +22,11 @@ Wheels::Wheels() { int channel; for (channel = 0; channel < 4; channel++) { + LGPosition *channelPosition; + WheelsGetWheelHandles(this)[channel] = static_cast(-1); - reinterpret_cast(this)[channel].err = -1; + channelPosition = &reinterpret_cast(this)[channel]; + channelPosition->err = -1; } memset(WheelsGetPositionLast(this), 0, sizeof(LGPosition) * 4); @@ -62,16 +65,16 @@ short Wheels::ReadAll() { } bool Wheels::ButtonIsPressed(long channel, unsigned long buttonMask) { - const LGPosition *position = reinterpret_cast(this); - return (position[channel].button & buttonMask) != 0; + const LGPosition *channelPosition = &reinterpret_cast(this)[channel]; + return (channelPosition->button & buttonMask) != 0; } bool Wheels::IsConnected(long channel) { - const LGPosition *position = reinterpret_cast(this); - return !position[channel].err; + const LGPosition *channelPosition = &reinterpret_cast(this)[channel]; + return !channelPosition->err; } bool Wheels::PedalsConnected(long channel) { - const LGPosition *position = reinterpret_cast(this); - return (position[channel].misc >> 3) & 1; + const LGPosition *channelPosition = &reinterpret_cast(this)[channel]; + return (channelPosition->misc >> 3) & 1; } From a2b2f1b6a2e66119094a4ac11af9731d5d8938bc Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 08:48:29 +0100 Subject: [PATCH 138/172] 89.9%: improve LGWheels SameParams helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 3eb1cb5e6..2e301ed9e 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -498,7 +498,8 @@ void LGWheels::StopDamperForce(long channel) { } bool LGWheels::SameDamperForceParams(long channel, short coefficient) { - return LGWheelsGetDamperForceParams(this)[channel].coefficient == coefficient; + const short *channelCoefficient = &LGWheelsGetDamperForceParams(this)[channel].coefficient; + return *channelCoefficient == coefficient; } void LGWheels::PlayFrontalCollisionForce(long channel, unsigned char magnitude) { @@ -535,7 +536,8 @@ void LGWheels::PlayFrontalCollisionForce(long channel, unsigned char magnitude) } bool LGWheels::SameFrontalCollisionForceParams(long channel, short magnitude) { - return LGWheelsGetFrontalCollisionParams(this)[channel].magnitude == magnitude; + const short *channelMagnitude = &LGWheelsGetFrontalCollisionParams(this)[channel].magnitude; + return *channelMagnitude == magnitude; } void LGWheels::PlayDirtRoadEffect(long channel, unsigned char magnitude) { @@ -587,7 +589,8 @@ void LGWheels::StopDirtRoadEffect(long channel) { } bool LGWheels::SameDirtRoadEffectParams(long channel, short magnitude) { - return LGWheelsGetDirtRoadParams(this)[channel].magnitude == magnitude; + const short *channelMagnitude = &LGWheelsGetDirtRoadParams(this)[channel].magnitude; + return *channelMagnitude == magnitude; } void LGWheels::PlayBumpyRoadEffect(long channel, unsigned char magnitude) { @@ -639,7 +642,8 @@ void LGWheels::StopBumpyRoadEffect(long channel) { } bool LGWheels::SameBumpyRoadEffectParams(long channel, short magnitude) { - return LGWheelsGetBumpyRoadParams(this)[channel].magnitude == magnitude; + const short *channelMagnitude = &LGWheelsGetBumpyRoadParams(this)[channel].magnitude; + return *channelMagnitude == magnitude; } void LGWheels::PlaySlipperyRoadEffect(long channel, short magnitude) { @@ -701,7 +705,8 @@ void LGWheels::StopSlipperyRoadEffect(long channel) { } bool LGWheels::SameSlipperyRoadEffectParams(long channel, short magnitude) { - return LGWheelsGetSlipperyRoadParams(this)[channel].magnitude == magnitude; + const short *channelMagnitude = &LGWheelsGetSlipperyRoadParams(this)[channel].magnitude; + return *channelMagnitude == magnitude; } void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { From 16fb4fa99ddf1fa329c9bd34a64857728a42614b Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 08:52:51 +0100 Subject: [PATCH 139/172] 90.1%: improve LGWheels multi-field comparators Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 2e301ed9e..f8430ff52 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -391,8 +391,10 @@ void LGWheels::StopSpringForce(long channel) { } bool LGWheels::SameSpringForceParams(long channel, signed char offset, unsigned char saturation, short coefficient) { - const SpringForceParams ¶ms = LGWheelsGetSpringForceParams(this)[channel]; - return params.offset == offset && params.saturation == saturation && params.coefficient == coefficient; + const char *channelParams = reinterpret_cast(this) + channel * 4; + return *reinterpret_cast(channelParams + 0x167C) == offset && + *reinterpret_cast(channelParams + 0x167D) == saturation && + *reinterpret_cast(channelParams + 0x167E) == coefficient; } void LGWheels::PlayConstantForce(long channel, short magnitude, unsigned short direction) { @@ -783,8 +785,10 @@ void LGWheels::StopSurfaceEffect(long channel) { } bool LGWheels::SameSurfaceEffectParams(long channel, unsigned char type, unsigned char magnitude, unsigned short period) { - const SurfaceEffectParams ¶ms = LGWheelsGetSurfaceEffectParams(this)[channel]; - return params.type == type && params.magnitude == magnitude && params.period == period; + const char *channelParams = reinterpret_cast(this) + channel * 4; + return *reinterpret_cast(channelParams + 0x16D4) == type && + *reinterpret_cast(channelParams + 0x16D5) == magnitude && + *reinterpret_cast(channelParams + 0x16D6) == period; } void LGWheels::PlayCarAirborne(long channel) { From 70e127cb43f77fb6b21d9111873de0f2224c51b9 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:09:41 +0100 Subject: [PATCH 140/172] 90.1%: match ReadLGWheelButtonsForProgressiveMenu(int) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index a3fcec2c7..a873c5ae5 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -100,8 +100,8 @@ unsigned int ReadLGWheelButtonsForProgressiveMenu(int ix) { wheel_buttons = 0; if (plat_lgwheels && plat_lgwheels->IsConnected(ix)) { - const LGPosition *wheel = &reinterpret_cast(plat_lgwheels)[ix]; - wheel_buttons = wheel->button; + const LGPosition *wheels = reinterpret_cast(plat_lgwheels); + wheel_buttons = wheels[ix].button; } return wheel_buttons; } From 40be2197e3297856735c36a623a7e3132ea033c5 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:21:10 +0100 Subject: [PATCH 141/172] 90.1%: dwarf improve ActualReadJoystickData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index a873c5ae5..49a033aa9 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -182,10 +182,8 @@ int ActualReadJoystickData() { } if (calibrationTimer[port] > 0.0f) { - float now = RealTimer.GetSeconds(); - float elapsed = now - lastCalibTime[port]; - lastCalibTime[port] = now; - calibrationTimer[port] -= elapsed; + calibrationTimer[port] -= RealTimer.GetSeconds() - lastCalibTime[port]; + lastCalibTime[port] = RealTimer.GetSeconds(); } joy_data->padSTATUS.button = reinterpret_cast(plat_lgwheels)[port].button; From 1164acaeb6a06a4d65902ad9ca424af1f037e48c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:35:23 +0100 Subject: [PATCH 142/172] 90.3%: improve eBuildSunPolyFix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 134 ++++++++++++------------- 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index bb6d1dc17..fcf800955 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -116,76 +116,70 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float } void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { - const float PixelFadeAmount = 28.0f; - const float PixelFadeLimit = 28.0f; - float screen_widthf = static_cast(eGetScreenWidth()); - float screen_heightf = static_cast(eGetScreenHeight()); - float layer_intensity = layer->Size; - float main_intensity; - float delta_center_x; - float delta_center_y; + float screen_width = static_cast(eGetScreenWidth()); + float half_size; + float sin_angle; + float cos_angle; + float diagonal0; + float diagonal1; + float intensity; + float center_x; + float center_y; + unsigned char alpha; unsigned short angle; - float max_sweep_angle = 65536.0f; - float scale_x = (x + max_size) / ((screen_widthf + max_size) + max_size); - float sweep_angle; - float rx; - float ry; - float angle_sin; - float angle_cos; - float dx; - float dy; - int a; - float lx; - float ly; - int r; - int g; - int b; - - (void)screen_heightf; - if (layer->Texture == SUNTEX_CENTER && max_size < layer_intensity) { - max_size = layer_intensity; + unsigned char c0; + unsigned char c1; + unsigned char c2; + float sum; + float diff; + + eGetScreenHeight(); + + if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { + max_size = layer->Size; } - rx = layer_intensity * 0.5f; - sweep_angle = layer->SweepAngleAmount * (scale_x * max_sweep_angle); - angle = static_cast(layer->Angle + static_cast(sweep_angle)); - angle_sin = bSin(angle); - angle_cos = bCos(angle); + half_size = layer->Size * 0.5f; + angle = static_cast( + layer->Angle + + static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) + ); + sin_angle = bSin(angle); + cos_angle = bCos(angle); sun_vis_poly_fix_ini[2] = 1.0f; poly->Vertices[1].z = 1.0f; poly->Vertices[2].z = sun_vis_poly_fix_ini[2]; poly->Vertices[3].z = sun_vis_poly_fix_ini[2]; - - ry = rx * angle_cos; poly->Vertices[0].z = sun_vis_poly_fix_ini[2]; - rx *= angle_sin; - dx = ry - rx; - dy = rx + ry; + diagonal1 = half_size * sin_angle; + diagonal0 = half_size * cos_angle; + sum = diagonal1 + diagonal0; + diff = diagonal0 - diagonal1; sun_vis_poly_fix_ini[6] = poly->Vertices[1].z; sun_vis_poly_fix_ini[10] = poly->Vertices[2].z; - main_intensity = layer_intensity * SunVisibility * SunMaxIntensity; + intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; sun_vis_poly_fix_ini[14] = poly->Vertices[3].z; - r = layer->Colour[0]; - delta_center_x = x + layer->OffsetX; - g = layer->Colour[1]; - delta_center_y = y + layer->OffsetY; - b = layer->Colour[2]; - if (PixelFadeLimit <= main_intensity) { - a = static_cast(main_intensity - PixelFadeAmount); + c0 = layer->Colour[0]; + center_x = x + layer->OffsetX; + c1 = layer->Colour[1]; + center_y = y + layer->OffsetY; + c2 = layer->Colour[2]; + if (intensity < 28.0f) { + alpha = static_cast(static_cast(intensity)); } else { - a = static_cast(main_intensity); + alpha = static_cast(static_cast(intensity - 28.0f)); } - poly->Vertices[3].x = delta_center_x - dx; - poly->Vertices[3].y = delta_center_y + dy; - poly->Vertices[0].y = delta_center_y - dx; - sun_vis_poly_fix_ini[0] = delta_center_x - dy; + poly->Vertices[3].x = center_x - diff; + poly->Vertices[3].y = center_y + sum; + poly->Vertices[0].y = center_y - diff; + sun_vis_poly_fix_ini[0] = center_x - sum; poly->Vertices[0].x = sun_vis_poly_fix_ini[0]; - poly->Vertices[1].y = delta_center_y - dy; - poly->Vertices[1].x = delta_center_x + dx; - poly->Vertices[2].y = delta_center_y + dx; - poly->Vertices[2].x = delta_center_x + dy; + poly->Vertices[1].y = center_y - sum; + poly->Vertices[1].x = center_x + diff; + poly->Vertices[2].y = center_y + diff; + poly->Vertices[2].x = center_x + sum; sun_vis_poly_fix_ini[4] = poly->Vertices[1].x; sun_vis_poly_fix_ini[8] = poly->Vertices[2].x; @@ -195,22 +189,22 @@ void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, flo sun_vis_poly_fix_ini[9] = poly->Vertices[2].y; sun_vis_poly_fix_ini[13] = poly->Vertices[3].y; - poly->Colours[0][0] = r; - poly->Colours[0][1] = g; - poly->Colours[0][2] = b; - poly->Colours[0][3] = a; - poly->Colours[1][0] = r; - poly->Colours[1][1] = g; - poly->Colours[1][2] = b; - poly->Colours[1][3] = a; - poly->Colours[2][0] = r; - poly->Colours[2][1] = g; - poly->Colours[2][2] = b; - poly->Colours[3][3] = a; - poly->Colours[3][0] = r; - poly->Colours[3][1] = g; - poly->Colours[3][2] = b; - poly->Colours[2][3] = a; + poly->Colours[0][0] = c0; + poly->Colours[0][1] = c1; + poly->Colours[0][2] = c2; + poly->Colours[0][3] = alpha; + poly->Colours[1][0] = c0; + poly->Colours[1][1] = c1; + poly->Colours[1][2] = c2; + poly->Colours[1][3] = alpha; + poly->Colours[2][0] = c0; + poly->Colours[2][1] = c1; + poly->Colours[2][2] = c2; + poly->Colours[3][3] = alpha; + poly->Colours[3][0] = c0; + poly->Colours[3][1] = c1; + poly->Colours[3][2] = c2; + poly->Colours[2][3] = alpha; } void eUpdateSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, float y) { From 8334341b698b298014e6ae389a7ec305faf97809 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:46:21 +0100 Subject: [PATCH 143/172] 90.3%: improve eBuildSunPoly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index fcf800955..1f43cbfd2 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -49,6 +49,11 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float float center_y; unsigned char alpha; unsigned short angle; + unsigned char c0; + unsigned char c1; + unsigned char c2; + float sum; + float diff; eGetScreenHeight(); @@ -74,6 +79,9 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; center_x = x + layer->OffsetX; center_y = y + layer->OffsetY; + c0 = layer->Colour[0]; + c1 = layer->Colour[1]; + c2 = layer->Colour[2]; if (intensity < 28.0f) { alpha = static_cast(static_cast(intensity)); @@ -81,8 +89,8 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float alpha = static_cast(static_cast(intensity - 28.0f)); } - float sum = diagonal1 + diagonal0; - float diff = diagonal0 - diagonal1; + sum = diagonal1 + diagonal0; + diff = diagonal0 - diagonal1; poly->Vertices[3].x = center_x - diff; poly->Vertices[0].x = center_x - sum; @@ -93,10 +101,6 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float poly->Vertices[2].x = center_x + sum; poly->Vertices[2].y = center_y + diff; - unsigned char c0 = layer->Colour[0]; - unsigned char c1 = layer->Colour[1]; - unsigned char c2 = layer->Colour[2]; - poly->Colours[3][3] = alpha; poly->Colours[3][0] = c0; poly->Colours[3][1] = c1; From d97ddf887e74f821b8e6479dd48b997335833daf Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:51:00 +0100 Subject: [PATCH 144/172] 90.3%: improve eBuildSunPoly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 1f43cbfd2..a50a9ffc9 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -56,12 +56,13 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float float diff; eGetScreenHeight(); + half_size = layer->Size; - if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { - max_size = layer->Size; + if (layer->Texture == SUNTEX_CENTER && half_size > max_size) { + max_size = half_size; } - half_size = layer->Size * 0.5f; + half_size *= 0.5f; angle = static_cast( layer->Angle + static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) From 73589af578b5ade223f43fe385cc52b46589949e Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:52:04 +0100 Subject: [PATCH 145/172] 90.4%: improve eBuildSunPolyFix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index a50a9ffc9..7c6948da4 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -140,11 +140,13 @@ void eBuildSunPolyFix(ePoly *poly, SunLayer *layer, float max_size, float x, flo eGetScreenHeight(); - if (layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { - max_size = layer->Size; + half_size = layer->Size; + + if (layer->Texture == SUNTEX_CENTER && half_size > max_size) { + max_size = half_size; } - half_size = layer->Size * 0.5f; + half_size *= 0.5f; angle = static_cast( layer->Angle + static_cast(layer->SweepAngleAmount * (((x + max_size) / ((screen_width + max_size) + max_size)) * 65536.0f)) From 13a4c9ebdd3df72658915afb3663c8bd558b58e2 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:54:50 +0100 Subject: [PATCH 146/172] 90.4%: improve eBuildSunPoly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 7c6948da4..992baf09c 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -77,11 +77,13 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float diagonal1 = half_size * sin_angle; diagonal0 = half_size * cos_angle; + sum = diagonal1 + diagonal0; + diff = diagonal0 - diagonal1; intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; - center_x = x + layer->OffsetX; - center_y = y + layer->OffsetY; c0 = layer->Colour[0]; + center_x = x + layer->OffsetX; c1 = layer->Colour[1]; + center_y = y + layer->OffsetY; c2 = layer->Colour[2]; if (intensity < 28.0f) { @@ -90,9 +92,6 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float alpha = static_cast(static_cast(intensity - 28.0f)); } - sum = diagonal1 + diagonal0; - diff = diagonal0 - diagonal1; - poly->Vertices[3].x = center_x - diff; poly->Vertices[0].x = center_x - sum; poly->Vertices[3].y = center_y + sum; From 35b2fa14a5e93846c82e5bdcbaa500abd7ab23b8 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 09:58:50 +0100 Subject: [PATCH 147/172] 90.4%: improve eBuildSunPoly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 992baf09c..2da24423f 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -42,8 +42,6 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float float half_size; float sin_angle; float cos_angle; - float diagonal0; - float diagonal1; float intensity; float center_x; float center_y; @@ -75,10 +73,10 @@ void eBuildSunPoly(ePoly *poly, SunLayer *layer, float max_size, float x, float poly->Vertices[2].z = 1.0f; poly->Vertices[3].z = 1.0f; - diagonal1 = half_size * sin_angle; - diagonal0 = half_size * cos_angle; - sum = diagonal1 + diagonal0; - diff = diagonal0 - diagonal1; + sin_angle *= half_size; + cos_angle *= half_size; + sum = sin_angle + cos_angle; + diff = cos_angle - sin_angle; intensity = layer->IntensityScale * SunVisibility * SunMaxIntensity; c0 = layer->Colour[0]; center_x = x + layer->OffsetX; From 23a1e7497dedae4e2b94ef36a636eb28c396ab46 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 10:45:58 +0100 Subject: [PATCH 148/172] 90.6%: improve __static_initialization_and_destruction_0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 2c05cc4f2..dbbc48f40 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -68,12 +68,16 @@ struct XenonEffectLists { XenonEffectVec lists[2]; // [0]=active, [1]=staging XenonEffectLists() { - for (int i = ACTIVE; i <= STAGING; i++) { - lists[i].start = 0; - lists[i].finish = 0; - lists[i].end_of_storage = 0; - reinterpret_cast(lists[i]).reserve(20); - } + int i = STAGING; + XenonEffectVec *list = lists; + + do { + list->start = 0; + list->finish = 0; + list->end_of_storage = 0; + reinterpret_cast(*list).reserve(20); + list++; + } while (i-- != 0); } }; From 974aeee9772684918bca69c75977716420925a07 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 10:55:44 +0100 Subject: [PATCH 149/172] 90.6%: improve ParticleList::AgeParticles(float) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 37 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index dbbc48f40..70dd4af47 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -262,26 +262,29 @@ NGEffect::NGEffect(const XenonEffectDef &eDef) } void ParticleList::AgeParticles(float dt) { - int alive = 0; - int i = 0; + int numOutParticles = 0; + NGParticle *inParticle = mParticles; + NGParticle *outParticle = mParticles; - if (static_cast(mNumParticles) > 0) { - NGParticle *src = mParticles; - NGParticle *dst = mParticles; - do { - if (dt * 8191.0f <= static_cast(src->life)) { - alive++; - *dst = *src; - dst->age += dt; - dst->life = static_cast(static_cast(src->life) - dt * 8191.0f); - dst++; - } - src++; - i++; - } while (i < static_cast(mNumParticles)); + { + int i = 0; + + if (i < static_cast(mNumParticles)) { + do { + if (static_cast(inParticle->life) >= dt * 8191.0f) { + numOutParticles++; + *outParticle = *inParticle; + outParticle->age += dt; + outParticle->life = static_cast(static_cast(inParticle->life) - dt * 8191.0f); + outParticle++; + } + inParticle++; + i++; + } while (i < static_cast(mNumParticles)); + } } - mNumParticles = alive; + mNumParticles = numOutParticles; } void ParticleList::GeneratePolys() { From 5655a71f6a8efcdf2883a90ba0c9c5f968791127 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:25:34 +0100 Subject: [PATCH 150/172] 90.6%: match PlatformInitJoystick Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 2 +- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 36 +++++----- src/Speed/GameCube/Src/Logitech/LGWheels.hpp | 75 +++++++++++++++++++- 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 49a033aa9..111c914d8 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -70,7 +70,7 @@ static inline unsigned char ClampAnalogValue(int value) { void PlatformInitJoystick() { int i; - plat_lgwheels = new LGWheels(); + plat_lgwheels = new LGWheels; for (i = 0; i < 4; i++) { notYetCalibrating[i] = 1; wasWheelConnected[i] = 0; diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index f8430ff52..7e62a8576 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -358,9 +358,9 @@ void LGWheels::PlaySpringForce(long channel, signed char offset, unsigned char s return; } - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].offset = offset; - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].offset = offset; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; return; } @@ -375,9 +375,9 @@ void LGWheels::PlaySpringForce(long channel, signed char offset, unsigned char s } if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].offset = offset; - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; - reinterpret_cast(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].offset = offset; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].saturation = saturation; + reinterpret_cast< ::SpringForceParams *>(reinterpret_cast(this) + 0x167C)[channel].coefficient = coefficient; } reinterpret_cast(reinterpret_cast(this) + 0x11A8)->Start(channel, 0); @@ -445,7 +445,7 @@ void LGWheels::StopConstantForce(long channel) { } bool LGWheels::SameConstantForceParams(long channel, short magnitude, unsigned short direction) { - const ConstantForceParams ¶ms = LGWheelsGetConstantForceParams(this)[channel]; + const ::ConstantForceParams ¶ms = LGWheelsGetConstantForceParams(this)[channel]; return params.magnitude == magnitude && params.direction == direction; } @@ -724,7 +724,7 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char return; } - if (type != reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type) { + if (type != reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].type) { reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Destroy(channel, 3); ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); @@ -735,9 +735,9 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char } if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].period = period; } return; } @@ -746,9 +746,9 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].period = period; } reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); return; @@ -759,7 +759,7 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char return; } - if (type != reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type) { + if (type != reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].type) { reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Destroy(channel, 3); ret = reinterpret_cast(reinterpret_cast(this) + 0x13A8) ->DownloadForce(channel, 3, reinterpret_cast(reinterpret_cast(this) + 0x1050)[channel], type, static_cast(-1), 0, magnitude, 90, period, 0, 0, 0, 0, 0, 0); @@ -769,9 +769,9 @@ void LGWheels::PlaySurfaceEffect(long channel, unsigned char type, unsigned char } if (ret >= 0) { - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].type = type; - reinterpret_cast(reinterpret_cast(this) + 0x16D4)[channel].period = period; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].magnitude = magnitude; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].type = type; + reinterpret_cast< ::SurfaceEffectParams *>(reinterpret_cast(this) + 0x16D4)[channel].period = period; } reinterpret_cast(reinterpret_cast(this) + 0x13A8)->Start(channel, 3); diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp index 3a68866f8..7fec2e02e 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.hpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.hpp @@ -71,24 +71,42 @@ struct LGForceEffect { }; struct Wheels { + // total size: 0x880 Wheels(); ~Wheels(); + void InitLGDevLibrary(); short ReadAll(); + int FirstConnectedPort(); bool ButtonIsPressed(long channel, unsigned long buttonMask); + bool ButtonTriggered(long channel, unsigned long buttonMask); + bool ButtonReleased(long channel, unsigned long buttonMask); bool IsConnected(long channel); bool PedalsConnected(long channel); + bool PowerConnected(long channel); + void GenerateNonLinValues(long channel, unsigned char nonLinCoeff); + float CalculateNonLinValue(int inputValue, unsigned char nonLinearCoeff, short nonLinMinOutput, short nonLinMaxOutput); + + LGPosition Position[4]; // offset 0x0, size 0x28 + short NonLinearWheel[256][4]; // offset 0x28, size 0x800 + unsigned long WheelHandles[4]; // offset 0x828, size 0x10 + unsigned long type[4]; // offset 0x838, size 0x10 + unsigned long WheelHandle[4]; // offset 0x848, size 0x10 + LGPosition PositionLast[4]; // offset 0x858, size 0x28 }; struct Force { + // total size: 0x100 Force(); void InitVars(); int Start(long channel, long forceNumber); int Stop(long channel, long forceNumber); int Destroy(long channel, long forceNumber); + void SetOverallForceGain(unsigned long &handle, int value); + int GetOverallForceGain(unsigned long &handle); - bool Playing[8][4]; - unsigned long EffectID[8][4]; + bool Playing[8][4]; // offset 0x0, size 0x20 + unsigned long EffectID[8][4]; // offset 0x20, size 0x80 }; struct Condition : public Force { @@ -110,10 +128,13 @@ struct Periodic : public Force { }; struct Ramp : public Force { + // total size: 0x100 Ramp(); }; -struct LGWheels { +class LGWheels { + // total size: 0x16E4 + public: LGWheels(); ~LGWheels(); @@ -150,6 +171,54 @@ struct LGWheels { bool SameSurfaceEffectParams(long channel, unsigned char type, unsigned char magnitude, unsigned short period); void PlayCarAirborne(long channel); void StopCarAirborne(long channel); + + private: + LGPosition Position[4]; // offset 0x0, size 0x28 + short NonLinearWheel[256][4]; // offset 0x28, size 0x800 + unsigned char wheels[0x880]; // offset 0x828, size 0x880 + unsigned char force[0x100]; // offset 0x10A8, size 0x100 + unsigned char condition[0x100]; // offset 0x11A8, size 0x100 + unsigned char constant[0x100]; // offset 0x12A8, size 0x100 + unsigned char periodic[0x100]; // offset 0x13A8, size 0x100 + unsigned char ramp[0x100]; // offset 0x14A8, size 0x100 + unsigned char OverallGain; // offset 0x15A8, size 0x1 + bool damperWasPlaying[4]; // offset 0x15AC, size 0x4 + bool springWasPlaying[4]; // offset 0x15BC, size 0x4 + bool wasPlayingBeforeAirborne[10][4]; // offset 0x15CC, size 0x28 + bool IsAirborne[4]; // offset 0x166C, size 0x4 + struct { + char offset; // offset 0x0, size 0x1 + unsigned char saturation; // offset 0x1, size 0x1 + short coefficient; // offset 0x2, size 0x2 + } SpringForceParams[4]; // offset 0x167C, size 0x10 + struct { + short magnitude; // offset 0x0, size 0x2 + unsigned short direction; // offset 0x2, size 0x2 + } ConstantForceParams[4]; // offset 0x168C, size 0x10 + struct { + short coefficient; // offset 0x0, size 0x2 + } DamperForceParams[4]; // offset 0x169C, size 0x8 + struct { + short magnitude; // offset 0x0, size 0x2 + unsigned short direction; // offset 0x2, size 0x2 + } SideCollisionParams[4]; // offset 0x16A4, size 0x10 + struct { + short magnitude; // offset 0x0, size 0x2 + } FrontalCollisionParams[4]; // offset 0x16B4, size 0x8 + struct { + short magnitude; // offset 0x0, size 0x2 + } DirtRoadParams[4]; // offset 0x16BC, size 0x8 + struct { + short magnitude; // offset 0x0, size 0x2 + } BumpyRoadParams[4]; // offset 0x16C4, size 0x8 + struct { + short magnitude; // offset 0x0, size 0x2 + } SlipperyRoadParams[4]; // offset 0x16CC, size 0x8 + struct { + unsigned char type; // offset 0x0, size 0x1 + unsigned char magnitude; // offset 0x1, size 0x1 + unsigned short period; // offset 0x2, size 0x2 + } SurfaceEffectParams[4]; // offset 0x16D4, size 0x10 }; extern LGWheels *plat_lgwheels; From 3ffb8657fa2b53c0e97b8108eb0b804a449d6d74 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:28:15 +0100 Subject: [PATCH 151/172] 90.6%: dwarf improve PlatformInitJoystick Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 111c914d8..5d92aa9f4 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -70,7 +70,7 @@ static inline unsigned char ClampAnalogValue(int value) { void PlatformInitJoystick() { int i; - plat_lgwheels = new LGWheels; + plat_lgwheels = ::new (__FILE__, __LINE__) LGWheels; for (i = 0; i < 4; i++) { notYetCalibrating[i] = 1; wasWheelConnected[i] = 0; From 22315f405bf73f0c99cc5252b5a6bb0b94de0593 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:31:57 +0100 Subject: [PATCH 152/172] 90.6%: match+ LGWheels::LGWheels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 12 +++++---- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 26 +++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index 5d92aa9f4..bd089d86a 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -68,12 +68,14 @@ static inline unsigned char ClampAnalogValue(int value) { } void PlatformInitJoystick() { - int i; + { + int i; - plat_lgwheels = ::new (__FILE__, __LINE__) LGWheels; - for (i = 0; i < 4; i++) { - notYetCalibrating[i] = 1; - wasWheelConnected[i] = 0; + plat_lgwheels = ::new (__FILE__, __LINE__) LGWheels; + for (i = 0; i < 4; i++) { + notYetCalibrating[i] = 1; + wasWheelConnected[i] = 0; + } } PADRead(HardwarePadStatus); JoystickInitialized = 1; diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 7e62a8576..124bd277c 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -130,19 +130,21 @@ static inline SurfaceEffectParams *LGWheelsGetSurfaceEffectParams(LGWheels *self } LGWheels::LGWheels() { - int ii; + { + int ii; + + Wheels_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x828)); + Force_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x10A8)); + Condition_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x11A8)); + Constant_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x12A8)); + Periodic_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x13A8)); + Ramp_Ctor(reinterpret_cast(reinterpret_cast(this) + 0x14A8)); + LGInit(); + *reinterpret_cast(reinterpret_cast(this) + 0x15A8) = 0xFF; - Wheels_Ctor(LGWheelsGetWheels(this)); - Force_Ctor(LGWheelsGetForce(this)); - Condition_Ctor(LGWheelsGetCondition(this)); - Constant_Ctor(LGWheelsGetConstant(this)); - Periodic_Ctor(LGWheelsGetPeriodic(this)); - Ramp_Ctor(LGWheelsGetRamp(this)); - LGInit(); - LGWheelsGetOverallGain(this) = 0xFF; - - for (ii = 0; ii < 4; ii++) { - InitVars(ii); + for (ii = 0; ii < 4; ii++) { + InitVars(ii); + } } } From a2dae4241cb589f44c4a82894f026b5260d5d558 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:33:05 +0100 Subject: [PATCH 153/172] 90.6%: match+ PlatformInitJoystick Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/JoyE.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/JoyE.cpp b/src/Speed/GameCube/Src/JoyE.cpp index bd089d86a..72b32a1f6 100644 --- a/src/Speed/GameCube/Src/JoyE.cpp +++ b/src/Speed/GameCube/Src/JoyE.cpp @@ -68,10 +68,11 @@ static inline unsigned char ClampAnalogValue(int value) { } void PlatformInitJoystick() { + plat_lgwheels = ::new (__FILE__, __LINE__) LGWheels; + { int i; - plat_lgwheels = ::new (__FILE__, __LINE__) LGWheels; for (i = 0; i < 4; i++) { notYetCalibrating[i] = 1; wasWheelConnected[i] = 0; From 700a11966a99d1763eb1fa1f49e1e4a4913a0334 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 12:55:08 +0100 Subject: [PATCH 154/172] dwarf TU scan tool Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/decomp-workflow.py | 364 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index a00215d82..369c76436 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,314 @@ 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 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, + -(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") + ), + } + + filtered_rows = filter_dwarf_scan_rows(scan_rows, args.dwarf_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']}" + ) + + if not filtered_rows: + print("No functions match the given filters.") + return + + print() + print( + f"{'DSTAT':<8} {'DWARF':>7} {'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 "-" + ) + print( + f"{row['dwarf_status']:<8} {dwarf_percent:>7} {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`" + ) + 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 +1527,55 @@ 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( + "--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 34117ccdb5213a47c21f4259a16a7cd6e27f2d98 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:11:56 +0100 Subject: [PATCH 155/172] 90.6%: improve decomp-workflow::dwarf-scan signature filter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/decomp-workflow.py | 51 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tools/decomp-workflow.py b/tools/decomp-workflow.py index 369c76436..78e7fee04 100644 --- a/tools/decomp-workflow.py +++ b/tools/decomp-workflow.py @@ -500,6 +500,20 @@ def filter_dwarf_scan_rows( 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( @@ -508,6 +522,11 @@ def sort_dwarf_scan_rows(rows: List[Dict[str, Any]]) -> None: 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"] @@ -581,9 +600,15 @@ def command_dwarf_scan(args: argparse.Namespace) -> None: 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] @@ -611,6 +636,10 @@ def command_dwarf_scan(args: argparse.Namespace) -> None: "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.") @@ -618,7 +647,7 @@ def command_dwarf_scan(args: argparse.Namespace) -> None: print() print( - f"{'DSTAT':<8} {'DWARF':>7} {'CHG':>4} {'OBJ':>7} {'OSTAT':<10} {'UNM':>6} FUNCTION" + f"{'DSTAT':<8} {'DWARF':>7} {'SIG':>3} {'CHG':>4} {'OBJ':>7} {'OSTAT':<10} {'UNM':>6} FUNCTION" ) print("-" * 120) for row in filtered_rows: @@ -635,8 +664,15 @@ def command_dwarf_scan(args: argparse.Namespace) -> None: 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} {changed_groups:>4} " + 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']}" ) @@ -650,6 +686,11 @@ def command_dwarf_scan(args: argparse.Namespace) -> None: "`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) @@ -1548,6 +1589,12 @@ def build_parser() -> argparse.ArgumentParser: 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, From e445c8b66a42d941fa1c8a39f39a0ac0a1f5a11f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:14:54 +0100 Subject: [PATCH 156/172] 90.6%: match+ LGWheels::ReadAll Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/LGWheels.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp index 124bd277c..0a2c02c02 100644 --- a/src/Speed/GameCube/Src/Logitech/LGWheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/LGWheels.cpp @@ -185,8 +185,8 @@ void LGWheels::InitVars(long channel) { void LGWheels::ReadAll() { short wheelUnplugged; - wheelUnplugged = LGWheelsGetWheels(this)->ReadAll(); - memcpy(this, LGWheelsGetWheels(this), sizeof(LGPosition) * 4); + wheelUnplugged = reinterpret_cast(reinterpret_cast(this) + 0x828)->ReadAll(); + memcpy(this, reinterpret_cast(reinterpret_cast(this) + 0x828), sizeof(LGPosition) * 4); if (wheelUnplugged != -1) { InitVars(wheelUnplugged); } From 97ff40ea2b086e8fb98ec4b8cd0418e3fa8b5fd4 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:23:18 +0100 Subject: [PATCH 157/172] 90.6%: match+ DVDValidErrorState Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 08751614e..11ff97fb7 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -273,20 +273,18 @@ void CheckReset(int resetMode) { } int DVDValidErrorState(int error) { + int errorstate; + switch (error) { - case 5: - return 5; - case 4: - return 4; - case 6: - return 6; - case 11: - return 11; - case -1: - return -1; - default: - return 0; + case 5: errorstate = 5; break; + case 4: errorstate = 4; break; + case 6: errorstate = 6; break; + case 11: errorstate = 11; break; + case -1: errorstate = -1; break; + default: errorstate = 0; break; } + + return errorstate; } void DVDErrorTask(void *, int) { From fcc0cdf4a798708d9097dd6c0111def7172c7d48 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:27:00 +0100 Subject: [PATCH 158/172] 90.6%: match+ GetMemcard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index 70578dec6..f6284184a 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -100,7 +100,7 @@ MemoryCard *GetMemcard(); extern const char *gComment1; MemoryCard *GetMemcard() { - return MemoryCard::s_pThis; + return MemoryCard::GetInstance(); } const char *MemoryCardImp::GetPrefix() { From 16d79e47676e790055176a78a28f6f9c72115ab7 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:29:13 +0100 Subject: [PATCH 159/172] 90.6%: match+ PlatSetFirstMovieFrame Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Movie_GC.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Movie_GC.cpp b/src/Speed/GameCube/Src/Movie_GC.cpp index 8c31f3412..8d53cd0a5 100644 --- a/src/Speed/GameCube/Src/Movie_GC.cpp +++ b/src/Speed/GameCube/Src/Movie_GC.cpp @@ -232,7 +232,7 @@ void GCDrawMovie() { void PlatSetFirstMovieFrame(TextureInfo *texture_info, RealShape::Shape *yuv_shape, bool isVP6Movie) { if (!gGCVD) { - gGCVD = new GCHW_VD(yuv_shape, isVP6Movie); + gGCVD = ::new (__FILE__, __LINE__) GCHW_VD(yuv_shape, isVP6Movie); } } From 8825cce4382affd4b1c6a7043612573f1ae1e972 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:35:30 +0100 Subject: [PATCH 160/172] 90.6%: match+ MemoryCardImp::ConstructSaveInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GameCube/Src/MemoryCard/MemoryCardImp.cpp | 37 +++++++++++-------- .../Src/Frontend/MemoryCard/MemoryCard.hpp | 13 +++++++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp index f6284184a..28bdacaf9 100644 --- a/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp +++ b/src/Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp @@ -108,27 +108,34 @@ const char *MemoryCardImp::GetPrefix() { } RealmcIface::SaveInfo *MemoryCardImp::ConstructSaveInfo(MemoryCard::SaveType type, const char *DisplayName, int aSize) { + RealmcIface::SaveInfo *pInfo; static char sDisplayName[32]; - RealmcIface::SaveInfo *save_info = new RealmcIface::SaveInfo; + + pInfo = ::new (__FILE__, __LINE__) RealmcIface::SaveInfo; if (type == MemoryCard::ST_PROFILE) { bStrCpy(sDisplayName, DisplayName); } - save_info->mGcInfo.mComment1 = gComment1; - save_info->mGcInfo.mComment1Size = bStrLen(gComment1); - save_info->mGcInfo.mComment2 = sDisplayName; - save_info->mGcInfo.mComment2Size = bStrLen(sDisplayName); - save_info->mGcInfo.mIconDataInfo = - *reinterpret_cast(reinterpret_cast(MemoryCard::s_pThis) + 0x10); - save_info->mGcInfo.mBannerDataInfo = - *reinterpret_cast(reinterpret_cast(MemoryCard::s_pThis) + 0x14); - this->m_SaveReq.mSaveInfo = save_info; - save_info->mTypeName = MemoryCardImp::gEntryType[type]; - save_info->mContentName = MemoryCardImp::gContentName; - save_info->mHeaderSize = 8; - save_info->mBodySize = aSize; - return save_info; + { + int len; + + pInfo->mGcInfo.mComment1 = gComment1; + len = bStrLen(gComment1); + pInfo->mGcInfo.mComment1Size = len; + pInfo->mGcInfo.mComment2 = sDisplayName; + len = bStrLen(sDisplayName); + pInfo->mGcInfo.mComment2Size = len; + } + + pInfo->mGcInfo.mIconDataInfo = MemoryCard::GetInstance()->GetSaveIcon(); + pInfo->mGcInfo.mBannerDataInfo = MemoryCard::GetInstance()->GetSaveBanner(); + this->m_SaveReq.mSaveInfo = pInfo; + pInfo->mTypeName = MemoryCardImp::gEntryType[type]; + pInfo->mContentName = MemoryCardImp::gContentName; + pInfo->mHeaderSize = 8; + pInfo->mBodySize = aSize; + return pInfo; } void MemoryCardImp::DestructSaveInfo() { diff --git a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp index 5095431b4..3d94f6ee2 100644 --- a/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp +++ b/src/Speed/Indep/Src/Frontend/MemoryCard/MemoryCard.hpp @@ -5,6 +5,11 @@ #pragma once #endif +namespace RealmcIface { +struct GCIconDataInfo; +struct GCBannerDataInfo; +} + class MemoryCard { public: enum SaveType { @@ -20,6 +25,14 @@ class MemoryCard { return s_pThis; } + RealmcIface::GCIconDataInfo *GetSaveIcon() { + return *reinterpret_cast(reinterpret_cast(this) + 0x10); + } + + RealmcIface::GCBannerDataInfo *GetSaveBanner() { + return *reinterpret_cast(reinterpret_cast(this) + 0x14); + } + static MemoryCard *s_pThis; static int IsCardBusy(); }; From 2d052d14f50f69de9da7045cdbf90a5fa41d3c5d Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 13:57:00 +0100 Subject: [PATCH 161/172] fix build --- src/Speed/Indep/SourceLists/zPlatform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Speed/Indep/SourceLists/zPlatform.cpp b/src/Speed/Indep/SourceLists/zPlatform.cpp index fe94e4c08..e5bc21880 100644 --- a/src/Speed/Indep/SourceLists/zPlatform.cpp +++ b/src/Speed/Indep/SourceLists/zPlatform.cpp @@ -1,3 +1,4 @@ +#ifdef EA_PLATFORM_GAMECUBE #include "Speed/GameCube/Src/Platform_G.cpp" #include "Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp" @@ -31,3 +32,4 @@ #include "Speed/GameCube/Src/xSparks.cpp" #include "Speed/GameCube/Src/MemoryCard/MemoryCardImp.cpp" +#endif From c91ad034becb958f24a8de4181dcb8beb35f9cba Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:10:08 +0100 Subject: [PATCH 162/172] 90.6%: match+ InitDisplaySystem Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Platform_G.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/Platform_G.cpp b/src/Speed/GameCube/Src/Platform_G.cpp index 11ff97fb7..b69e85242 100644 --- a/src/Speed/GameCube/Src/Platform_G.cpp +++ b/src/Speed/GameCube/Src/Platform_G.cpp @@ -233,10 +233,14 @@ int eSetDisplaySystem(int video_mode); void InitDisplaySystem() { if (bEURGB60) { - SetVideoMode(MODE_PAL60); + int video_mode = MODE_PAL60; + + SetVideoMode(static_cast(video_mode)); eSetDisplaySystem(GetVideoMode()); } else { - SetVideoMode(GetBuildRegionVideoMode()); + int video_mode = GetBuildRegionVideoMode(); + + SetVideoMode(static_cast(video_mode)); eSetDisplaySystem(GetVideoMode()); } } From 8b54f226065cff252ea5708103bf9fa790519bf3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:14:39 +0100 Subject: [PATCH 163/172] 90.6%: match+ TextureInfoPlatInterface::SetAnimData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index c25779946..7a5efcc02 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -108,12 +108,11 @@ void TextureInfoPlatInterface::ReleaseAnimData(void *anim_data) { } void TextureInfoPlatInterface::SetAnimData(void *anim_data) { - TextureInfo *info = static_cast(this); TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); - unsigned int *val = reinterpret_cast(anim_data); + TextureInfo *info = static_cast(this); - info->ImageData = reinterpret_cast(val[0]); - info->PaletteData = reinterpret_cast(val[1]); + info->ImageData = reinterpret_cast(reinterpret_cast(anim_data)[0]); + info->PaletteData = reinterpret_cast(reinterpret_cast(anim_data)[1]); plat_info->SetImage(info); } From 7e0462b79fb873c24c9e08f8c857a212643f73f3 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:16:00 +0100 Subject: [PATCH 164/172] 90.6%: match+ TextureInfoPlatInterface::CreateAnimData Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp index 7a5efcc02..69336583c 100644 --- a/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/TextureInfoPlat.cpp @@ -94,8 +94,8 @@ void TextureInfoPlatInterface::UnlockPalette(void *palette_lock) { } void *TextureInfoPlatInterface::CreateAnimData() { - TextureInfo *info = static_cast(this); TextureInfoPlatInfo *plat_info = this->GetPlatInfo(); + TextureInfo *info = static_cast(this); unsigned int *val = reinterpret_cast(bOMalloc(eAnimTextureSlotPool)); val[0] = reinterpret_cast(info->ImageData); From 382e44b207a589939355f67e390635d770592a23 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:26:36 +0100 Subject: [PATCH 165/172] 90.6%: match+ ParticleList::GeneratePolys Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 70dd4af47..f4f3686ca 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -288,13 +288,15 @@ void ParticleList::AgeParticles(float dt) { } void ParticleList::GeneratePolys() { + NGParticle *particle; + if (mNumParticles != 0) { if (!mContrail_tex) { mContrail_tex = GetTextureInfo(bStringHash("PS2_CONTRAIL"), 0, 0); mSparks_tex = GetTextureInfo(bStringHash("PS2_SPARKS"), 0, 0); } - NGParticle *particle = mParticles; + particle = mParticles; for (unsigned int i = 0; i < mNumParticles; i++) { if (particle->uv[0] == 0x7f) { mCurrentTexture = mContrail_tex; From 0570f4182f24e3e2281c77635b951fcac5fac857 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:34:01 +0100 Subject: [PATCH 166/172] 90.6%: match+ CGEmitter::CGEmitter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 19 +++++++++++++------ .../Generated/AttribSys/Classes/emitteruv.h | 8 ++++---- .../AttribSys/Classes/fuelcell_effect.h | 8 ++++---- .../AttribSys/Classes/fuelcell_emitter.h | 8 ++++---- .../Indep/Tools/AttribSys/Runtime/AttribSys.h | 6 ++++++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index f4f3686ca..3a9614dea 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -250,13 +250,20 @@ void CGEmitter::SpawnParticles(float dt, float intensity) { NGEffect::NGEffect(const XenonEffectDef &eDef) : mEffectDef(eDef.spec, 0, nullptr) { + int numEmitters; + if (mEffectDef.IsValid()) { - int i = 0; - int length = mEffectDef.Num_NGEmitter(); - while (i < length) { - CGEmitter emitter(mEffectDef.NGEmitter(i).GetCollection(), eDef); - emitter.SpawnParticles(1.0f / 30.0f, 1.0f); - i++; + numEmitters = mEffectDef.Num_NGEmitter(); + { + int i = 0; + + while (i < numEmitters) { + const Attrib::Collection *emspec = mEffectDef.NGEmitter(i).GetCollection(); + CGEmitter anEmitter(emspec, eDef); + + anEmitter.SpawnParticles(1.0f / 30.0f, 1.0f); + i++; + } } } } diff --git a/src/Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h b/src/Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h index 6d4975d0e..3d70348d7 100644 --- a/src/Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h +++ b/src/Speed/Indep/Src/Generated/AttribSys/Classes/emitteruv.h @@ -33,19 +33,19 @@ struct emitteruv : Instance { emitteruv(Key collectionKey, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(FindCollection(ClassKey(), collectionKey), msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } emitteruv(const Collection *collection, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(collection, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } emitteruv(const emitteruv &src) : Instance(src) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } emitteruv(const RefSpec &refspec, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(refspec, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } ~emitteruv() {} diff --git a/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h b/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h index 0f4f45973..da93ea414 100644 --- a/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h +++ b/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_effect.h @@ -30,19 +30,19 @@ struct fuelcell_effect : Instance { fuelcell_effect(Key collectionKey, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(FindCollection(ClassKey(), collectionKey), msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_effect(const Collection *collection, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(collection, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_effect(const fuelcell_effect &src) : Instance(src) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_effect(const RefSpec &refspec, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(refspec, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } ~fuelcell_effect() {} diff --git a/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h b/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h index d2f732422..f38e61740 100644 --- a/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h +++ b/src/Speed/Indep/Src/Generated/AttribSys/Classes/fuelcell_emitter.h @@ -48,19 +48,19 @@ struct fuelcell_emitter : Instance { fuelcell_emitter(Key collectionKey, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(FindCollection(ClassKey(), collectionKey), msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_emitter(const Collection *collection, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(collection, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_emitter(const fuelcell_emitter &src) : Instance(src) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } fuelcell_emitter(const RefSpec &refspec, unsigned int msgPort, UTL::COM::IUnknown *owner) : Instance(refspec, msgPort, owner) { - SetDefaultLayout(sizeof(_LayoutStruct)); + static_cast(this)->SetDefaultLayout(sizeof(_LayoutStruct)); } ~fuelcell_emitter() {} diff --git a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h index f458308a4..f7053e939 100644 --- a/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h +++ b/src/Speed/Indep/Tools/AttribSys/Runtime/AttribSys.h @@ -779,6 +779,12 @@ class Instance { } } + void SetDefaultLayout(unsigned int bytes) const { + if (mLayoutPtr == nullptr) { + const_cast(this)->mLayoutPtr = const_cast(DefaultDataArea(bytes)); + } + } + bool IsValid() const { return mCollection != nullptr; } From a3eaf089217537cb825616f72c85e3452af1317c Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:44:58 +0100 Subject: [PATCH 167/172] 90.6%: dwarf improve PlatAddParticle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/AcidFX_G.cpp | 50 ++++++++----------- .../dolphinsdk/include/dolphin/gx/GXVert.h | 10 ++-- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp index 83d86a66a..2bd5d632e 100644 --- a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp +++ b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp @@ -74,53 +74,45 @@ void PlatEndParticleRender() { void PlatAddParticle(const EmitterParticle &particle, const UMath::Vector3 &upVec, const UMath::Vector3 &rightVec, unsigned int hack_flags, bVector4 *x_constrain_basis, bVector4 *y_constrain_basis) { - unsigned int uv_start_u = particle.mUVStart >> 16; - unsigned int uv_start_v = particle.mUVStart & 0xFFFF; - unsigned int uv_end_u = particle.mUVEnd >> 16; - unsigned int uv_end_v = particle.mUVEnd & 0xFFFF; - - float size = particle.mSize; - UMath::Vector3 bx; - bx.x = BillboardedParticleBasisX.x * size; - bx.y = BillboardedParticleBasisX.y * size; - bx.z = BillboardedParticleBasisX.z * size; - UMath::Vector3 by; - by.x = BillboardedParticleBasisY.x * size; - by.y = BillboardedParticleBasisY.y * size; - by.z = BillboardedParticleBasisY.z * size; - - float u0 = static_cast(uv_start_u) * (1.0f / 65535.0f); - float v0 = static_cast(uv_start_v) * (1.0f / 65535.0f); - float u1 = static_cast(uv_end_u) * (1.0f / 65535.0f); - float v1 = static_cast(uv_end_v) * (1.0f / 65535.0f); - - unsigned int color = particle.mColour; + float particle_scale_factor = particle.mSize; + bVector3 bx(BillboardedParticleBasisX.x * particle_scale_factor, + BillboardedParticleBasisX.y * particle_scale_factor, + BillboardedParticleBasisX.z * particle_scale_factor); + bVector3 by(BillboardedParticleBasisY.x * particle_scale_factor, + BillboardedParticleBasisY.y * particle_scale_factor, + BillboardedParticleBasisY.z * particle_scale_factor); + unsigned int colour = particle.mColour; + const float fs0 = static_cast(particle.mUVStart >> 16) * (1.0f / 65535.0f); + const float ft0 = static_cast(particle.mUVStart & 0xFFFF) * (1.0f / 65535.0f); + const float fs1 = static_cast(particle.mUVEnd >> 16) * (1.0f / 65535.0f); + const float ft1 = static_cast(particle.mUVEnd & 0xFFFF) * (1.0f / 65535.0f); GXBegin(GX_QUADS, static_cast(crtVtxFmt), 4); GXPosition3f32(particle.mPosX + bx.x + by.x, particle.mPosY + bx.y + by.y, particle.mPosZ + bx.z + by.z); - GXColor1u32(color); - GXTexCoord2f32(u1, v1); + GXColor1u32(colour); + GXTexCoord2f32(fs1, ft1); GXPosition3f32(particle.mPosX - bx.x + by.x, particle.mPosY - bx.y + by.y, particle.mPosZ - bx.z + by.z); - GXColor1u32(color); - GXTexCoord2f32(u0, v1); + GXColor1u32(colour); + GXTexCoord2f32(fs0, ft1); GXPosition3f32(particle.mPosX - bx.x - by.x, particle.mPosY - bx.y - by.y, particle.mPosZ - bx.z - by.z); - GXColor1u32(color); - GXTexCoord2f32(u0, v0); + GXColor1u32(colour); + GXTexCoord2f32(fs0, ft0); GXPosition3f32(particle.mPosX + bx.x - by.x, particle.mPosY + bx.y - by.y, particle.mPosZ + bx.z - by.z); - GXColor1u32(color); - GXTexCoord2f32(u1, v0); + GXColor1u32(colour); + GXTexCoord2f32(fs1, ft0); + GXEnd(); } void PlatGetViewVectors(eView *view, UMath::Vector3 &right, UMath::Vector3 &up, UMath::Vector3 &forward) { diff --git a/src/Speed/GameCube/bWare/GameCube/dolphinsdk/include/dolphin/gx/GXVert.h b/src/Speed/GameCube/bWare/GameCube/dolphinsdk/include/dolphin/gx/GXVert.h index 0528c2c4c..6a950c06f 100644 --- a/src/Speed/GameCube/bWare/GameCube/dolphinsdk/include/dolphin/gx/GXVert.h +++ b/src/Speed/GameCube/bWare/GameCube/dolphinsdk/include/dolphin/gx/GXVert.h @@ -126,8 +126,8 @@ static inline void GXNormal3f32(const f32 x, const f32 y, const f32 z) { GXWGFifo.f32 = z; } -static inline void GXColor1u32(const u32 v) { - GXWGFifo.u32 = v; +static inline void GXColor1u32(const u32 x) { + GXWGFifo.u32 = x; } static inline void GXColor3u8(const u8 r, const u8 g, const u8 b) { @@ -148,9 +148,9 @@ static inline void GXTexCoord2s16(const s16 u, const s16 v) { GXWGFifo.s16 = v; } -static inline void GXTexCoord2f32(const f32 u, const f32 v) { - GXWGFifo.f32 = u; - GXWGFifo.f32 = v; +static inline void GXTexCoord2f32(const f32 x, const f32 y) { + GXWGFifo.f32 = x; + GXWGFifo.f32 = y; } static inline void GXPosition1x8(u8 index) { From 4b39db88549d930848149cf7f017a4f9b31386c2 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 14:55:48 +0100 Subject: [PATCH 168/172] 90.6%: dwarf improve eRenderSun Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/SunE.cpp | 53 +++++++++++++++----------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/SunE.cpp b/src/Speed/GameCube/Src/Render/SunE.cpp index 2da24423f..827fa6858 100644 --- a/src/Speed/GameCube/Src/Render/SunE.cpp +++ b/src/Speed/GameCube/Src/Render/SunE.cpp @@ -265,21 +265,22 @@ void eCalcSunVisibility(eView *view, float x, float y) { void eRenderSun(eView *view) { SunChunkInfo *sun_info = SunInfo; + Camera *camera; + bMatrix4 *world_view; + bVector4 position3d; + bVector4 position2d; + bVector4 view3d; + float screen_widthf; + float screen_heightf; + float x; + float y; + float max_size; SetCurrentSunInfo(); if (IsGameFlowInGame()) { - Camera *camera = view->GetCamera(); - bMatrix4 *world_view = camera->GetCameraMatrix(); - bVector4 position3d; - bVector4 position2d; - bVector4 view3d; - float screen_widthf; - float screen_heightf; - float x; - float y; - float max_size; - + camera = view->GetCamera(); + world_view = camera->GetCameraMatrix(); position3d.x = sun_info->PositionX; position3d.y = sun_info->PositionY; position3d.z = sun_info->PositionZ; @@ -299,11 +300,15 @@ void eRenderSun(eView *view) { max_size = 0.0f; - for (int i = 0; i < 4; i++) { - SunLayer *layer = &sun_info->SunLayers[i]; + { + int i; + + for (i = 0; i < 4; i++) { + SunLayer *layer = &sun_info->SunLayers[i]; - if (layer->IntensityScale > 0.0f && layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { - max_size = layer->Size; + if (layer->IntensityScale > 0.0f && layer->Texture == SUNTEX_CENTER && layer->Size > max_size) { + max_size = layer->Size; + } } } @@ -314,15 +319,19 @@ void eRenderSun(eView *view) { eRecalculateOthographicProjection(1, 0.0f); eSetOrthographicMatrixToHW(); - for (int i = 0; i < 4; i++) { - SunLayer *layer = &sun_info->SunLayers[i]; - TextureInfo *texture_info = SunTextures[layer->Texture]; + { + int i; + + for (i = 0; i < 4; i++) { + SunLayer *layer = &sun_info->SunLayers[i]; + TextureInfo *texture_info = SunTextures[layer->Texture]; - if (texture_info) { - ePoly sun_poly; + if (texture_info) { + ePoly sun_poly; - eBuildSunPoly(&sun_poly, layer, max_size, x, y); - RenderViewPoly(view, &sun_poly, texture_info, 0); + eBuildSunPoly(&sun_poly, layer, max_size, x, y); + RenderViewPoly(view, &sun_poly, texture_info, 0); + } } } } From cee7e9967d468aec8c90559db6af196d4e5ca35f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 15:09:02 +0100 Subject: [PATCH 169/172] 90.6%: match+ ClearXenonEmitters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/xSparks.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Speed/GameCube/Src/xSparks.cpp b/src/Speed/GameCube/Src/xSparks.cpp index 3a9614dea..439d54638 100644 --- a/src/Speed/GameCube/Src/xSparks.cpp +++ b/src/Speed/GameCube/Src/xSparks.cpp @@ -320,8 +320,8 @@ void ParticleList::GeneratePolys() { void DrawXenonEmitters(eView *view) {} void ClearXenonEmitters() { - gNGEffectList.lists[XenonEffectLists::ACTIVE].clear(); - gNGEffectList.lists[XenonEffectLists::STAGING].clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::ACTIVE]).clear(); + reinterpret_cast(gNGEffectList.lists[XenonEffectLists::STAGING]).clear(); } void AddXenonEffect(EmitterGroup *piggyback_fx, const Attrib::Collection *spec, const UMath::Matrix4 *mat, const UMath::Vector4 *vel) { From 49f4d0de5542ea483ab36ddd76e0bd7438ef8035 Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 15:12:18 +0100 Subject: [PATCH 170/172] 90.6%: match+ PlatAddParticle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Render/AcidFX_G.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp index 2bd5d632e..9c5b55c18 100644 --- a/src/Speed/GameCube/Src/Render/AcidFX_G.cpp +++ b/src/Speed/GameCube/Src/Render/AcidFX_G.cpp @@ -75,12 +75,12 @@ void PlatEndParticleRender() { void PlatAddParticle(const EmitterParticle &particle, const UMath::Vector3 &upVec, const UMath::Vector3 &rightVec, unsigned int hack_flags, bVector4 *x_constrain_basis, bVector4 *y_constrain_basis) { float particle_scale_factor = particle.mSize; - bVector3 bx(BillboardedParticleBasisX.x * particle_scale_factor, - BillboardedParticleBasisX.y * particle_scale_factor, - BillboardedParticleBasisX.z * particle_scale_factor); - bVector3 by(BillboardedParticleBasisY.x * particle_scale_factor, - BillboardedParticleBasisY.y * particle_scale_factor, - BillboardedParticleBasisY.z * particle_scale_factor); + bVector3 bx(BillboardedParticleBasisX.x * particle.mSize, + BillboardedParticleBasisX.y * particle.mSize, + BillboardedParticleBasisX.z * particle.mSize); + bVector3 by(BillboardedParticleBasisY.x * particle.mSize, + BillboardedParticleBasisY.y * particle.mSize, + BillboardedParticleBasisY.z * particle.mSize); unsigned int colour = particle.mColour; const float fs0 = static_cast(particle.mUVStart >> 16) * (1.0f / 65535.0f); const float ft0 = static_cast(particle.mUVStart & 0xFFFF) * (1.0f / 65535.0f); From fc36af2b9153132209e0b85fd00b6592d009eb6a Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 15:31:54 +0100 Subject: [PATCH 171/172] 90.6%: match+ Wheels::Wheels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Logitech/Wheels.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Speed/GameCube/Src/Logitech/Wheels.cpp b/src/Speed/GameCube/Src/Logitech/Wheels.cpp index 2b7e8b5d7..2da969176 100644 --- a/src/Speed/GameCube/Src/Logitech/Wheels.cpp +++ b/src/Speed/GameCube/Src/Logitech/Wheels.cpp @@ -22,14 +22,11 @@ Wheels::Wheels() { int channel; for (channel = 0; channel < 4; channel++) { - LGPosition *channelPosition; - - WheelsGetWheelHandles(this)[channel] = static_cast(-1); - channelPosition = &reinterpret_cast(this)[channel]; - channelPosition->err = -1; + WheelHandles[channel] = static_cast(-1); + Position[channel].err = -1; } - memset(WheelsGetPositionLast(this), 0, sizeof(LGPosition) * 4); + memset(PositionLast, 0, sizeof(LGPosition) * 4); } short Wheels::ReadAll() { From de2f7cf359c2951e739e4e57088b2cc9ae37da5f Mon Sep 17 00:00:00 2001 From: Johann Berger Date: Sat, 21 Mar 2026 15:35:20 +0100 Subject: [PATCH 172/172] 90.6%: match+ XSpriteManager::AddSpark Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Speed/GameCube/Src/Ecstasy/xSprites.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp index d881d9d82..810daa345 100644 --- a/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp +++ b/src/Speed/GameCube/Src/Ecstasy/xSprites.cpp @@ -47,6 +47,7 @@ void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTe UMath::ScaleAdd(particle.vel, particle.age, particle.initialPos, startPos); startPos.z += particle.gravity * particle.age * particle.age; float endAge = static_cast(static_cast(particle.length)) * (1.0f / 2048.0f) + particle.age; + float width = static_cast(static_cast(particle.width)) * (1.0f / 2048.0f); UMath::ScaleAdd(particle.vel, endAge, particle.initialPos, endPos); @@ -58,9 +59,7 @@ void XSpriteManager::AddSpark(const NGParticle &particle, TextureInfo *CurrentTe particle.color >> 24 | particle.color >> 8 & 0xFF00 | (particle.color & 0xFF00) << 8 | particle.color << 24; XSpriteBufferP->startPos = startPos; XSpriteBufferP->EndPosPos = endPos; - - float width = static_cast(static_cast(particle.width)) * (1.0f / 2048.0f); - XSpriteBufferP->width = width; + XSpriteBufferP->width = static_cast(static_cast(particle.width)) * (1.0f / 2048.0f); this->position++; } }